diff --git a/.github/workflows/agent-pr-ready-automerge.yml b/.github/workflows/agent-pr-ready-automerge.yml index 82470666..78840bd7 100644 --- a/.github/workflows/agent-pr-ready-automerge.yml +++ b/.github/workflows/agent-pr-ready-automerge.yml @@ -5,6 +5,8 @@ # non-`cursor/*` branches are never touched — the draft flag is the actor's # veto (the CLI skips them). Fork PRs are excluded (the repo is public; a # merged fork PR would deploy) — same RCE-guard class as the other workflows. +# OPS98: `audit/*` branches (engineering-audit overnight run) are also skipped — +# that single Ready PR merges by human hand only. # # GitHub's NATIVE auto-merge replaces the Forgejo-era poll CLI (removed in # OPS71 Fase 2): `enablePullRequestAutoMerge` (GraphQL, REBASE — diff --git a/scripts/github-pr-automerge.mjs b/scripts/github-pr-automerge.mjs index a7828184..ee6b983a 100644 --- a/scripts/github-pr-automerge.mjs +++ b/scripts/github-pr-automerge.mjs @@ -13,7 +13,9 @@ * mergeable. The OPS64 "never trust the rollup" pin is structural here: a * single sequential job posts one honest check-run, and the server enforces * it. Draft policy (OPS57) preserved: only `cursor/*` heads are marked - * ready — any other draft is the actor's veto (skip, exit 0). + * ready — any other draft is the actor's veto (skip, exit 0). OPS98: `audit/*` + * heads are vetoed entirely (ready or draft) — the engineering-audit single PR + * merges by human hand only. * * Idempotent: already merged / closed / non-main base → no-op success. * A GraphQL error (e.g. repo auto-merge disabled, PR in conflict) exits 1 so diff --git a/scripts/lib/github-pr-flow.mjs b/scripts/lib/github-pr-flow.mjs index 17823876..f8ac8857 100644 --- a/scripts/lib/github-pr-flow.mjs +++ b/scripts/lib/github-pr-flow.mjs @@ -6,7 +6,8 @@ * * GitHub's native auto-merge only fires with the required checks green, so * the safety net's only real decisions are: skip (non-main base, draft veto, - * already merged) or mark-ready (cursor/* draft) before arming auto-merge. + * already merged, audit/* veto) or mark-ready (cursor/* draft) before arming + * auto-merge. */ export const CURSOR_HEAD_PREFIX = 'cursor/' @@ -14,6 +15,15 @@ export const CURSOR_HEAD_PREFIX = 'cursor/' /** OPS57: only `cursor/*` drafts get force-marked ready — any other draft is the actor's veto. */ export const isCursorHead = (ref) => typeof ref === 'string' && ref.startsWith(CURSOR_HEAD_PREFIX) +/** + * OPS98: branches `audit/*` belong to the engineering-audit overnight run — + * its single PR must stay Ready WITHOUT auto-merge (the human is the final + * gate). The veto is dominant: it applies to ready and draft states alike. + */ +export const AUDIT_HEAD_PREFIX = 'audit/' + +export const isAuditHead = (ref) => typeof ref === 'string' && ref.startsWith(AUDIT_HEAD_PREFIX) + /** * OPS71-FLIP: the auto-merge MUST be armed with a real user PAT. Arming with * the built-in GITHUB_TOKEN makes the merge happen as `github-actions[bot]`, @@ -41,6 +51,7 @@ export const automergeArmingToken = (env = process.env) => { */ export const decideAutomergeAction = (pr) => { if (!pr) return { action: 'skip', reason: 'pr-inexistente' } + if (isAuditHead(pr.head?.ref)) return { action: 'skip', reason: 'audit-veto' } if (pr.merged) return { action: 'skip', reason: 'ja-mergeada' } if (pr.state !== 'OPEN') return { action: 'skip', reason: 'pr-nao-aberta' } if (pr.base?.ref !== 'main') return { action: 'skip', reason: 'base-nao-main' } diff --git a/tests/unit/githubPrFlow.unit.spec.ts b/tests/unit/githubPrFlow.unit.spec.ts index 66b889e4..4b684d91 100644 --- a/tests/unit/githubPrFlow.unit.spec.ts +++ b/tests/unit/githubPrFlow.unit.spec.ts @@ -1,9 +1,11 @@ import { describe, expect, it } from 'vitest' import { + AUDIT_HEAD_PREFIX, CURSOR_HEAD_PREFIX, automergeArmingToken, decideAutomergeAction, + isAuditHead, isCursorHead, } from '../../scripts/lib/github-pr-flow.mjs' @@ -66,6 +68,29 @@ describe('github-pr-flow automerge decision (OPS71)', () => { expect(isCursorHead(undefined)).toBe(false) }) + it('skips a ready audit/* PR — the single-PR overnight delivery (OPS98)', () => { + const verdict = decideAutomergeAction( + pr({ head: { ref: `${AUDIT_HEAD_PREFIX}pass-6`, sha: 'abc' } }), + ) + expect(verdict).toEqual({ action: 'skip', reason: 'audit-veto' }) + }) + + it('audit/* veto dominates the draft logic — a draft audit/* never becomes mark-ready', () => { + const verdict = decideAutomergeAction( + pr({ draft: true, head: { ref: `${AUDIT_HEAD_PREFIX}pass-6`, sha: 'abc' } }), + ) + expect(verdict).toEqual({ action: 'skip', reason: 'audit-veto' }) + }) + + it('isAuditHead only matches the exact audit/ prefix', () => { + expect(isAuditHead('audit/pass-6')).toBe(true) + expect(isAuditHead('OPS71-x')).toBe(false) + expect(isAuditHead('cursor/x')).toBe(false) + expect(isAuditHead('auditoria/x')).toBe(false) + expect(isAuditHead('')).toBe(false) + expect(isAuditHead(undefined)).toBe(false) + }) + it('the merge contract has no poll path: ready always arms native auto-merge', () => { // OPS64 pin, GitHub shape: the Forgejo rollup-lie bug is structural here // (single job → one honest check-run), and the server only merges with @@ -73,8 +98,9 @@ describe('github-pr-flow automerge decision (OPS71)', () => { const actions = [ decideAutomergeAction(pr()).action, decideAutomergeAction(pr({ draft: true, head: { ref: 'cursor/x', sha: 'a' } })).action, + decideAutomergeAction(pr({ head: { ref: 'audit/pass-6', sha: 'a' } })).action, ] - expect(actions).toEqual(['enable-auto-merge', 'mark-ready']) + expect(actions).toEqual(['enable-auto-merge', 'mark-ready', 'skip']) }) it('automergeArmingToken accepts a non-empty AUTOMERGE_PAT (OPS71-FLIP)', () => {