ci: auto-queue PRs in Mergify once verify, Unfret and live-gate pass (#100) - #107
Conversation
Use a serial in-place Mergify queue so Unfret reviews the PR head and live-gate must pass before automatic queueing. Co-Authored-By: mastra-platform[bot] <284800079+mastra-platform[bot]@users.noreply.github.com>
Merge Protections🟢 Merge protection satisfied — ready to merge. Show 1 satisfied protection🟢 📃 Configuration Change RequirementsMergify configuration change
|
|
Operator checks on exact head
|
|
| queue_conditions: | ||
| - -draft | ||
| - check-success=verify | ||
| - check-success=Unfret | ||
| - check-success=live-gate | ||
| merge_conditions: [] |
There was a problem hiding this comment.
If main moves after a PR passes verify, Unfret, and live-gate, these queue conditions still allow the older branch to merge. The merge commit can then contain a tree the live test never covered. Require the branch to be up to date with main so a move calls for fresh checks and live evidence.
There was a problem hiding this comment.
Operator adjudication: no change. With in-place checks, Mergify updates a behind branch; the new head must pass the queue conditions again, including fresh exact-head live-gate after operator re-testing.
There was a problem hiding this comment.
Agreed. With max_parallel_checks: 1, batch_size: 1, and no additional merge_conditions, Mergify uses the in-place queue flow: when main advances, it updates the queued branch and restarts validation on the resulting head. Because check-success results are commit-specific, the old verify, Unfret, and exact-head live-gate results cannot satisfy the new head; fresh checks and live evidence are required before merging. No configuration change is needed.
| - check-success=verify | ||
| - check-success=Unfret | ||
| - check-success=live-gate | ||
| merge_conditions: [] |
There was a problem hiding this comment.
If main changes while a PR waits, its head can keep the earlier green checks. merge_conditions: [] does not require the branch to catch up or the checks to run against the new base. Mergify can then merge code that was not tested against current main. Require the branch to be up to date before merging, with fresh checks after it changes.
There was a problem hiding this comment.
Operator adjudication: no change. With in-place checks, Mergify updates a behind branch; the new head must pass the queue conditions again, including fresh exact-head live-gate after operator re-testing.
There was a problem hiding this comment.
Understood. Given Mergify’s in-place update behavior, advancing main produces a new PR head and causes the queue conditions—including the exact-head live-gate—to be evaluated again before merge. That addresses the stale-base concern, so no change is needed.
Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.
✅ Accepted riskAccepted by ericlitman: MASTRA-738 (authority 48fa77b) 🟡 Medium · Stale queue admission invalidates the exact-head live gate DetailsRun: @unfret review · @unfret status · @unfret help |
There was a problem hiding this comment.
🛑 2 blocking findings
🔴 Critical · Auto-queue gates accept spoofable check names
🟡 Medium · In-place update drops exact-head live-gate, stalling queue
Check
| - check-success=verify | ||
| - check-success=Unfret | ||
| - check-success=live-gate |
There was a problem hiding this comment.
🔴 Critical · Auto-queue gates accept spoofable check names
The queue conditions (L11-13) and merge-protection success conditions (L27-29) match verify, Unfret and live-gate by name only, with no source check. auto_merge_conditions (L18) admits any PR to main with no author or approval restriction. Scenario: a non-draft PR adds a workflow with jobs named live-gate/Unfret (and can edit the verify workflow). The PR could come from a fork whose Actions run, or from the builder bot. Those jobs post successful check-runs with the gated names on the PR head. Mergify then auto-queues the PR and, with empty merge_conditions, merges it. The operator never posted live-gate and the real Unfret may never have passed. Before this diff, merges were manual. Branch protection that requires human approval would block this, but it is not visible here and may exist outside this diff. Also reported for this defect: - Unqualified check names allow gate spoofing (.mergify.yml:11-13): Mergify's bare check-success form accepts a successful check with the matching name regardless of its producer. A pull request can publish trivially successful checks named verify, Unfret, and live-gate; these conditions then pass even if the trusted checks failed or never ran. The same unqualified names at lines 27-29 let the spoof satisfy merge protection too, allowing an untrusted PR to be auto-queued and merged without the mandated gates. - Authenticate the live-gate result's source (.mergify.yml:13): Mergify's unqualified check-success accepts a matching check from any app, not just the operator's commit status. A fork contributor can add a pull_request Actions job named live-gate that simply succeeds; GitHub publishes that check even with a read-only token. For an up-to-date, non-draft PR whose genuine verify and Unfret pass, this line and line 29 accept the workflow-produced result, allowing automatic merging without the operator's installed-candidate test required by AGENTS.md. Both gate conditions need to distinguish the trusted live-test result from PR-controlled check runs.
Also affects:
Withdrawn in the latest review.There was a problem hiding this comment.
Applied the operator-confirmed correction in f65069f: both queue_conditions and success_conditions now include -files~=^\.github/, with a short explanatory comment. PRs touching .github/ cannot auto-queue and require manual operator queueing. No other mechanism added.
| - -draft | ||
| - check-success=verify | ||
| - check-success=Unfret | ||
| - check-success=live-gate |
There was a problem hiding this comment.
🟡 Medium · Stale queue admission invalidates the exact-head live gate
These admission conditions allow a PR whose branch is behind main to enter the serial in-place queue while all three checks pass on its old head. Mergify then merges main into the PR branch, creating a new head SHA without the operator-posted live-gate. The gate required here and at line 31 cannot pass until the operator installs, live-tests, and publishes another status on that Mergify-created head, so the admitted PR cannot complete automatically. No automatic live-gate publisher is present. An up-to-date admission guard is still missing; the carried repair remains unresolved. Also reported for this defect: - Queue updates invalidate the exact-head live gate (.mergify.yml:13): A PR can satisfy this condition and enter the serial queue, then become stale when an earlier PR merges. Mergify's in-place update creates a new head SHA without the manually published live-gate, so the PR cannot complete automatically until an operator installs, retests, and posts a new status for the Mergify-created commit. - In-place update strands exact-head live-gate (.mergify.yml:31): Still present. Serial in-place checks (L2 max_parallel_checks: 1, L6 batch_size: 1) make Mergify merge main into a queued PR that has fallen behind, for example after an earlier queued PR merges. That creates a new head SHA. The operator's manual live-gate exists only on the tested SHA, and admission (L9-13) has no up-to-date guard. So L13 and L31 cannot pass on the head Mergify would merge, and nothing publishes live-gate automatically. With checks_timeout: null (L7), the PR either waits at the serial queue head, blocking every later PR, or is dequeued. Either way, progress requires an operator live-gate on a Mergify-made commit nobody live-tested. - In-place update strands exact-head live-gate (.mergify.yml:33, .mergify.yml:10-15, .mergify.yml:15, .mergify.yml:29): Carried, unchanged at this head. Serial in-place checking (L2, L6) merges main into a behind PR, creating a new head SHA. The operator's live-gate exists only on the old SHA, so L33 (and L15) cannot pass on the head Mergify would merge. With checks_timeout null (L7), the PR either waits at the serial queue head, blocking later PRs, or is dequeued. Either way, progress requires a live-gate on a Mergify-made commit the operator never tested. Trigger: a queued PR falls behind after another PR merges. Also reported for this defect: - Queue updates invalidate the approved live-gate (.mergify.yml:10-15): These admission conditions accept a green PR whose branch is behind main, including one made stale by an earlier queued merge. The serial in-place queue then merges main into that branch, creating a new head SHA. The operator's live-gate status remains on the old SHA, while lines 15 and 33 require it on the new one and no automatic publisher exists. The admitted PR consequently cannot complete its automatic merge until the operator installs and live-tests the Mergify-created head and posts another status. The prior missing up-to-date admission guard remains unresolved. - Updating a stale queued PR invalidates its live gate (.mergify.yml:15): A PR can satisfy this exact-head status while current, then become stale when an earlier PR merges. The serial in-place queue updates that PR to a new head, where the manually posted live-gate no longer exists; the protection therefore cannot pass until an operator tests and posts another status on Mergify's new commit, and checks_timeout: null leaves the wait unbounded. Thus ordinary queue advancement can strand or dequeue an otherwise ready PR instead of automatically merging it. - In-place update drops exact-head live-gate, stalling queue (.mergify.yml:29): batch_size: 1 (L6) and max_parallel_checks: 1 (L2) make Mergify check queued PRs in place. Scenario: a ready PR is behind main because another PR merged first. Mergify merges main into the PR branch and creates a new head SHA. The operator's exact-head live-gate exists only on the old SHA. So check-success=live-gate cannot pass on the head Mergify is about to merge, whether it comes from merge protections (L29), the queue condition (L13), or a live-gate check Mergify injects from branch protection. With checks_timeout: null, the serial queue then waits indefinitely, or the PR is dequeued. Every later PR is blocked until the operator posts live-gate on a Mergify-made commit they never live-tested. The planned proof PR will be up to date with main, so it will not exercise this path.
Now at .mergify.yml:9-13
Also affects:
There was a problem hiding this comment.
Operator adjudication: no change. This is the documented fails-loudly hold: an updated head lacks exact-head live-gate and leaves the queue; the operator re-tests that head and re-posts live-gate before it can proceed.
There was a problem hiding this comment.
Operator adjudication: no change. Mergify removes a PR from the queue when queue_conditions stop matching, including a new head without exact-head live-gate, so the queue does not stall. The operator re-tests the new head and re-posts live-gate.
Require operator queueing for .github/ changes to prevent workflow-generated checks from spoofing the automatic gates. Co-Authored-By: mastra-platform[bot] <284800079+mastra-platform[bot]@users.noreply.github.com>
|
@unfret review |
|
Operator checks on exact head Mergify |
There was a problem hiding this comment.
🛑 2 blocking findings
🟡 Medium · .github/ exclusion also blocks the promised operator queue
🟡 Medium · In-place update strands exact-head live-gate
Check
| merge_method: merge | ||
| # Block workflow check spoofing; .github/ changes require an operator queue. | ||
| queue_conditions: | ||
| - -files~=^\.github/ |
There was a problem hiding this comment.
🟡 Medium · .github/ exclusion also blocks the promised operator queue
L11 puts -files~=^\.github/ in the only queue rule's queue_conditions; L29 puts it in the merge protection's success_conditions. Mergify applies queue_conditions to the manual queue command too. Scenario: the operator runs @mergifyio queue on a PR editing .github/workflows/verify.yml or the PR template. The PR waits for queue conditions forever, and its merge-protection check never succeeds. The comment's 'operator queue' path therefore does not exist, and such PRs can merge only by bypassing Mergify. Placing the exclusion only on the auto-merge trigger (auto_merge_conditions) would avoid this. Also reported for this defect: - The workflow guard also blocks authorized manual queueing (.mergify.yml:11): For any PR changing .github/workflows/..., this negated file condition is false. Mergify requires a target queue's queue_conditions even for an explicit @mergifyio queue default command, and no alternate queue exists, so the operator queue described by the comment cannot admit such a PR; the command remains pending and the workflow change cannot progress through this merge queue. - Workflow PRs cannot use the operator queue (.mergify.yml:11): Mergify applies queue_conditions to manual queue commands too. A PR changing .github/workflows/ci.yml therefore cannot enter the default queue even after all three gates genuinely pass and an operator issues @mergifyio queue default. The identical predicate at line 29 also permanently fails its merge protection. This blocks the operator-controlled path promised by the comments, rather than merely excluding workflow changes from automatic queueing.
There was a problem hiding this comment.
Corrected in 91b0d5b: removed the .github/ exclusion and comments from queue_conditions and success_conditions; placed the exclusion only in auto_merge_conditions below base = main, with the requested short comment. Workflow changes cannot auto-queue, but the operator can queue them by hand after the required gates pass.
Restrict the workflow exclusion to automatic queue admission so operators can still manually queue workflow changes after the required gates pass. Co-Authored-By: mastra-platform[bot] <284800079+mastra-platform[bot]@users.noreply.github.com>
There was a problem hiding this comment.
🛑 1 blocking finding
🟡 Medium · Stale queue admission invalidates the exact-head live gate
Check
|
Operator waiver for head 91b0d5b (open-pstack#100, MASTRA-738 live run). Finding v1:f47057cba2df325d463805f016bd47e61951a53fca9508b55efd8ca96815a21e, "Stale queue admission invalidates the exact-head live gate". Class: fails-loudly. The trigger is real: a queued PR falls behind main, and Mergify's in-place update creates a new head with no |
|
Operator checks on exact head Mergify |
|
Queued — the merge queue status continues in this comment ↓. |
|
@Mergifyio queue |
Merge Queue Status
This pull request spent 18 seconds in the queue, including 1 second running CI. Required conditions to merge
|
Merged-completion verification — #100Agent verification on October 2, 2026: all eight approved-plan acceptance items are satisfied, applying the operator-approved replacement of #101 with deployment #107 and proof PR #108. Delivery #107 is actually MERGED to main at Acceptance items 1–4 and 8: operator artifacts read back
Acceptance items 5–7: independently executed in sandboxR=ericlitman/open-pstack
P=108
S=$(gh pr view $P -R $R --json headRefOid --jq .headRefOid)
gh pr view $P -R $R --json state,mergedBy,mergeCommit,comments --jq '{state, by: .mergedBy.login, manual: [.comments[].body|select(test("@Mergifyio"))]|length}'
gh api repos/$R/commits/$(gh pr view $P -R $R --json mergeCommit --jq .mergeCommit.oid) --jq '.parents|length'
gh pr list -R $R --state all --search "head:mergify/merge-queue" --json numberActual output (all commands exited 0): Additional direct API corroboration: on proof head, verify ( No merge, status, readiness or queue action was performed during this verification. Acceptance is complete; requesting Factory Delivery Done separately. FindingsNone |
Related to #100. Supersedes the configuration proposed in #101; the operator will close #101 after this PR merges. Keep #100 open until the separate proof PR demonstrates automatic queueing and merging.
What changed
Add the approved
.mergify.ymlwith a single serial, in-placedefaultqueue. Both queue conditions and merge protections require a non-draft PR withverify,Unfret, and exact-headlive-gatepassing. Empty merge conditions, batch size 1, and max parallel checks 1 avoid temporary batch PRs that Unfret would not review. Mergify owns automatic queueing and uses merge commits. Requeue remains restricted to writers.Only
.mergify.ymlchanges: 34 gross added lines, within the 45-line card budget (including the future one-line proof PR).Verification
Candidate:
92b224961c4ff05a6ac0b16d233a69cffebea230onfactory/issue-100.Sandbox checks actually run:
PSTACK_STATIC_ONLY=1 bash tests/skill-collision-repro.sh— exit 0, all 13 static invariant groups reportedok.JSON.parse— exit 0, four PASS outputs. This replaces only the Bun-based manifest parsing command, not Bun tests or typecheck.git diff --checkandgit diff --cached --check— exit 0.git diff --cached --numstat—34 0 .mergify.ymlbefore committing.Unavailable/deferred (no tools installed, per operator decision):
bun install --frozen-lockfile,bun run test, andbun run typecheck: Bun unavailable.claude plugin validate .andclaude plugin validate plugins/pstack: Claude CLI unavailable.YAML parsing: Python
import yamlfailed withModuleNotFoundError; Nodeyamlandjs-yamlcould not resolve in workspace/system paths.bunxnot used. Operator must validate YAML on the Mac, and the Mergify configuration check must pass.Bun tests, strict typecheck, static invariants, and plugin validation pass (operator, Mac; see evidence).
The exact candidate is installed in every affected harness: none affected (no plugin content changes; repository Mergify configuration only).
The changed behavior passes from its real surface: Mergify's
Configuration changedcheck on this head. Queue behavior is proven by the follow-up proof PR (Configure Mergify to auto-queue PRs once verify and Unfret pass #100).The installed version, action, and observed result appear below.
Live evidence:
91b0d5b47c46e1bcb57e38ded79f1f98f64987f1.mergify.yml; operator ran Bun 1.4.0 tests and typecheck, the manifest parse, static invariants,claude plugin validate(2.1.283) for.andplugins/pstack, the YAML parse andgit diff --checkConfiguration changedpass; 158 tests 0 fail; typecheck, manifests, static invariants, both validations, YAML and diff-check all passOperator handoff
This PR stays draft until the operator runs and records Mac checks and installed-harness evidence for the exact head. The operator marks it ready for Unfret, posts exact-head
live-gate, and performs the one manual queue command needed to deploy this configuration frommain. The builder does not post statuses, mark it ready, queue, or merge it.After this PR merges, the operator posts a comment on #100 starting with
live-gate evidence: #107and resumes the builder. The separate proof PR must hold withoutlive-gate, then auto-queue and merge without a manual queue command. The operator also captures babysit-pr's before/after output with the MASTRA-749 required-check map.A pull request without live evidence remains a draft. Do not merge, tag, release, or roll it out.