Skip to content

ci(actions): make mergeraptor auto-merge actually fire on main - #400

Merged
castrojo merged 5 commits into
mainfrom
ci/mergeraptor-automerge
Aug 6, 2026
Merged

ci(actions): make mergeraptor auto-merge actually fire on main#400
castrojo merged 5 commits into
mainfrom
ci/mergeraptor-automerge

Conversation

@castrojo

@castrojo castrojo commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

What does this change?

The mergeraptor review-bypass allowance is already live on main's branch protection, but nothing could ever use it. This repo had no workflow_run caller for reusable-renovate-automerge.yml, so qualifying Renovate/MergeRaptor PRs sat green and unmerged, waiting for a human approval. Tasks 2–4 of docs/superpowers/plans/2026-08-04-renovate-automerge-review-exception.md were never implemented.

Add the caller (.github/workflows/renovate-automerge.yml), triggered by every CI workflow in this repo, passing base_branch: main, require_auto_merge: true, and the MergeRaptor app credentials.

Harden the reusable workflow:

  • Mint a MergeRaptor installation token. The bypass allowance applies only to that app identity, so github-actions[bot] could never have merged. Credential presence is mirrored into job-level env because the secrets context is unavailable in step-level if:.
  • Fix the author matcher. GraphQL reports a GitHub App author as the bare slug mergeraptor; REST/gh pr list reports app/mergeraptor; a legacy bot user reports renovate[bot]. The previous filter matched none of these reliably. Now normalised.
  • Gate merges on a genuinely complete rollup. An absent check is not a passing check: gh pr checks reports only check-runs that already exist, so a queued workflow is missing from the rollup rather than PENDING, and a re-run removes its check-runs while they re-queue. Observed live on chore(deps): update taiki-e/install-action digest to 05a4690 #394, where 3 of 7 checks were absent and a naive gate would have merged with them never having run. Since this workflow is triggered by one CI workflow completing while siblings may still be queued, that race is the normal case. Now cross-checks in-flight runs via gh run list --commit. An empty rollup or unreadable run list also keeps waiting rather than failing terminally.
  • Gate merges on a complete check rollup. SKIPPED/NEUTRAL are non-blocking per GitHub's own merge semantics. PENDING is waited out (bounded by the new check_timeout_seconds input) rather than skipped — a consumer triggering from a single CI workflow would otherwise stall permanently when sibling checks finish later. Anything else blocks.
  • Respect a merge queue. main is governed by a merge queue, which rejects any explicit strategy flag (gh pr merge --squash"merge strategy for main is set by the merge queue"). The original hardcoded --squash would therefore have failed every merge on main. Added a merge_method input (default squash, or queue) plus automatic retry without the flag when that error is detected. Found while a review subagent was merging the open dependency PRs.
  • Never infer the merge outcome from stderr or exit code. A plain gh pr merge enqueues while printing nothing and exiting 0 — so the merge_method: queue path, the one actually used on a queued branch, reported every enqueue as "Merged". gh also writes its success line (including the PR title) to stderr, so a PR titled "…merge queue…" could be misread. Now queries the real PR state.
  • Skip drafts and conflicting PRs, and let a failed gh pr merge surface as a job failure instead of a swallowed warning.

renovate.json stays the single source of truth for what may merge unattended: require_auto_merge: true means only PRs Renovate marked auto-mergeable qualify. Major bumps still need a human.

Consumer validation

Consumer PR: projectbluefin/bluefin#976
Consumer CI run: https://github.com/projectbluefin/bluefin/actions/runs/31131080663
Out-of-org consumer impact: N/A — ublue-os/aurora and ublue-os/bazzite do not call reusable-renovate-automerge.yml. In-org consumers (bluefin, bluefin-lts, dakota) all pass only head_sha; both new inputs (require_auto_merge, check_timeout_seconds) default to the previous behaviour, so the change is additive. No input was removed or renamed.

  • Opened a draft consumer PR pinned to this branch SHA
  • Linked a passing consumer CI run that exercised this change
  • Evaluated out-of-org consumers (ublue-os/aurora, ublue-os/bazzite) and documented the impact above

Verification

  • actionlint clean on both workflows (exit 0).
  • Author matcher validated against live GraphQL data for the open PRs on main — correctly selects only the MergeRaptor PRs with autoMergeRequest set.
  • Merge-strategy fallback exercised against five scenarios (plain squash, queue-rejects-flag, merge_method: queue, genuine merge failure, invalid input) — only the genuine failure exits non-zero.
  • Check-rollup gate exercised against all five state combinations (all-green, one failure, empty rollup, pending, all-skipped) and against live gh pr checks output for real PRs on this repo.

Skill update

docs/skills/factory-operations.md §4 rewritten: it documented a packageRules block that does not exist in renovate.json. Now describes the actual policy, the three merge gates, the bypass verification command, a troubleshooting table, and the three gotchas found here (REST-vs-GraphQL bot login, --auto not honouring bypass_pull_request_allowances, secrets unavailable in step-level if:).

Checklist

  • I am using an agent and I take responsibility for this PR (if AI-assisted)
  • Conventional commit message (feat:, fix:, chore:, etc.)
  • No hardcoded secrets or credentials

