Skip to content

fix: verify GitHub pull request merge outcomes - #3064

Open
wjkawecki-jt wants to merge 12 commits into
kunchenguid:mainfrom
wjkawecki-jt:fm/fm-pr-merge-verify-outcome
Open

fix: verify GitHub pull request merge outcomes#3064
wjkawecki-jt wants to merge 12 commits into
kunchenguid:mainfrom
wjkawecki-jt:fm/fm-pr-merge-verify-outcome

Conversation

@wjkawecki-jt

@wjkawecki-jt wjkawecki-jt commented Aug 25, 2026

Copy link
Copy Markdown

Intent

GitHub can return success from a merge command without actually merging or queueing the pull request when branch rules require the merge queue. This change reads the pull request back after every successful GitHub merge call and reports success only when that read proves the pull request is merged or in the merge queue; every other readable outcome refuses with its concrete observed state.

When the base branch requires the merge queue, the refusal names the exact -- --auto --<method> retry flags only when all applicable queue rules agree, and reports conflicting methods as ambiguous rather than guessing. The script never changes or auto-applies the operator's chosen merge semantics.

PR metadata and the merge poll remain recorded whenever the pull request exists, including when the merge call succeeds but the outcome read fails. That read failure still exits non-zero and makes no success claim, preserving teardown bookkeeping without treating an unverifiable merge as landed.

The existing gh prerequisite remains because gh-axi depends on it and the verification reads GitHub's API; failing closed when that read is unavailable is intentional. The existing GitLab verification path is unchanged, and no harness or runtime-backend behavior is involved.

What Changed

  • Added fail-closed GitHub merge verification through a post-call read-back, accepting only merged or merge-queued pull requests and reporting the concrete observed state for unsuccessful outcomes.
  • Preserved metadata and merge-poll bookkeeping for forge failures and unreadable GitHub outcomes, while providing exact queue retry flags without changing caller-selected merge semantics; the existing GitLab verification path remains intact.
  • Updated documentation and executable stub-CLI coverage for merged, queued, refusal, queue-guidance, bookkeeping, and read-failure cases. No harness or runtime-backend behavior is involved.

Risk Assessment

⚠️ Medium: Captain, the change is bounded and no concrete defect was found, but it adds higher-impact GitHub API verification to the central merge path.

Testing

The focused merge and adjacent PR-check suites passed, with CLI evidence saved. The teardown suite failed only on the unchanged Herdr missing-adapter fixture. Lint and ShellCheck were not run because this assigned test phase forbids static-analysis commands; the outer executor owns those checks.

Evidence: Focused merge executable transcript

Source: Focused merge executable transcript

