diff --git a/bin/gstack-next-version b/bin/gstack-next-version index ddfc27b81..b2b85abcd 100755 --- a/bin/gstack-next-version +++ b/bin/gstack-next-version @@ -499,12 +499,26 @@ function fetchGitClaimed( // claim set and pushed the allocation further than the real queue. // GIT_TERMINAL_PROMPT=0 + a 5s timeout keep a dead/credential-prompting // remote from hanging the allocator. - const lsRemote = spawnSync("git", ["ls-remote", "--heads", "origin"], { - encoding: "utf8", - timeout: 5000, - env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }, - }); - const lsOk = lsRemote.status === 0 && !lsRemote.error; + // Guard: only consult ls-remote when a remote named `origin` is actually + // CONFIGURED. Without the guard, git DWIMs the literal `origin` as an + // ssh host/path — and on hosts whose ssh launders exit codes (observed: + // Vercel sandboxes' ssh exits 0 on "Could not resolve hostname"), the + // probe "succeeds" with zero branches and the allocator silently sees an + // empty queue: the exact duplicate-allocation failure (#2545) this + // function exists to prevent. + const originConfigured = + spawnSync("git", ["remote", "get-url", "origin"], { + encoding: "utf8", + timeout: 5000, + }).status === 0; + const lsRemote = originConfigured + ? spawnSync("git", ["ls-remote", "--heads", "origin"], { + encoding: "utf8", + timeout: 5000, + env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }, + }) + : null; + const lsOk = lsRemote !== null && lsRemote.status === 0 && !lsRemote.error; // Each candidate carries the LIVE tip sha when it came from ls-remote, so // the VERSION read prefers the fresh commit (present locally after any