You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Every gating workflow in this repo runs only on pull_request targeting main — verified across all 21 files in .github/workflows/: static-testing.yml, unit-testing.yml, bats-testing.yml, dependency-cruiser.yml, eslint-suppressions.yml, mutation-testing.yml, e2e-testing.yml, etc. all declare on: pull_request: branches: ['main'] and nothing else. No workflow anywhere in the repo has a merge_group, schedule, or test-running push: branches: [main] trigger.
The only workflow that fires on push:main is autorelease.yml ("generate changelog and create release") — and it runs zero tests: it generates a token, checks out, and cuts a changelog/release. So the moment anything lands on main, the very next automated action is a release from a tree nothing has verified.
Why PR-only gating is insufficient: checks run against the PR branch head, not the true merge result. Two individually-green PRs can compose into a broken main (a logical/semantic merge conflict), and nothing in this repo ever tests the merged state. Even if "require branches to be up to date" is enabled in branch protection (unverifiable from the repo — the protection API returns 403 and /rulesets returns [], so protection is classic-config only and invisible), that setting merely serializes merges; it does not re-run checks on the actual merge commit the way a merge queue does.
Defect class prevented
Semantically conflicting green PRs breaking main undetected — with the breakage misattributed and immediately released.
Concretely: PR A renames an exported symbol; PR B, branched before A merged, consumes the old name. Both are individually green (each was tested against a main that made it valid). Both merge. main now fails make lint-tsc — but nothing runs on push:main except autorelease.yml, which happily cuts a changelog/release from the broken tree. The breakage surfaces only when the next unrelated PR's checks go red on code its author never touched, burning that contributor's time on misattributed triage while main (and the latest release) stay broken.
Evidence
085caf1 "fix(ci): bump python3 alpine pin to 3.12.13-r0" — Alpine's package repo rolled python3 forward and deleted the old version; per the commit message this broke every Docker-based CI job (static, unit, integration, dep-cruiser, e2e, mutation, memory-leak, perf, visual) with unable to select packages: python3-3.12.13-r0 breaks world[python3=3.12.12-r0]. The outage was discovered reactively when unrelated PR pipelines went red, and the same fix had to be re-applied inside the 0706f61 main hot-patch ("fix: update alpine python package pin"). With no scheduled or push:main verification run anywhere, the first signal of a repo-wide red state was an innocent contributor's PR failing on code they didn't touch — a main verification run would have surfaced and attributed the breakage immediately. (Verified via git show 085caf1 and the DockerfileFROM node:24.8.0-alpine3.21 tag-only pin.)
Issue Fix qlty CI check in main #79 / PR Fix qlty CI check in main #80 (merged 2026-05-17, "Fix qlty CI check in main") — the qlty quality check went red on the default branch itself and stayed red until a dedicated repair PR landed, touching 30 files: 16 workflow YAMLs, .markdownlint.yaml, docker-compose.test.yml, common-healthchecks.yml, two Dockerfiles, three scripts under scripts/, the apollo-server bootstrap, and src/ components. Drift accumulated and merged green through PRs until main's own signal broke — exactly the "main broken, discovered later, misattributed" hole this issue closes.
No OpenSSF Scorecard baseline datum applies directly to merge-time verification; the gap is grounded in the incidents above and the verified absence of any merge_group / push:main / schedule test trigger.
Proposed check
Phase 1 (S — land immediately and independently): post-merge main verification. Add .github/workflows/main-verification.yml on push: branches: [main] running make start && make lint && make test-unit-all && make codegen-check (all existing Makefile targets), with a failure path that opens or updates a single pinned "main is red" issue — it must query for an existing open issue with the main-is-red label first, so consecutive red pushes update one issue instead of spamming. Use concurrency: group: main-verification with cancel-in-progress: false so back-to-back merges are each verified in order, none skipped.
Important scoping: Phase 1 detects but does not preventautorelease.yml cutting a release from a broken tree — both workflows fire concurrently on push:main. Sell Phase 1 as detection + attribution, not release protection. Sequencing autorelease behind verification (e.g. workflow_run on main-verification success, or folding the verification steps ahead of the changelog step) depends on issue #138's release-repair work — add that as an explicit follow-up dependency.
Phase 2 (M — gated on the #141 branch-protection policy work; do not block Phase 1 on it): merge queue. Enable the repository merge queue and add merge_group: to the on: block of the fast, deterministic gate workflows only: static-testing.yml, unit-testing.yml, bats-testing.yml, eslint-suppressions.yml, dependency-cruiser.yml. Do NOT add mutation-testing.yml — it is a 4-way sharded matrix with an ~1h wall-clock path (per CLAUDE.md's "CI parallelization"); queuing it would add that latency plus 4x runner cost to every merge. Consequence to handle in #141: if mutation-testing remains a required branch-protection check unscoped by context, the merge queue will wait forever for a check that never reports on merge_group — the #141 coordination must explicitly scope required checks per context (mutation stays PR-required only). Keep the slow perf suites (performance-testing.yml, load-testing.yml, memory-leak-testing.yml) PR-only. Update concurrency groups so queue runs are not cancelled by PR pushes.
# .github/workflows/main-verification.yml (Phase 1)name: main verificationon:
push:
branches: ['main']permissions:
contents: readissues: writeconcurrency:
group: main-verificationcancel-in-progress: false # verify every merge, in orderjobs:
verify:
runs-on: ubuntu-latesttimeout-minutes: 30steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- run: make start
- run: make lint
- run: make test-unit-all
- run: make codegen-check
- name: Open or update pinned "main is red" issueif: failure()env:
GH_TOKEN: ${{ github.token }}run: | existing=$(gh issue list --label main-is-red --state open --json number --jq '.[0].number') run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" if [ -n "$existing" ]; then gh issue comment "$existing" --body "Still red @ ${GITHUB_SHA} — ${run_url}" else gh issue create --title "main is red @ ${GITHUB_SHA}" \ --label main-is-red --label ci-health \ --body "Post-merge verification failed on main: ${run_url}" fi# Phase 2 — add to static-testing.yml, unit-testing.yml, bats-testing.yml,# eslint-suppressions.yml, dependency-cruiser.yml (NOT mutation-testing.yml):on:
pull_request:
branches: ['main']merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.merge_group.head_ref || github.ref }}cancel-in-progress: ${{ github.event_name == 'pull_request' }}
Benchmark: grafana/grafana and getsentry/sentry gate merges through GitHub merge queues; rust-lang pioneered never-break-main with bors (test the merge commit, not the branch); mui/material-ui runs CI on push to master/next so a broken default branch is detected and attributed within minutes.
Overlap note: issue #141 tracks documenting/drift-checking branch protection and required checks — Phase 2's merge-queue enablement and the required-check context scoping land inside that governance change, but #141 nowhere proposes merge_group triggers or post-merge main verification, so this gap is distinct. Sequencing autorelease behind verification depends on issue #138.
Effort
M — Phase 1 is a single new workflow reusing existing Make targets (S on its own); Phase 2 requires merge-queue settings, on:/concurrency edits across five workflows, and required-check context scoping coordinated with #141.
Acceptance criteria
.github/workflows/main-verification.yml exists and runs make lint, make test-unit-all, and make codegen-check on every push to main, with concurrency: main-verification / cancel-in-progress: false, and its failure path opens or updates a single open main-is-red issue (verified: two consecutive red pushes produce one issue, not two); Phase 2: merge queue is enabled and merge_group: triggers report on static-testing, unit-testing, bats-testing, eslint-suppressions, and dependency-cruiser (and not on mutation-testing).
The check has failed at least once on a deliberately seeded defect — e.g. merge two individually-green branches with a rename/consumer conflict (or push a commit that breaks make lint-tsc) and confirm main-verification goes red and files/updates the main-is-red issue; for Phase 2, confirm a queue run rejects a PR that is green on its branch but red against the speculative merge result.
Current state
Coverage 3/5, enforcement 4/5 → target 5/5.
Every gating workflow in this repo runs only on
pull_requesttargetingmain— verified across all 21 files in.github/workflows/:static-testing.yml,unit-testing.yml,bats-testing.yml,dependency-cruiser.yml,eslint-suppressions.yml,mutation-testing.yml,e2e-testing.yml, etc. all declareon: pull_request: branches: ['main']and nothing else. No workflow anywhere in the repo has amerge_group,schedule, or test-runningpush: branches: [main]trigger.The only workflow that fires on
push:mainisautorelease.yml("generate changelog and create release") — and it runs zero tests: it generates a token, checks out, and cuts a changelog/release. So the moment anything lands onmain, the very next automated action is a release from a tree nothing has verified.Why PR-only gating is insufficient: checks run against the PR branch head, not the true merge result. Two individually-green PRs can compose into a broken
main(a logical/semantic merge conflict), and nothing in this repo ever tests the merged state. Even if "require branches to be up to date" is enabled in branch protection (unverifiable from the repo — the protection API returns 403 and/rulesetsreturns[], so protection is classic-config only and invisible), that setting merely serializes merges; it does not re-run checks on the actual merge commit the way a merge queue does.Defect class prevented
Semantically conflicting green PRs breaking
mainundetected — with the breakage misattributed and immediately released.Concretely: PR A renames an exported symbol; PR B, branched before A merged, consumes the old name. Both are individually green (each was tested against a
mainthat made it valid). Both merge.mainnow failsmake lint-tsc— but nothing runs onpush:mainexceptautorelease.yml, which happily cuts a changelog/release from the broken tree. The breakage surfaces only when the next unrelated PR's checks go red on code its author never touched, burning that contributor's time on misattributed triage whilemain(and the latest release) stay broken.Evidence
085caf1"fix(ci): bump python3 alpine pin to 3.12.13-r0" — Alpine's package repo rolled python3 forward and deleted the old version; per the commit message this broke every Docker-based CI job (static, unit, integration, dep-cruiser, e2e, mutation, memory-leak, perf, visual) withunable to select packages: python3-3.12.13-r0 breaks world[python3=3.12.12-r0]. The outage was discovered reactively when unrelated PR pipelines went red, and the same fix had to be re-applied inside the0706f61main hot-patch ("fix: update alpine python package pin"). With no scheduled orpush:mainverification run anywhere, the first signal of a repo-wide red state was an innocent contributor's PR failing on code they didn't touch — amainverification run would have surfaced and attributed the breakage immediately. (Verified viagit show 085caf1and theDockerfileFROM node:24.8.0-alpine3.21tag-only pin.).markdownlint.yaml,docker-compose.test.yml,common-healthchecks.yml, two Dockerfiles, three scripts underscripts/, the apollo-server bootstrap, andsrc/components. Drift accumulated and merged green through PRs until main's own signal broke — exactly the "main broken, discovered later, misattributed" hole this issue closes.No OpenSSF Scorecard baseline datum applies directly to merge-time verification; the gap is grounded in the incidents above and the verified absence of any
merge_group/push:main/scheduletest trigger.Proposed check
Phase 1 (S — land immediately and independently): post-merge main verification. Add
.github/workflows/main-verification.ymlonpush: branches: [main]runningmake start && make lint && make test-unit-all && make codegen-check(all existing Makefile targets), with a failure path that opens or updates a single pinned "main is red" issue — it must query for an existing open issue with themain-is-redlabel first, so consecutive red pushes update one issue instead of spamming. Useconcurrency: group: main-verificationwithcancel-in-progress: falseso back-to-back merges are each verified in order, none skipped.Important scoping: Phase 1 detects but does not prevent
autorelease.ymlcutting a release from a broken tree — both workflows fire concurrently onpush:main. Sell Phase 1 as detection + attribution, not release protection. Sequencing autorelease behind verification (e.g.workflow_runon main-verification success, or folding the verification steps ahead of the changelog step) depends on issue #138's release-repair work — add that as an explicit follow-up dependency.Phase 2 (M — gated on the #141 branch-protection policy work; do not block Phase 1 on it): merge queue. Enable the repository merge queue and add
merge_group:to theon:block of the fast, deterministic gate workflows only:static-testing.yml,unit-testing.yml,bats-testing.yml,eslint-suppressions.yml,dependency-cruiser.yml. Do NOT addmutation-testing.yml— it is a 4-way sharded matrix with an ~1h wall-clock path (per CLAUDE.md's "CI parallelization"); queuing it would add that latency plus 4x runner cost to every merge. Consequence to handle in #141: if mutation-testing remains a required branch-protection check unscoped by context, the merge queue will wait forever for a check that never reports onmerge_group— the #141 coordination must explicitly scope required checks per context (mutation stays PR-required only). Keep the slow perf suites (performance-testing.yml,load-testing.yml,memory-leak-testing.yml) PR-only. Update concurrency groups so queue runs are not cancelled by PR pushes.Benchmark: grafana/grafana and getsentry/sentry gate merges through GitHub merge queues; rust-lang pioneered never-break-main with bors (test the merge commit, not the branch); mui/material-ui runs CI on push to master/next so a broken default branch is detected and attributed within minutes.
Overlap note: issue #141 tracks documenting/drift-checking branch protection and required checks — Phase 2's merge-queue enablement and the required-check context scoping land inside that governance change, but #141 nowhere proposes
merge_grouptriggers or post-merge main verification, so this gap is distinct. Sequencing autorelease behind verification depends on issue #138.Effort
M — Phase 1 is a single new workflow reusing existing Make targets (S on its own); Phase 2 requires merge-queue settings,
on:/concurrency edits across five workflows, and required-check context scoping coordinated with #141.Acceptance criteria
.github/workflows/main-verification.ymlexists and runsmake lint,make test-unit-all, andmake codegen-checkon everypushtomain, withconcurrency: main-verification/cancel-in-progress: false, and its failure path opens or updates a single openmain-is-redissue (verified: two consecutive red pushes produce one issue, not two); Phase 2: merge queue is enabled andmerge_group:triggers report onstatic-testing,unit-testing,bats-testing,eslint-suppressions, anddependency-cruiser(and not onmutation-testing).make lint-tsc) and confirm main-verification goes red and files/updates themain-is-redissue; for Phase 2, confirm a queue run rejects a PR that is green on its branch but red against the speculative merge result.Generated by Claude Code