fix(ship): exclude current PR from queue-awareness (self-reference bug)

Version gate flagged PR #1168 as stale because the util counted the PR
itself as a queued claim. The exclude filter removes that self-reference.

New --exclude-pr <N> flag on bin/gstack-next-version. CI workflows pass
github.event.pull_request.number / CI_MERGE_REQUEST_IID. Local /ship
auto-detects via gh pr view when the flag isn't passed, with a warning
recording the auto-exclusion so it's observable.

Caught during the first live ship through the v1.8.0.0 gate — the kind
of dogfood the whole release is designed for.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-23 11:28:02 -07:00
parent bd39b6995f
commit 416a56a5c8
3 changed files with 35 additions and 9 deletions
+1
View File
@@ -58,6 +58,7 @@ jobs:
--bump "${{ steps.bump.outputs.level }}" \
--current-version "${{ steps.versions.outputs.base_version }}" \
--workspace-root null \
--exclude-pr "${{ github.event.pull_request.number }}" \
> next.json 2> next.err
RC=$?
if [ "$RC" != "0" ] || [ ! -s next.json ]; then
+1
View File
@@ -35,6 +35,7 @@ version-gate:
--bump "$LEVEL" \
--current-version "$BASE_VERSION" \
--workspace-root null \
--exclude-pr "$CI_MERGE_REQUEST_IID" \
> next.json
RC=$?
if [ "$RC" != "0" ] || [ ! -s next.json ]; then
+33 -9
View File
@@ -140,7 +140,7 @@ function readBaseVersion(base: string, warnings: string[]): string {
return r.stdout.trim();
}
async function fetchGithubClaimed(base: string, warnings: string[]): Promise<{ claimed: ClaimedPR[]; offline: boolean }> {
async function fetchGithubClaimed(base: string, excludePR: number | null, warnings: string[]): Promise<{ claimed: ClaimedPR[]; offline: boolean }> {
const list = runCommand("gh", [
"pr",
"list",
@@ -175,9 +175,10 @@ async function fetchGithubClaimed(base: string, warnings: string[]): Promise<{ c
// otherwise return our main's VERSION as a phantom claim.
const viewer = runCommand("gh", ["repo", "view", "--json", "owner", "-q", ".owner.login"]);
const myOwner = viewer.ok ? viewer.stdout.trim() : "";
const sameRepoPRs = myOwner
const sameRepoPRs = (myOwner
? prs.filter((p) => (p.headRepositoryOwner?.login ?? "") === myOwner)
: prs;
: prs
).filter((p) => excludePR === null || p.number !== excludePR);
// Fetch each PR's VERSION at its head in parallel (bounded concurrency).
const results: ClaimedPR[] = [];
const queue = [...sameRepoPRs];
@@ -214,7 +215,7 @@ async function fetchGithubClaimed(base: string, warnings: string[]): Promise<{ c
return { claimed: results, offline: false };
}
async function fetchGitlabClaimed(base: string, warnings: string[]): Promise<{ claimed: ClaimedPR[]; offline: boolean }> {
async function fetchGitlabClaimed(base: string, excludePR: number | null, warnings: string[]): Promise<{ claimed: ClaimedPR[]; offline: boolean }> {
const list = runCommand("glab", [
"mr",
"list",
@@ -237,6 +238,9 @@ async function fetchGitlabClaimed(base: string, warnings: string[]): Promise<{ c
warnings.push(`glab mr list returned invalid JSON`);
return { claimed: [], offline: true };
}
if (excludePR !== null) {
mrs = mrs.filter((mr) => mr.iid !== excludePR);
}
const results: ClaimedPR[] = [];
for (const mr of mrs) {
const content = runCommand("glab", [
@@ -342,11 +346,12 @@ function markActiveSiblings(siblings: Sibling[], baseVersion: Version): Sibling[
});
}
function parseArgs(argv: string[]): { base: string; bump: Bump; current: string; workspaceRoot?: string; help: boolean } {
function parseArgs(argv: string[]): { base: string; bump: Bump; current: string; workspaceRoot?: string; excludePR: number | null; help: boolean } {
let base = "";
let bump: Bump | "" = "";
let current = "";
let workspaceRoot: string | undefined;
let excludePR: number | null = null;
let help = false;
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
@@ -354,9 +359,13 @@ function parseArgs(argv: string[]): { base: string; bump: Bump; current: string;
else if (a === "--bump") bump = (argv[++i] ?? "") as Bump;
else if (a === "--current-version") current = argv[++i] ?? "";
else if (a === "--workspace-root") workspaceRoot = argv[++i];
else if (a === "--exclude-pr") {
const n = Number(argv[++i]);
excludePR = Number.isFinite(n) && n > 0 ? n : null;
}
else if (a === "-h" || a === "--help") help = true;
}
if (help) return { base: "", bump: "micro", current: "", help: true };
if (help) return { base: "", bump: "micro", current: "", excludePR: null, help: true };
if (!base) base = "main";
if (!bump) {
console.error("Error: --bump is required (major|minor|patch|micro)");
@@ -366,7 +375,17 @@ function parseArgs(argv: string[]): { base: string; bump: Bump; current: string;
console.error(`Error: --bump must be major|minor|patch|micro (got ${bump})`);
process.exit(2);
}
return { base, bump: bump as Bump, current, workspaceRoot, help: false };
return { base, bump: bump as Bump, current, workspaceRoot, excludePR, help: false };
}
// Auto-detect: if --exclude-pr wasn't passed, check whether the current branch
// already has an open PR and exclude it by default. This prevents the self-
// reference bug where /ship's own PR inflates the queue on rerun.
function autoDetectExcludePR(): number | null {
const r = runCommand("gh", ["pr", "view", "--json", "number", "-q", ".number"]);
if (!r.ok) return null;
const n = Number(r.stdout.trim());
return Number.isFinite(n) && n > 0 ? n : null;
}
async function main() {
@@ -386,12 +405,17 @@ async function main() {
process.exit(2);
}
const excludePR = args.excludePR ?? autoDetectExcludePR();
if (excludePR !== null && args.excludePR === null) {
warnings.push(`auto-excluded PR #${excludePR} (current branch's own PR)`);
}
let claimed: ClaimedPR[] = [];
let offline = false;
if (host === "github") {
({ claimed, offline } = await fetchGithubClaimed(args.base, warnings));
({ claimed, offline } = await fetchGithubClaimed(args.base, excludePR, warnings));
} else if (host === "gitlab") {
({ claimed, offline } = await fetchGitlabClaimed(args.base, warnings));
({ claimed, offline } = await fetchGitlabClaimed(args.base, excludePR, warnings));
} else {
warnings.push("host unknown; queue-awareness unavailable");
}