fix(#2515): auto-merge the release/hotfix -> main PR when clean (#2516)

The finalize job created the release/hotfix -> main merge-back PR but
never merged it, so every release and hotfix needed a manual merge click
on main. The opposite direction (main -> next) is already admin-merged by
auto-backmerge.yml; this direction was the asymmetric manual step where
the recurring divergence got hand-reconciled.

Adds a finalize step (after "Verify publish", so main only absorbs a
confirmed-published release) that finds the open merge-back PR, polls
until GitHub settles its mergeability, and admin-merges it with a merge
commit ONLY when MERGEABLE. A CONFLICTING PR is left open for manual
resolution rather than force-merged. Non-fatal (continue-on-error): the
tag + npm publish already happened, so a merge-back that can't complete
(org PR policy, token) must not fail the release.

With the main-is-ancestor-of-next invariant restored (#2504), this merge
is clean every release -- verified: a simulated 1.8.1 hotfix merges to
main producing [1.8.1]->[1.8.0]->[1.7.0]->[1.6.1] and version 1.8.1 with
no conflict, and main's hardened auto-backmerge.yml survives the merge.

Guarded by three assertions in release-backmerge-invariants.test.cjs
(step present + admin-merge, gates on MERGEABLE, continue-on-error);
verified they fail when --admin or the MERGEABLE guard or the
continue-on-error is removed.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-21 23:23:45 -04:00
committed by GitHub
parent 21c22d8cb0
commit 9181c77df5
2 changed files with 98 additions and 0 deletions

View File

@@ -119,4 +119,59 @@ describe('release backmerge invariants (#2504) — release.yml finalize', () =>
'cancels finalize mid-test before tag/publish as the unit suite grows. See #2280/#2281.'
);
});
// #2515: the release/hotfix → main merge-back must complete automatically when
// clean, not sit as a manual merge every release. This locks in that step and
// its guardrails: it merges only a MERGEABLE PR (never force-merges a
// conflict), and is non-fatal (a published release stands even if the
// merge-back can't auto-complete).
const finalizeSteps = (wf.jobs && wf.jobs.finalize && wf.jobs.finalize.steps) || [];
const automerge = finalizeSteps.find(
(s) => typeof s.name === 'string' && s.name.includes('Auto-merge the release')
);
test('finalize auto-merges the release/hotfix → main PR', () => {
assert.ok(automerge, "expected a finalize step named like 'Auto-merge the release → main PR'");
assert.match(
automerge.run || '',
/pr merge\b[^\n]*--admin/,
'the auto-merge step must admin-merge the merge-back PR (symmetric with auto-backmerge main→next). See #2515.'
);
});
test('the auto-merge only fires on a cleanly MERGEABLE PR (never force-merges a conflict)', () => {
assert.ok(automerge, "expected the 'Auto-merge the release → main PR' step");
assert.match(
automerge.run || '',
/MERGEABLE/,
'the auto-merge must gate on the PR being MERGEABLE so a genuine divergence is left open for ' +
'manual resolution rather than force-merged into main. See #2515.'
);
});
test('the auto-merge step is non-fatal (a published release stands even if it cannot complete)', () => {
assert.ok(automerge, "expected the 'Auto-merge the release → main PR' step");
assert.equal(
automerge['continue-on-error'],
true,
'the merge-back auto-merge must be continue-on-error: the tag + npm publish already happened, ' +
'so a merge-back that cannot complete (org policy, token) must not fail the release. See #2515.'
);
});
test('the auto-merge runs AFTER "Verify publish" (main only absorbs a published release)', () => {
const verifyIdx = finalizeSteps.findIndex(
(s) => typeof s.name === 'string' && s.name.includes('Verify publish')
);
const automergeIdx = finalizeSteps.findIndex(
(s) => typeof s.name === 'string' && s.name.includes('Auto-merge the release')
);
assert.ok(verifyIdx !== -1, "expected a 'Verify publish' step");
assert.ok(automergeIdx !== -1, "expected the 'Auto-merge the release → main PR' step");
assert.ok(
automergeIdx > verifyIdx,
'the auto-merge must run after "Verify publish" so main never absorbs a release whose npm ' +
'publish was not confirmed. Order is the invariant, not just a comment. See #2515.'
);
});
});