ok - fm-pr-merge reports exact queue retry flags after a zero-exit false success
ok - fm-pr-merge omits merge-queue retry guidance for a closed GitHub PR
ok - fm-pr-merge aggregates agreeing merge-queue rules
ok - fm-pr-merge reports ambiguity for conflicting merge-queue rules
ok - fm-pr-merge records pr= and pr_head= for a verified GitHub merge
ok - fm-pr-merge propagates a real merge failure without silently succeeding
ok - fm-pr-merge refuses a GitHub merge call that leaves the PR open and unqueued
ok - fm-pr-merge keeps PR bookkeeping when it cannot read a successful merge call's outcome
ok - fm-pr-merge verifies a genuinely merged GitHub pull request
ok - fm-pr-merge refuses to claim a merge when poll recording fails
ok - fm-pr-merge accepts and accurately reports a GitHub merge-queue entry
ok - fm-pr-merge explains how to retry with the required GitHub merge queue method
ok - fm-pr-merge forwards extra flags to gh-axi pr merge after the -- separator
ok - fm-pr-merge refuses before merging when task meta is missing
ok - fm-pr-merge refuses malformed PR URLs before calling gh-axi
ok - fm-pr-merge refuses unsafe PR URL segments before recording state
ok - fm-pr-merge refuses repo override args before recording state
ok - fm-pr-merge refuses a bundled short-option repo override and forwards other short flags
ok - fm-pr-merge does not add default --squash when the caller passes an explicit merge method
ok - fm-pr-merge respects --method=<value> as an explicit merge method
ok - fm-pr-merge parses a GitHub PR URL into gh-axi number and --repo arguments
ok - fm-pr-merge leaves GitHub extra-arg handling unchanged, including --sha
ok - fm-pr-merge merges a GitLab merge request through glab instead of refusing it
ok - fm-pr-merge takes the GitLab instance from the URL rather than assuming one
ok - fm-pr-merge imposes no merge method on GitLab, leaving the project's own one
ok - fm-pr-merge forwards extra flags to glab mr merge after the -- separator
ok - fm-pr-merge propagates a real glab merge failure without silently succeeding
ok - fm-pr-merge refuses on each GitLab pre-merge condition independently
ok - fm-pr-merge reports every failing GitLab condition, not only the first
ok - fm-pr-merge reports a stale recorded head and verifies the live one
ok - fm-pr-merge refuses an unreadable GitLab merge request state rather than merging blind
ok - fm-pr-merge refuses a GitLab head commit it cannot validate
ok - fm-pr-merge refuses before recording anything when glab or jq is absent
ok - fm-pr-merge refuses a GitLab head override before recording state
- Outcome: ⚠️ 1 warning across 1 run (21m9s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ⚠️ bin/fm-pr-merge.sh:346 - Required criterion: “when the base branch requires the merge queue ... name the exact operator flags that work.” The rules endpoint returns all active rules, and multiple rulesets may apply; if two applicable merge-queue rules use the same method, lines 346-348 reject count=2 and silently omit the compatible retry flags. Aggregate agreeing methods, or explicitly report an ambiguity when they conflict. See rules API and ruleset layering.

🔧 Fix: Aggregate queue rules and report conflicts explicitly
✅ Re-checked - no issues remain.

⚠️ **Test** - 1 warning
  • ⚠️ tests/fm-teardown.test.sh:1547 - The unchanged teardown suite fails at the Herdr missing-adapter fixture: removing the adapter causes an early source error instead of the expected refusal message. This is outside the target diff and unrelated to GitHub merge verification.
  • rtk bash tests/fm-pr-merge.test.sh
  • rtk bash tests/fm-pr-check-security.test.sh
  • rtk bash tests/fm-teardown.test.sh
  • Focused Herdr missing-adapter reproduction with the adapter removed from the copied test fixture
  • Evidence capture via rtk bash -o pipefail -c ...
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@greptile-apps

greptile-apps Bot commented Aug 25, 2026

Copy link
Copy Markdown

Confidence Score: 4/5

The PR does not appear safe to merge until the outstanding GitHub dependency gate stops bypassing the configured gh-axi merge abstraction.

The current GitHub path still exits when standalone gh is unavailable before it can invoke an otherwise usable gh-axi merge command.

Files Needing Attention: bin/fm-pr-merge.sh

Reviews (5): Last reviewed commit: "no-mistakes(review): Aggregate queue rul..." | Re-trigger Greptile

Comment thread bin/fm-pr-merge.sh
Comment thread bin/fm-pr-merge.sh
@wjkawecki-jt

Copy link
Copy Markdown
Author

Re: P1 "Hard dependency bypasses merge abstraction" (bin/fm-pr-merge.sh:145) - not accepted

Thanks for the flag. It was checked against the cited context and the prerequisite check is being kept. Reasoning, point by point:

  1. The cited principle is real, but its subject is different. VISION.md line 62 reads: "Harness adapters earn trust through verification, and the fleet keeps sailing when any one vendor's tool degrades." The subject of that sentence is harness adapters - the swappable agent runtimes firstmate dispatches work onto. gh is not a swappable harness vendor in this script.

  2. The check sits inside an existing provider split. The GitLab branch immediately above it already requires glab and jq up front, with a comment stating that a missing tool should be "a named prerequisite rather than a merge that is armed and then refused for an unexplained reason". This change makes the GitHub path symmetric with a pattern the file already established.

  3. gh is already part of firstmate's universal toolchain, with auth. It is listed in bin/fm-bootstrap.sh's COMMON_TOOLS, and bootstrap reports MISSING: gh and NEEDS_GH_AUTH before any work is dispatched. No working installation gains a new dependency here.

  4. The specific scenario - gh-axi present, gh absent - is not practically reachable, because gh-axi is a wrapper around gh: it invokes the gh binary directly (execFile("gh", ...) in its own src/gh.js).

  5. Refusing when the outcome cannot be verified is the deliberate purpose of this PR. Reporting a merge that was never proven is the exact defect being removed.

One factual note on the current head. An automated fix round removed that preflight after this finding was posted. It did not remove the dependency: bin/fm-pr-merge.sh still calls gh api graphql for the outcome read and gh api for the branch-rules read. The follow-up P1 on line 399 ("Successful merges lose bookkeeping") describes precisely the failure mode the preflight prevented - without the up-front refusal, gh-axi pr merge can land the merge and the later read then fails, so the metadata is never recorded. That second finding argues for restoring the check, not for its removal. The removal is under review.

This reverts commit c326cfa.

The automatic CI repair round removed the up-front `gh` prerequisite check
while keeping the `gh` dependency: `bin/fm-pr-merge.sh` still calls
`gh api graphql` for the outcome read and `gh api` for the branch-rules read.
That left the same hard requirement without the clear named error, and review
immediately raised a new finding for exactly the failure the check prevents -
`gh-axi pr merge` landing the merge while the follow-up read fails, so the PR
metadata is never recorded.

The check is also symmetric with the GitLab arm directly above it, which
already refuses up front when `glab` or `jq` is missing, on the stated
principle that a missing tool should be a named prerequisite rather than a
merge that is armed and then refused for an unexplained reason.

The workflows this round was chasing sit at `action_required` because this is
a fork pull request; no code change can turn them green.
On the GitHub path a merge call that returned success was followed by
`github_read_outcome || exit 1`, so a transient API failure, rate limit,
or network blip during the read dropped out of the script before
`record_pr_metadata` ever ran. The merge could have landed while `pr=`
went unrecorded and the merge poll was never armed - bookkeeping lost on
a real merge. The failure path just above already recorded metadata
before exiting, so the error path was more careful than the success one.

Record the PR before that refusal. Recording arms the later merge poll
and is not a success claim, which is the same reasoning that keeps
`record_pr_metadata` on the gh-axi failure path. The refusal itself is
unchanged: exit stays non-zero and the message still names the concrete
observed state. Metadata is withheld only when the read succeeds and
proves the pull request neither merged nor queued.

Pin it with a case that stubs `gh api graphql` into failure after a
successful `gh-axi pr merge`, asserting both the non-zero exit and the
recorded metadata.
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

Reviewed HEAD fe8443375a62020f4c84ca0a5f87f4817a86f88d vs main 6a2cd6cd2c642c9422bb43dccedbd91103285fb0. Full diff reviewed.

Class: corrective. A GitHub merge is accepted only after a live read proves merged or queued; otherwise the script exits non-zero, names the observed state, and withholds success metadata. Queue retry flags are reported, never auto-applied. Bookkeeping is kept when the merge call succeeds but the follow-up read fails.

VISION (per rule):

  • One captain, one interface — aligns. False success is no longer reported as landed work.
  • Authority is explicit — aligns. Queue flags are named, not applied.
  • Scripts own the mechanics — aligns. Outcome verification is scripted.
  • A restart is a non-event — aligns. Teardown metadata is not recorded for an unproved merge.
  • Delegation with a spine — aligns. Strengthens a refusal path.
  • The fleet outlives any vendor — aligns. The gh preflight is a named forge prerequisite, matching the existing GitLab glab/jq check; gh is not a harness vendor.
  • Scope — aligns. Forge merge verification, not a new merge authority.

The named-gh prerequisite is kept. The author's reply on that point is accepted: refusing when the outcome cannot be verified is the defect being removed, and gh-axi already invokes gh.

Fork CI was approved after this review.

What is not cleared:

  1. Body attestation d83733308c2608cb23aab6fdc87ef6954ebccbd2 does not match this HEAD. Blocking.
  2. CI / Require-no-mistakes were parked at action_required and have now been approved; they had not finished at comment time.

This is waiting on the author, not the captain: push a matching no-mistakes-pipeline-attestation:v1 for this HEAD, and let CI finish green.

Merge-eligible: NO.

@wjkawecki-jt wjkawecki-jt changed the title fix(bin): verify GitHub merge outcomes before recording success fix: verify GitHub pull request merge outcomes Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants