fix(drift): enforce real green-gate before auto-merge (pass>0, required contexts, no false-green)#306
Merged
Merged
Conversation
commit: |
Replace the row-count and --watch based false-green with a jq-based gate that asserts pass>=1 and zero pending, failing, cancelled, or unknown checks, and that every required context is present and green. The gate is fail-closed by construction: an explicit jq preflight plus integer assertions, with no `:-0` defaulting that could mask a missing or malformed field. A non-string field guard rejects unexpected JSON shapes rather than silently treating them as green. Pin the evaluated commit with HEAD_SHA and pass --match-head-commit so the gate cannot pass against a stale or moved head. Emit distinct Slack reasons per failure mode so operators can tell why a merge was blocked. Add a full red-green regression lock in ci-merge-gate.test.ts covering each false-green hole the previous gate allowed through.
jpr5
force-pushed
the
harden/drift-merge-gate
branch
from
July 16, 2026 23:57
f865f05 to
8d9a425
Compare
…lert gaps Structural: replace the ~9 scattered VAR=$(jq) scoring calls with ONE guarded jq verdict computation. set -e does not guard a command-substitution RHS, so a jq crash in any of them yielded empty — and for UNACCEPTED_CHECKS/MISSING_REQUIRED that empty read as the green signal. Now the whole pass/fail decision + reasons + scalars are computed inside one jq program emitting a single verdict object, and the shell asserts once (jq rc==0, non-empty valid JSON, boolean .green) before reading any field. Any jq/parse failure -> exit 2, never green. No bare VAR=$(jq) whose emptiness can read as green remains. Observability (fix-drift.yml): quarantine alert now emits a visible ::error:: and FAILS when SLACK_WEBHOOK is missing (was silent exit 0); a Step-1 collector crash gets its own distinct alert/reason instead of the misleading auto-fix STEP failed; a gh pr merge --match-head-commit failure sets a specific merge-command-failed reason. Tests: tightened loose refusal-reason regexes to the specific reason; corrected the factually-wrong jq-crash-exit-5 rationale (the field-type guard actually closes a false-GREEN via the state fallback) and added an isolated proof of that vector; locked the [1,2,3] object-guard branch distinctly from the field-type guard; exercised the derived-state legs individually; added consolidated-verdict jq-failure/empty-verdict/non-boolean-green guards and a structural invariant test.
jpr5
marked this pull request as ready for review
July 17, 2026 00:25
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
The in-workflow auto-merge "green gate" in
fix-drift.ymldid not actually enforce green. It countedgh pr checks | wc -lrows (a "no checks reported" message is itself a row) and relied ongh pr checks --watch --fail-fast, which exits 0 on empty / pending / skipped / neutral. That is a false-green: an unverified drift PR could auto-merge tomain. This fix-forward replaces the gate with a real, testable assertion.Findings addressed
--watch-exit-0 with a machine-readable assertion: poll ongh pr checks --jsonlength for registration,--watchunder a hard timeout, then re-query JSON and require pass>=1 AND zero in {pending, fail, cancel}. neutral/skipped do not count toward pass.prettier,eslint,exports,commitlint,test (20/22/24),agui-schema-drift,drift-live-pr,zizmor— derived from the real check set on drift PR Fix Drift: authenticate via copilotkit-devops-bot app token #305) are present and concluded green, so one unrelated fast check can't satisfy the gate and a registration race can't slip an incomplete set through.headRefOid == git rev-parse HEAD), notgh pr list ... .[0]. Addedset -euo pipefailto the multi-command run blocks.checks: read+statuses: readto the jobpermissions:and to the minted app-token permissions.continue-on-error: trueautofix that fails now triggers a distinct "autofix STEP failed" Slack alert and fails the job (steps.autofix.outcome != 'success').quarantine=trueoutput and routes to a distinct "drift quarantine — needs human" Slack alert instead of the silent skip path.merged=trueoutput); otherwise "PR open, not yet merged". MissingSLACK_WEBHOOKon the failure path is now a visible::error::annotation, not a silentexit 0. Droppedcurl -sforcurl -fsS --show-error.--watchwrapped intimeout 1200; the failure Slack distinguishestimeout/not-green/no-checksreasons.Design
The gate decision is factored into a standalone, unit-testable script
scripts/ci-merge-gate.shthat reads thegh pr checks --json name,state,bucketarray (file arg or stdin) and exits0only on true-green. The workflow calls this script as the authoritative decision (the--watchexit status is advisory only). Required contexts default to the drift-PR set and are overridable viaREQUIRED_CONTEXTS.Red-green proof
The gate is exercised by
src/__tests__/ci-merge-gate.test.ts(13 tests) plus a direct shell comparison.RED — OLD workflow logic (row-count +
--watch) treats false-green as mergeableGREEN — NEW gate (
scripts/ci-merge-gate.sh)vitest
Local verification
actionlint .github/workflows/fix-drift.yml— cleanshellcheck scripts/ci-merge-gate.sh— cleanpnpm format:check— cleanpnpm lint— cleanScope
Diff touches only:
.github/workflows/fix-drift.yml,scripts/ci-merge-gate.sh,src/__tests__/ci-merge-gate.test.ts. No ruleset, secret, or GitHub App / bypass-config changes.Opened as DRAFT — do not merge; orchestrator gates the merge.