feat: complete Phase 2 core architecture, DTOs, and driver contract #14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI – Maatify Security Guard | |
| on: | |
| push: | |
| branches: [ main, develop ] | |
| pull_request: | |
| branches: [ main, develop ] | |
| jobs: | |
| test: | |
| name: 🧪 Security Guard Tests | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| env: | |
| GH_START_ISO: ${{ github.event.head_commit.timestamp }} | |
| APP_ENV: testing | |
| # Redis | |
| REDIS_HOST: 127.0.0.1 | |
| REDIS_PORT: 6379 | |
| REDIS_PASS: "" | |
| REDIS_DSN: "redis://127.0.0.1:6379" | |
| # Mongo | |
| MONGO_HOST: 127.0.0.1 | |
| MONGO_PORT: 27017 | |
| MONGO_DB: security_guard_test | |
| MONGO_DSN: "mongodb://127.0.0.1:27017/security_guard_test" | |
| # MySQL | |
| MYSQL_HOST: 127.0.0.1 | |
| MYSQL_PORT: 3306 | |
| MYSQL_USER: root | |
| MYSQL_PASS: root | |
| MYSQL_DB: security_guard_test | |
| MYSQL_DSN: "mysql:host=127.0.0.1;dbname=security_guard_test;charset=utf8mb4" | |
| services: | |
| redis: | |
| image: redis:7 | |
| ports: | |
| - "6379:6379" | |
| mongo: | |
| image: mongo:7 | |
| ports: | |
| - "27017:27017" | |
| mysql: | |
| image: mysql:8.0 | |
| env: | |
| MYSQL_ROOT_PASSWORD: root | |
| MYSQL_DATABASE: security_guard_test | |
| MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password | |
| ports: | |
| - "3306:3306" | |
| steps: | |
| # --------------------------------------------- | |
| # 1) Checkout | |
| # --------------------------------------------- | |
| - name: 📦 Checkout Repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| # --------------------------------------------- | |
| # 2) PHP Setup | |
| # --------------------------------------------- | |
| - uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: "8.4" | |
| coverage: pcov | |
| extensions: mbstring, intl, pdo, pdo_mysql, redis, mongodb | |
| tools: composer | |
| # --------------------------------------------- | |
| # 3) Composer cache | |
| # --------------------------------------------- | |
| - name: 🧰 Cache Composer | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.composer/cache | |
| ~/.cache/composer | |
| key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} | |
| restore-keys: ${{ runner.os }}-composer- | |
| - name: 🔑 Configure Composer GitHub token | |
| run: composer config -g github-oauth.github.com "${{ github.token }}" | |
| # --------------------------------------------- | |
| # 4) Install dependencies | |
| # --------------------------------------------- | |
| - name: 📥 Install dependencies | |
| run: composer install --no-interaction --prefer-dist --no-progress | |
| - name: ⚙️ Rebuild Autoload | |
| run: composer dump-autoload --optimize | |
| # --------------------------------------------- | |
| # 5) Wait for MySQL | |
| # --------------------------------------------- | |
| - name: 🕒 Wait for MySQL | |
| run: | | |
| for i in {1..25}; do | |
| mysqladmin ping -h $MYSQL_HOST --silent && break | |
| echo "⏳ Waiting for MySQL..." | |
| sleep 2 | |
| done | |
| - name: 🗄️ Init MySQL database | |
| run: | | |
| mysql -h $MYSQL_HOST -u$MYSQL_USER -p$MYSQL_PASS \ | |
| -e "CREATE DATABASE IF NOT EXISTS security_guard_test;" | |
| # --------------------------------------------- | |
| # 6) PHPStan | |
| # --------------------------------------------- | |
| - name: 🧹 Run PHPStan | |
| id: phpstan | |
| run: | | |
| set +e | |
| vendor/bin/phpstan analyse src tests --level=max > phpstan.log | |
| EXIT=${?} | |
| ERR=$(grep -Eo "Found [0-9]+ errors" phpstan.log | grep -Eo "[0-9]+") | |
| echo "phpstan_exit=$EXIT" >> $GITHUB_OUTPUT | |
| echo "phpstan_errors=${ERR:-0}" >> $GITHUB_OUTPUT | |
| - name: 📄 Show PHPStan output | |
| if: always() | |
| run: cat phpstan.log | |
| # --------------------------------------------- | |
| # 7) PHPUnit + Coverage | |
| # --------------------------------------------- | |
| - name: Install bc | |
| run: sudo apt-get install -y bc | |
| - name: 🧪 Run PHPUnit with Coverage | |
| id: tests | |
| run: | | |
| touch phpunit.log | |
| php -d pcov.enabled=1 \ | |
| -d pcov.directory=./src \ | |
| -d pcov.exclude="~(tests|vendor)~" \ | |
| vendor/bin/phpunit \ | |
| --configuration phpunit.xml.dist \ | |
| --coverage-clover coverage.xml \ | |
| --coverage-html coverage \ | |
| | tee -a phpunit.log | |
| # --------------------------------------------- | |
| # 8) Extract Coverage | |
| # --------------------------------------------- | |
| - name: Extract Coverage % (Clover) | |
| id: coverage | |
| run: | | |
| if [ ! -f coverage.xml ]; then | |
| echo "coverage=0" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| COV=$(php -r ' | |
| $xml=@simplexml_load_file("coverage.xml"); | |
| if(!$xml){ echo "0"; exit; } | |
| $m = $xml->project->metrics ?? null; | |
| if(!$m){ | |
| $statements=0; $covered=0; | |
| foreach($xml->xpath("//metrics") ?: [] as $mx){ | |
| $statements += (int)$mx["statements"]; | |
| $covered += (int)$mx["coveredstatements"]; | |
| } | |
| echo $statements>0 ? (int)round(($covered/$statements)*100) : 0; | |
| exit; | |
| } | |
| $statements=(int)$m["statements"]; | |
| $covered=(int)$m["coveredstatements"]; | |
| echo $statements>0 ? (int)round(($covered/$statements)*100) : 0; | |
| ') | |
| echo "Coverage: ${COV}%" | |
| echo "coverage=${COV}" >> $GITHUB_OUTPUT | |
| - name: Generate Coverage Badge JSON | |
| run: | | |
| echo "{ | |
| \"schemaVersion\": 1, | |
| \"label\": \"coverage\", | |
| \"message\": \"${{ steps.coverage.outputs.coverage }}%\", | |
| \"color\": \"9C27B0\" | |
| }" > coverage-badge.json | |
| - name: Stash CI artifacts (before switching branches) | |
| if: github.event_name == 'push' | |
| run: | | |
| cp coverage-badge.json /tmp/coverage-badge.json | |
| cp phpunit.log /tmp/phpunit.log || true | |
| - name: Upload Coverage Badge to badges branch | |
| if: github.event_name == 'push' | |
| run: | | |
| git config user.name "github-actions" | |
| git config user.email "actions@github.com" | |
| git fetch origin | |
| if git ls-remote --exit-code origin badges; then | |
| echo "Badges branch exists. Checking out..." | |
| git checkout badges | |
| else | |
| echo "Badges branch does NOT exist. Creating it..." | |
| git checkout --orphan badges | |
| fi | |
| git reset --hard | |
| git clean -fdx | |
| cp /tmp/coverage-badge.json coverage.json | |
| git add -f coverage.json | |
| if git diff --cached --quiet; then | |
| echo "No changes" | |
| else | |
| git commit -m "Update coverage badge to ${{ steps.coverage.outputs.coverage }}%" | |
| fi | |
| git push origin badges --force | |
| # --------------------------------------------- | |
| # 9) Summary Report | |
| # --------------------------------------------- | |
| - name: 🧾 Summary Report | |
| if: always() | |
| run: | | |
| echo "### 🧾 Security Guard CI Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "🧹 PHPStan: ${{ steps.phpstan.outputs.phpstan_errors }} errors" >> $GITHUB_STEP_SUMMARY | |
| echo "📊 Coverage: ${{ steps.coverage.outputs.coverage }}%" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ -f phpunit.log ]; then | |
| tail -n 20 phpunit.log >> $GITHUB_STEP_SUMMARY | |
| elif [ -f /tmp/phpunit.log ]; then | |
| tail -n 20 /tmp/phpunit.log >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "⚠️ phpunit.log not found — PHPUnit may have failed before log creation." >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # --------------------------------------------- | |
| # 10) Telegram Notification | |
| # --------------------------------------------- | |
| - name: 📲 Notify Telegram | |
| if: always() | |
| env: | |
| TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_CI_BOT_TOKEN }} | |
| TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CI_CHAT_ID }} | |
| run: | | |
| START_TS=$(date -u -d "$GH_START_ISO" +%s) | |
| END_TS=$(date +%s) | |
| DURATION=$((END_TS - START_TS)) | |
| if [ "$DURATION" -lt 60 ]; then | |
| DURATION_STR="${DURATION}s" | |
| else | |
| DURATION_STR="$(($DURATION / 60))m $(($DURATION % 60))s" | |
| fi | |
| PHPSTAN_ERR=${{ steps.phpstan.outputs.phpstan_errors }} | |
| COV=${{ steps.coverage.outputs.coverage }} | |
| STATUS="🟢 All tests passed" | |
| HEADER="Security Guard CI Report" | |
| COLOR="🟢" | |
| if grep -q "FAILURES!" phpunit.log 2>/dev/null || [ "${{ job.status }}" != "success" ]; then | |
| STATUS="🔴 Tests or PHPStan failed!" | |
| HEADER="Security Guard CI Alert" | |
| COLOR="🔴" | |
| fi | |
| MSG="📢 <b>${HEADER}</b> | |
| ${COLOR} ${STATUS} | |
| 🧹 <b>PHPStan:</b> ${PHPSTAN_ERR} errors | |
| 📊 <b>Coverage:</b> ${COV}% | |
| 📦 <b>Project:</b> ${GITHUB_REPOSITORY} | |
| 🌿 <b>Branch:</b> ${GITHUB_REF_NAME} | |
| 👨💻 <b>By:</b> ${GITHUB_ACTOR} | |
| ⏳ <b>Duration:</b> ${DURATION_STR} | |
| 🔗 <a href='${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}'>View CI Logs</a> | |
| " | |
| curl -s -X POST \ | |
| "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$(jq -n --arg chat_id "$TELEGRAM_CHAT_ID" --arg text "$MSG" --arg pm "HTML" '{chat_id: $chat_id, text: $text, parse_mode: $pm }')" |