diff --git a/.github/workflows/version-gate.yml b/.github/workflows/version-gate.yml index 58f2432c..262baf6e 100644 --- a/.github/workflows/version-gate.yml +++ b/.github/workflows/version-gate.yml @@ -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 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c755dee7..7e5e1fa3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 diff --git a/bin/gstack-next-version b/bin/gstack-next-version index 8bba3257..e10485d9 100755 --- a/bin/gstack-next-version +++ b/bin/gstack-next-version @@ -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"); }