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: branches: [main] 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) 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: 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: needs: detect if: needs.detect.outputs.python == 'true' uses: ./.github/workflows/tests.yml lint: needs: detect if: needs.detect.outputs.python == 'true' uses: ./.github/workflows/lint.yml with: event_name: ${{ needs.detect.outputs.event_name }} typecheck: needs: detect if: needs.detect.outputs.frontend == 'true' uses: ./.github/workflows/typecheck.yml docs-site: needs: detect if: needs.detect.outputs.site == 'true' uses: ./.github/workflows/docs-site-checks.yml history-check: needs: detect if: needs.detect.outputs.event_name == 'pull_request' uses: ./.github/workflows/history-check.yml contributor-check: needs: detect if: needs.detect.outputs.python == 'true' uses: ./.github/workflows/contributor-check.yml uv-lockfile: needs: detect uses: ./.github/workflows/uv-lockfile-check.yml docker-lint: needs: detect if: needs.detect.outputs.docker_meta == 'true' uses: ./.github/workflows/docker-lint.yml supply-chain: 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: needs: detect 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 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)') "