The mergeraptor review-bypass allowance was already live on main's branch
protection, but nothing could use it: this repo had no workflow_run caller
for reusable-renovate-automerge.yml, so qualifying Renovate PRs sat green
and unmerged waiting on a human approval.

Add the caller and harden the reusable workflow:

- Add .github/workflows/renovate-automerge.yml, triggered by every CI
  workflow in the repo, passing base_branch: main, require_auto_merge: true
  and the MergeRaptor app credentials.
- Mint a MergeRaptor installation token via app_id/private_key secrets. The
  bypass allowance only applies to that app identity, so github-actions[bot]
  could never have merged. Credential presence is mirrored into job-level
  env because the secrets context is unavailable in step-level if:.
- Fix the author matcher. GraphQL reports an app author as the bare slug
  "mergeraptor" while REST reports "app/mergeraptor"; the previous
  gh pr list filter matched neither reliably. Normalise all spellings.
- Gate merges on a complete check rollup. SKIPPED/NEUTRAL are non-blocking
  per GitHub's own merge semantics; PENDING is waited out (bounded by the
  new check_timeout_seconds input) rather than skipped, so a consumer that
  triggers from a single CI workflow cannot stall; anything else blocks.
- Skip drafts and conflicting PRs, and let a failed merge surface as a job
  failure instead of a swallowed warning.

Existing consumers (bluefin, bluefin-lts, dakota) pass only head_sha; both
new inputs default to the previous behaviour, so this is additive for them.

Assisted-by: Claude Opus 5 via GitHub Copilot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@castrojo
castrojo requested a review from p5 as a code owner August 6, 2026 23:29
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Jorge Castro and others added 4 commits August 6, 2026 19:34
The consumer validation protocol says to open a *draft* PR in
projectbluefin/bluefin, but its PR Validation workflow never starts while
the PR is a draft, so there is no run ID to cite as evidence. Pushing more
commits does not help. Record the `gh pr ready` workaround.

Assisted-by: Claude Opus 5 via GitHub Copilot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`gh pr merge --squash` fails with "merge strategy for main is set by the
merge queue" on a queued base branch — the queue owns the strategy. The
hardcoded --squash would therefore have failed every merge on main.

Add a merge_method input (squash by default, or "queue" to always omit the
flag) and retry without the strategy flag automatically when that error is
detected, so the workflow works on queued and unqueued branches alike. A
genuine merge failure still fails the job.

Assisted-by: Claude Opus 5 via GitHub Copilot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`gh pr merge --squash` on a merge-queue branch prints
"! The merge strategy for main is set by the merge queue" and still
enqueues the PR — with exit code 0. Exit status therefore says nothing
about what happened; only the stderr text does.

A retry can also hit "! Pull request <repo>#<n> is already queued to
merge", which shares no substring with the first message and must be
treated as success rather than a failed merge.

Capture and echo stderr on every path, report enqueued vs merged
accurately, and keep the no-flag retry for gh versions that hard-fail
instead of warning. A genuinely unmergeable PR still fails the job.

Verified against all eight real-world outcomes, using the verbatim
strings observed while merging PRs #393, #396, #397 and #383.

Assisted-by: Claude Opus 5 via GitHub Copilot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three defects found by auditing the gate against real merge behaviour
observed on PRs #393, #394, #396, #397 and #383.

1. An absent check is not a passing check. `gh pr checks` reports only
   check-runs that already exist, so a queued workflow is missing from the
   rollup rather than PENDING — and a re-run removes its check-runs while
   they re-queue. Observed live on #394: three of seven checks were absent
   and the gate would have merged with them never having run. Because this
   workflow is triggered by one CI workflow completing while siblings may
   still be queued, that race is the normal case. Cross-check in-flight
   runs for the same commit via `gh run list --commit`.

2. An empty rollup or an unreadable run list now keeps waiting until the
   timeout instead of breaking immediately, so a transient API error is no
   longer terminal for a PR nothing else will re-evaluate.

3. Never infer the merge outcome from stderr or exit code. A plain
   `gh pr merge` enqueues while printing nothing and exiting 0, so the
   merge_method: queue path — the one used on a queued branch — reported
   every enqueue as "Merged". gh also writes its success line, including
   the PR title, to stderr, so a PR titled "...merge queue..." could be
   misread. Query the real PR state instead.

All six gate scenarios re-verified, including the observed #394 and #397
cases; every ambiguous state now fails closed.

Assisted-by: Claude Opus 5 via GitHub Copilot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@castrojo
castrojo merged commit 1ecd732 into main Aug 6, 2026
6 checks passed
@castrojo
castrojo deleted the ci/mergeraptor-automerge branch August 6, 2026 23:57
castrojo added a commit that referenced this pull request Aug 7, 2026
renovate-automerge.yml was added in #400 with a workspace-relative
`uses: ./.github/workflows/reusable-renovate-automerge.yml`, which the
repository's own scripts/check-self-repository-references.py rejects. That
left both the `Lint GitHub Actions` and `pytest` required checks red on
`main`, blocking every subsequent PR.

Switch to the documented `$/` self-repository prefix used by the five other
in-repo reusable workflow calls.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

1 participant