feat: attach normalized provider usage to Perseus Plutus metering #7
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 | |
| # Orchestrator workflow. Runs ``detect-changes`` once, then conditionally | |
| # calls the sub-workflows that a PR can actually affect. A final | |
| # ``all-checks-pass`` gate job aggregates results so branch protection only | |
| # needs to require a single check. | |
| # | |
| # Sub-workflows are triggered via ``workflow_call`` and keep their own job | |
| # definitions, matrices, and concurrency settings. They no longer have | |
| # ``push:`` / ``pull_request:`` triggers of their own — everything flows | |
| # through this file. | |
| on: | |
| pull_request: | |
| push: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| pull-requests: write # needed by lint (PR comment) + supply-chain (PR comment) | |
| actions: read # needed by osv-scanner (SARIF upload) | |
| security-events: write # needed by osv-scanner (SARIF upload) | |
| packages: write # needed by docker build | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| # ───────────────────────────────────────────────────────────────────── | |
| # detect: run the classifier once. Every downstream job reads its outputs | |
| # to decide whether to run. On push/dispatch the classifier fails open | |
| # (all lanes true) so post-merge validation is never weakened. | |
| # ───────────────────────────────────────────────────────────────────── | |
| detect: | |
| name: Detect affected areas | |
| runs-on: ubuntu-latest | |
| outputs: | |
| python: ${{ steps.classify.outputs.python }} | |
| frontend: ${{ steps.classify.outputs.frontend }} | |
| site: ${{ steps.classify.outputs.site }} | |
| scan: ${{ steps.classify.outputs.scan }} | |
| deps: ${{ steps.classify.outputs.deps }} | |
| docker_meta: ${{ steps.classify.outputs.docker_meta }} | |
| mcp_catalog: ${{ steps.classify.outputs.mcp_catalog }} | |
| event_name: ${{ github.event_name }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Detect affected areas | |
| id: classify | |
| uses: ./.github/actions/detect-changes | |
| # ───────────────────────────────────────────────────────────────────── | |
| # Lane-gated sub-workflows. Each runs in parallel after detect finishes. | |
| # Skipped workflows (if condition is false) don't spin up runners. | |
| # ───────────────────────────────────────────────────────────────────── | |
| tests: | |
| name: Python tests | |
| needs: detect | |
| if: needs.detect.outputs.python == 'true' | |
| uses: ./.github/workflows/tests.yml | |
| with: | |
| slice_count: 8 | |
| lint: | |
| name: Python lints | |
| needs: detect | |
| if: needs.detect.outputs.python == 'true' | |
| uses: ./.github/workflows/lint.yml | |
| with: | |
| event_name: ${{ needs.detect.outputs.event_name }} | |
| typecheck: | |
| name: TypeScript | |
| needs: detect | |
| if: needs.detect.outputs.frontend == 'true' | |
| uses: ./.github/workflows/typecheck.yml | |
| docs-site: | |
| name: Docs Site | |
| needs: detect | |
| if: needs.detect.outputs.site == 'true' | |
| uses: ./.github/workflows/docs-site-checks.yml | |
| history-check: | |
| name: Deny unrelated histories | |
| needs: detect | |
| if: needs.detect.outputs.event_name == 'pull_request' | |
| uses: ./.github/workflows/history-check.yml | |
| contributor-check: | |
| name: Check contributors | |
| needs: detect | |
| if: needs.detect.outputs.python == 'true' | |
| uses: ./.github/workflows/contributor-check.yml | |
| uv-lockfile: | |
| name: Check uv.lock | |
| needs: detect | |
| uses: ./.github/workflows/uv-lockfile-check.yml | |
| docker-lint: | |
| name: Lint Docker scripts | |
| needs: detect | |
| if: needs.detect.outputs.docker_meta == 'true' | |
| uses: ./.github/workflows/docker-lint.yml | |
| docker: | |
| name: Build&Test Docker image | |
| needs: detect | |
| if: needs.detect.outputs.python == 'true' || needs.detect.outputs.frontend == 'true' || needs.detect.outputs.docker_meta == 'true' | |
| uses: ./.github/workflows/docker.yml | |
| secrets: inherit | |
| supply-chain: | |
| name: Supply-chain scan | |
| needs: detect | |
| if: needs.detect.outputs.event_name == 'pull_request' && (needs.detect.outputs.scan == 'true' || needs.detect.outputs.deps == 'true' || needs.detect.outputs.mcp_catalog == 'true') | |
| uses: ./.github/workflows/supply-chain-audit.yml | |
| with: | |
| event_name: ${{ needs.detect.outputs.event_name }} | |
| scan: ${{ needs.detect.outputs.scan == 'true' }} | |
| deps: ${{ needs.detect.outputs.deps == 'true' }} | |
| mcp_catalog: ${{ needs.detect.outputs.mcp_catalog == 'true' }} | |
| osv-scanner: | |
| name: OSV scan | |
| uses: ./.github/workflows/osv-scanner.yml | |
| # ───────────────────────────────────────────────────────────────────── | |
| # Gate: runs after everything. ``if: always()`` ensures it reports a | |
| # status even when some deps were skipped. Only actual ``failure`` | |
| # results cause it to fail; ``skipped`` is treated as success. | |
| # | |
| # Branch protection should require ONLY this check. | |
| # ───────────────────────────────────────────────────────────────────── | |
| all-checks-pass: | |
| name: All required checks pass | |
| needs: | |
| - tests | |
| - lint | |
| - typecheck | |
| - docs-site | |
| - history-check | |
| - contributor-check | |
| - uv-lockfile | |
| - docker-lint | |
| - supply-chain | |
| - osv-scanner | |
| # we don't require docker to pass rn because it's so slow lol | |
| # - docker | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Evaluate job results | |
| env: | |
| RESULTS: ${{ toJSON(needs.*.result) }} | |
| run: | | |
| echo "$RESULTS" | python3 -c " | |
| import json, sys | |
| results = json.load(sys.stdin) | |
| failed = [r for r in results if r == 'failure'] | |
| if failed: | |
| print(f'::error::{len(failed)} job(s) failed') | |
| sys.exit(1) | |
| print('All checks passed (or were skipped)') | |
| " | |
| # ───────────────────────────────────────────────────────────────────── | |
| # CI timing report: collect per-job/step durations from the GitHub API, | |
| # cache them on main (as a baseline), and on PRs generate an HTML diff | |
| # report with a gantt chart + per-step breakdown. The report is uploaded | |
| # as an artifact and a markdown summary is written to $GITHUB_STEP_SUMMARY. | |
| # ───────────────────────────────────────────────────────────────────── | |
| ci-timings: | |
| name: CI timing report | |
| needs: [all-checks-pass, docker] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Restore baseline cache (PR only) | |
| if: github.event_name == 'pull_request' | |
| uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: ci-timings-baseline.json | |
| # Prefix-match: exact key will never hit (run_id differs), so | |
| # restore-keys finds the most recent baseline from main. | |
| key: ci-timings-baseline-never-exact | |
| restore-keys: | | |
| ci-timings-baseline- | |
| - name: Collect timings and generate report | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| python3 scripts/ci/timings_report.py \ | |
| --baseline ci-timings-baseline.json \ | |
| --output ci-timings-report.html \ | |
| --json-out ci-timings.json \ | |
| --summary-out ci-timings-summary.md | |
| - name: Upload HTML report | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| id: ci-timings-artifact | |
| with: | |
| name: ci-timings-report | |
| path: ci-timings-report.html | |
| retention-days: 14 | |
| archive: false | |
| - name: Output summary | |
| env: | |
| REPORT_URL: ${{ steps.ci-timings-artifact.outputs.artifact-url}} | |
| run: | | |
| echo "# CI Timing report" >> "$GITHUB_STEP_SUMMARY" | |
| echo "[View the full interactive report]($REPORT_URL)" >> "$GITHUB_STEP_SUMMARY" | |
| cat ci-timings-summary.md >> "$GITHUB_STEP_SUMMARY" | |
| - name: Save baseline cache (main only) | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| run: | | |
| # Degraded runs (API rate-limited) produce no ci-timings.json — | |
| # skip rather than fail, and never cache an empty baseline. | |
| if [ -f ci-timings.json ]; then | |
| cp ci-timings.json ci-timings-baseline.json | |
| else | |
| echo "No timings JSON this run — skipping baseline update" | |
| fi | |
| - name: Upload baseline to cache (main only) | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' && hashFiles('ci-timings-baseline.json') != '' | |
| uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: ci-timings-baseline.json | |
| key: ci-timings-baseline-${{ github.run_id }} |