feat: add dependency updater command #84
Workflow file for this run
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: PHPUnit | |
| on: | |
| pull_request: | |
| # Runs on main too — the badges job needs the coverage + mutation artifacts | |
| # produced by the phpunit/mutation jobs to publish the badge data. | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| concurrency: | |
| # Cancel superseded PR runs; let main pushes finish so the badges job | |
| # (publishes coverage/MSI to the badges branch) is never cut off. | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| permissions: | |
| contents: read | |
| jobs: | |
| phpunit: | |
| name: Unit Tests (PHP ${{ matrix.php-version }}) | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| php-version: ["8.3", "8.4"] | |
| include: | |
| - php-version: "8.4" | |
| coverage: true | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| - name: Set up PHP | |
| uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2 | |
| with: | |
| php-version: ${{ matrix.php-version }} | |
| tools: composer:v2 | |
| coverage: ${{ matrix.coverage && 'pcov' || 'none' }} | |
| - name: Cache Composer packages | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: ~/.composer/cache/files | |
| key: ${{ runner.os }}-composer-phpunit-${{ matrix.php-version }}-${{ hashFiles('composer.json') }} | |
| restore-keys: ${{ runner.os }}-composer-phpunit-${{ matrix.php-version }} | |
| - name: Install dev dependencies | |
| run: composer install --no-interaction --no-progress | |
| - name: Run PHPUnit | |
| if: ${{ !matrix.coverage }} | |
| run: vendor/bin/phpunit | |
| - name: Run PHPUnit with coverage | |
| if: ${{ matrix.coverage }} | |
| # mkdir: the text report is written with a bare file_put_contents and | |
| # relies on the Clover/HTML writers having created reports/ first. | |
| run: | | |
| mkdir -p reports | |
| vendor/bin/phpunit \ | |
| --coverage-text=reports/coverage.txt \ | |
| --coverage-html reports/coverage \ | |
| --coverage-clover reports/clover.xml | |
| - name: Enforce minimum line coverage | |
| if: ${{ matrix.coverage }} | |
| # Quality ratchet — raise the threshold as coverage grows. | |
| run: php tests/coverage-checker.php reports/clover.xml 78 | |
| - name: Publish coverage summary | |
| if: ${{ matrix.coverage && always() }} | |
| run: | | |
| if [[ -f reports/coverage.txt ]]; then | |
| { | |
| echo '## Code Coverage' | |
| echo '```' | |
| cat reports/coverage.txt | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Upload coverage report | |
| if: ${{ matrix.coverage && always() }} | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: coverage-report | |
| path: | | |
| reports/coverage | |
| reports/clover.xml | |
| if-no-files-found: ignore | |
| mutation: | |
| name: Mutation Tests (Infection) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| - name: Set up PHP | |
| uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2 | |
| with: | |
| php-version: "8.4" | |
| tools: composer:v2 | |
| coverage: pcov | |
| - name: Cache Composer packages | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: ~/.composer/cache/files | |
| key: ${{ runner.os }}-composer-phpunit-8.4-${{ hashFiles('composer.json') }} | |
| restore-keys: ${{ runner.os }}-composer-phpunit-8.4 | |
| - name: Install dev dependencies | |
| run: composer install --no-interaction --no-progress | |
| - name: Run Infection | |
| run: vendor/bin/infection --threads=max --logger-github --no-progress | |
| - name: Publish mutation summary | |
| if: always() | |
| run: | | |
| if [[ -f reports/infection/summary.log ]]; then | |
| { | |
| echo '## Mutation Testing' | |
| echo '```' | |
| cat reports/infection/summary.log | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Upload mutation report | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: mutation-report | |
| path: reports/infection | |
| if-no-files-found: ignore | |
| # Publishes shields.io endpoint JSON (coverage %, mutation score) to the | |
| # "badges" branch; the README badges read from there via raw.githubusercontent. | |
| badges: | |
| name: Publish badge data | |
| if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} | |
| needs: [phpunit, mutation] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Download coverage report | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: coverage-report | |
| path: reports | |
| - name: Download mutation report | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: mutation-report | |
| path: reports/infection | |
| - name: Generate badge JSON | |
| run: | | |
| mkdir -p badge-data | |
| python3 - <<'EOF' | |
| import json | |
| import re | |
| import xml.etree.ElementTree as ET | |
| def color(value, bands): | |
| for limit, name in bands: | |
| if value < limit: | |
| return name | |
| return "brightgreen" | |
| metrics = ET.parse("reports/clover.xml").getroot().find("./project/metrics") | |
| statements = int(metrics.get("statements")) | |
| covered = int(metrics.get("coveredstatements")) | |
| coverage = covered / statements * 100 if statements else 0.0 | |
| summary = open("reports/infection/summary.log").read() | |
| def count(label): | |
| return int(re.search(rf"^{label}: (\d+)$", summary, re.M).group(1)) | |
| total = count("Total") | |
| detected = ( | |
| count("Killed by Test Framework") | |
| + count("Killed by Static Analysis") | |
| + count("Errored") | |
| + count("Timed Out") | |
| ) | |
| covered_mutants = total - count("Not Covered") | |
| msi = detected / covered_mutants * 100 if covered_mutants else 0.0 | |
| badges = { | |
| "coverage.json": ("coverage", coverage, [(20, "red"), (50, "orange"), (75, "yellow")]), | |
| "msi.json": ("mutation score", msi, [(50, "red"), (70, "orange"), (85, "yellow")]), | |
| } | |
| for filename, (label, value, bands) in badges.items(): | |
| with open(f"badge-data/{filename}", "w") as handle: | |
| json.dump( | |
| { | |
| "schemaVersion": 1, | |
| "label": label, | |
| "message": f"{value:.1f}%", | |
| "color": color(value, bands), | |
| }, | |
| handle, | |
| ) | |
| EOF | |
| - name: Push badge data to badges branch | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| remote="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" | |
| if ! git clone --depth 1 --branch badges "${remote}" badges-branch; then | |
| mkdir badges-branch | |
| git -C badges-branch init -b badges | |
| git -C badges-branch remote add origin "${remote}" | |
| fi | |
| cp badge-data/*.json badges-branch/ | |
| cd badges-branch | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add . | |
| git diff --cached --quiet || git commit -m "chore: update badge data" | |
| git push origin badges |