Files
OmniRoute/tests/unit/rehome-open-prs.test.ts
Diego Rodrigues de Sa e Souza d3a9ad557d chore(release): script the 0a.0b PR re-home with a verified read-back (#7312)
The parallel-cycle model hands the frozen release/vX to the captain and cuts
release/vX+1 for everyone else. Phase 0a.0b step 3 then re-homes every open PR
onto the new cycle — today as a hand-run loop of gh pr edit --base.

Three things make that loop unreliable at exactly the moment it matters:

  1. gh pr edit --base FAILS SILENTLY (v3.8.42). It exits 0 and leaves the base
     untouched, so every edit needs a gh pr view --json baseRefName read-back.
     A human mid-release skips that.
  2. gh pr list caps at 30 results by default. A loop written without --limit
     re-homes the first 30 of 148 and reports success.
  3. Volume: the v3.8.49 freeze had 148 open PRs — roughly 450 API calls across
     edit, verify and comment.

The script does the read-back on every PR, uses --limit 300, is idempotent (a
PR already on the next base is skipped, so a resumed release re-runs safely),
refuses to start when the next branch does not exist yet, and exits non-zero
listing any PR whose retarget did not take.

It also prints the reminder that it cannot solve the other half: PRs opened
AFTER it runs. Those need the repo default_branch pointed at the live cycle —
contributors open PRs against the default branch, and while that stays on main
they never target a release branch at all (6 such PRs on 2026-07-15).

classify() is pure and unit-tested: retarget open and draft PRs on the frozen
branch; never touch main (the release PR's own lane), an older shipped release,
or a PR already re-homed.

Refs #7307
2026-07-15 19:48:42 -03:00

57 lines
2.1 KiB
TypeScript

// Guard for scripts/release/rehome-open-prs.mjs — the parallel-cycle PR re-home
// (generate-release Phase 0a.0b step 3).
//
// The script exists because `gh pr edit --base` fails SILENTLY (v3.8.42): it exits 0
// while leaving the base untouched. The network side of that is verified by a read-back
// in the script itself; what is testable here is the decision — which PRs get retargeted
// and which are left alone. Getting that wrong in either direction is expensive:
// - retargeting a PR that was never on the frozen branch drags unrelated work into the cycle
// - skipping one strands a contributor on a branch the captain has taken over
import test from "node:test";
import assert from "node:assert/strict";
import { classify } from "../../scripts/release/rehome-open-prs.mjs";
const FROZEN = "release/v3.8.49";
const NEXT = "release/v3.8.50";
test("retargets an open PR sitting on the frozen release branch", () => {
const { action } = classify(
{ number: 1, baseRefName: FROZEN, isDraft: false },
FROZEN,
NEXT
);
assert.equal(action, "retarget");
});
test("retargets a DRAFT on the frozen branch — a draft still needs a live base", () => {
const { action } = classify({ number: 2, baseRefName: FROZEN, isDraft: true }, FROZEN, NEXT);
assert.equal(action, "retarget");
});
test("skips a PR already re-homed — the script must be idempotent for a resumed release", () => {
const { action, reason } = classify(
{ number: 3, baseRefName: NEXT, isDraft: false },
FROZEN,
NEXT
);
assert.equal(action, "skip");
assert.match(reason, /already re-homed/);
});
test("never touches a PR based on main — that is the release PR's own lane", () => {
const { action, reason } = classify({ number: 4, baseRefName: "main", isDraft: false }, FROZEN, NEXT);
assert.equal(action, "skip");
assert.match(reason, /not the frozen branch/);
});
test("never touches a PR based on an older, already-shipped release", () => {
const { action } = classify(
{ number: 5, baseRefName: "release/v3.8.47", isDraft: false },
FROZEN,
NEXT
);
assert.equal(action, "skip");
});