Repository navigation
Approval-triggered API squash-merge for the release PR (clean commit, all-CI gate) #303
Description
Goal
Merge the persistent next → master (and hotfixes → master) release PR via the REST merge API with an explicit commit message, triggered by the maintainer's approval, gated on all CI (not just required checks). This produces a clean, reliable release commit and eliminates two problems with the current manual UI squash:
- Body-drop. The mobile "Squash and merge" Default path lands a title-only commit (empty body), stripping the curated changelog + coverage. Supplying
commit_messageourselves makes the drop impossible. (Companion to Release notes & tag message are stripped to title-only when the release squash commit omits the PR body #298/fix(release): source release notes from the PR body when the squash drops it #299, which made the Release notes robust; this makes the commit robust.) - Messy co-author trailers. GitHub's squash UI auto-appends
Co-authored-by:for every distinct commit author — not deduped by account, not bot-stripped (e.g.cldmv-bot[bot]+ the same human under two email variants). That auto-population is a UI/default-composition behavior; a merge-API call with an explicitcommit_messageuses the message verbatim and adds nothing. The PR body already carries the clean, deduped, bot-free<!-- co-authors -->block (fix(release): credit release-PR contributors via Co-authored-by trailers #292), so it becomes the authoritative credit.
It also sidesteps GitHub's native auto-merge dead-end: enablePullRequestAutoMerge refuses a PR that's already green ("clean status"). A direct PUT …/merge is the opposite — it succeeds precisely when the PR is mergeable.
Trigger
Approval-triggered. The maintainer approves the release PR as they already do; the merge fires once CI is green. A label arm was considered and rejected — CI already stamps release / ! release → master, so an arming label would be confusing.
Caller events: pull_request_review (submitted, state == approved) to arm, plus check_suite: completed (and/or status) to re-evaluate so an approval given before CI finishes still fires when the last check goes green. If a later commit lands on next and the ruleset dismisses the stale approval, the maintainer re-approves — an accepted tradeoff.
Gate (all CI, not just required)
Before merging, enumerate every check-run (/commits/{sha}/check-runs) and commit status (/commits/{sha}/status) on the PR head — the full set, not just the ruleset's required contexts (coverage badge, PR-body refresh, etc. are non-required and must finish first). Merge only when:
- all are in a terminal state (nothing
queued/in_progress) — the merge workflow excludes its own run so it doesn't wait on itself; - none failed —
success/neutral/skippedpass, anyfailure/cancelled/timed_outstops and reports (an explicitly-allowed-to-fail check can be whitelisted); - an approving review from an allowed association (MEMBER/OWNER) is present.
If not all terminal yet → exit cleanly and wait for the next check-completion event to re-fire. Because the gate waits for the non-required body-refresh job too, the PR body is fully composed by merge time, so reading it fresh for commit_message can't capture a stale (pre-coverage) body — resolving the race raised in the original discussion.
Merge
PUT /repos/{o}/{r}/pulls/{n}/merge with merge_method: "squash", sha: <head>, commit_title: <PR title>, commit_message: <PR body fetched fresh at merge time>. Does not approve as the bot (the human approval is the gate). The bot App token (contents+PR write, allowed to land on master) performs the merge; the maintainer's approval satisfies the ruleset's required review, so no bypass of the human gate.
Shape
- New reusable workflow
reusable-release-merge.yml+ job actiongithub/jobs/release-merge+ a thin caller templaterelease-merge.yml, wired org-wide like the other release-flow workflows. - Reuses the existing scaffolding (
create-app-token, theapi()helper, the reusable→job structure) but notapproveAndEnableAutoMerge(that approves as the bot and uses native auto-merge with a no-commit_messagefallback — wrong for a release). - Only ever acts on the release PR (head
next/hotfixes→ base the release branch, carrying the release label); idempotent/benign on races and when already merged.
Consequence for #292
With this in place, keep #292's co-author block — an explicit-message API merge adds nothing of GitHub's own, so the block is exactly what lands in the commit (deduped, bot-free). The revert path (#301) is only relevant if we stay on UI merges.
Activity
cldmv-bot commentedon Sep 16, 2026
Closed in v4.28.0 — resolved by #304.