# PR validation: security gates + lint + full test suite. Builds, smoke and # soak stay maintainer-driven via the dry-run workflow_dispatch. Branch # protection requires `dco` + `ci-ok` — a single stable summary context that # fails unless every PR stage succeeded. name: PR on: pull_request: branches: [main] permissions: contents: read # A new push to the PR supersedes the running validation — cancel it # instead of stacking zombie pipelines. concurrency: group: pr-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: security: uses: ./.github/workflows/_security.yml secrets: inherit lint: uses: ./.github/workflows/_lint.yml test: needs: [lint] if: ${{ !cancelled() && needs.lint.result == 'success' }} uses: ./.github/workflows/_test.yml with: # Perf assertions are timing-sensitive on shared runners; they stay in # dry runs and releases where a flaky red does not block a merge. skip_perf: true ci-ok: # The one required context (besides dco) — fails unless every PR stage # succeeded, so matrix renames can never silently deadlock merges. needs: [security, lint, test] if: ${{ always() }} runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: All PR stages must have succeeded env: RESULTS: ${{ toJSON(needs) }} run: | echo "$RESULTS" | python3 -c " import json, sys needs = json.load(sys.stdin) bad = {k: v['result'] for k, v in needs.items() if v['result'] != 'success'} if bad: print('CI NOT OK:', bad) sys.exit(1) print('CI OK:', ', '.join(needs)) "