From c74c4266efed9196ce19eae9304d937ee56ede42 Mon Sep 17 00:00:00 2001 From: Romain Cascino Date: Wed, 26 Aug 2026 16:20:38 +0200 Subject: [PATCH] Prefer the syncing version's own release as scan base --- src/base-sha.test.ts | 41 ++++++++++++++++++++++++++++++++++++++--- src/base-sha.ts | 21 ++++++++++++++++++++- src/index.ts | 13 ++++++++++--- src/scan-base.ts | 3 ++- 4 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/base-sha.test.ts b/src/base-sha.test.ts index fcc9534..b52fbd7 100644 --- a/src/base-sha.test.ts +++ b/src/base-sha.test.ts @@ -18,10 +18,11 @@ function commit(cwd: string, file: string, content: string, message: string): st return runGit("rev-parse HEAD", cwd); } -function release(name: string, commitSha: string | undefined, daysAgoCreated: number): Release { +function release(name: string, commitSha: string | undefined, daysAgoCreated: number, version?: string): Release { return { id: `id-${name}`, name, + version, commitSha, createdAt: new Date(Date.now() - daysAgoCreated * 24 * 60 * 60 * 1000).toISOString(), }; @@ -61,11 +62,11 @@ function buildRepo() { // Back to main runGit("checkout -q main", cwd); - commit(cwd, "f", "2", "m2"); + const m2 = commit(cwd, "f", "2", "m2"); const mainPrev = commit(cwd, "f", "3", "m3 (1.71.0 release)"); const mainHead = commit(cwd, "f", "4", "m4 (1.72.0 HEAD)"); - return { cwd, hotfixSha, hotfixHead, mainPrev, mainHead }; + return { cwd, m1, m2, hotfixSha, hotfixHead, mainPrev, mainHead }; } describe("findBaseSha", () => { @@ -127,6 +128,40 @@ describe("findBaseSha", () => { it("scenario F — empty list (first-ever sync): returns fallback", () => { expect(findBaseSha([], repo.mainHead, deps)).toEqual({ kind: "fallback" }); }); + + it("scenario G — zombie ordering without a version match: picks the stale reachable candidate", () => { + // Server-side recency ordering is the guard against stale started releases + // appearing first; without a version match, the existing walk is preserved. + const candidates = [ + release("stale started release", repo.m1, 21), + release("newer completed release", repo.mainPrev, 1), + ]; + expect(findBaseSha(candidates, repo.mainHead, deps)).toEqual({ kind: "found", sha: repo.m1 }); + }); + + it("scenario H — reachable version match: takes priority over an earlier reachable candidate", () => { + const candidates = [ + release("stale started release", repo.m1, 21), + release("newer completed release", repo.mainPrev, 1), + release("matching release", repo.m2, 2, "1.72.0"), + ]; + expect(findBaseSha(candidates, repo.mainHead, deps, "1.72.0")).toEqual({ + kind: "found", + sha: repo.m2, + }); + }); + + it("scenario I — non-ancestor version match: falls back to the normal walk", () => { + const candidates = [ + release("stale started release", repo.m1, 21), + release("unreachable matching release", repo.hotfixSha, 2, "1.72.0"), + release("newer completed release", repo.mainPrev, 1), + ]; + expect(findBaseSha(candidates, repo.mainHead, deps, "1.72.0")).toEqual({ + kind: "found", + sha: repo.m1, + }); + }); }); /** diff --git a/src/base-sha.ts b/src/base-sha.ts index 16bcf4d..b3ff833 100644 --- a/src/base-sha.ts +++ b/src/base-sha.ts @@ -12,8 +12,27 @@ export type FindBaseShaDeps = { * release candidates (most-relevant first). Returns the first candidate whose * `commitSha` is reachable from `headSha` — the API can't disambiguate * concurrent release trains via SQL alone, so we use git as ground truth. + * When a syncing version is supplied, a reachable candidate with that version + * takes priority over the list order. */ -export function findBaseSha(candidates: Release[], headSha: string, deps: FindBaseShaDeps): BaseShaResult { +export function findBaseSha( + candidates: Release[], + headSha: string, + deps: FindBaseShaDeps, + syncingVersion?: string, +): BaseShaResult { + if (syncingVersion !== undefined) { + for (const candidate of candidates) { + if (candidate.version !== syncingVersion || !candidate.commitSha) { + continue; + } + if (deps.verifyAncestorReachable(candidate.commitSha, headSha)) { + verbose(`Using base SHA from release "${candidate.name}" (${candidate.commitSha.slice(0, 7)})`); + return { kind: "found", sha: candidate.commitSha }; + } + } + } + for (const candidate of candidates) { const sha = candidate.commitSha; if (!sha) { diff --git a/src/index.ts b/src/index.ts index fb84434..c903b7a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -292,7 +292,7 @@ async function syncCommand(): Promise<{ } const recentReleases = await getRecentReleases(); - const scanBase = getScanBase(recentReleases, currentCommit.commit); + const scanBase = getScanBase(recentReleases, currentCommit.commit, releaseVersion); let latestSha = scanBase.sha; let inspectingOnlyCurrentCommit = false; @@ -542,6 +542,7 @@ async function getRecentReleases(): Promise { recentReleasesByAccessKey(limit: $limit) { id name + version createdAt commitSha } @@ -553,7 +554,7 @@ async function getRecentReleases(): Promise { return response.data.recentReleasesByAccessKey; } -function getScanBase(candidates: Release[], currentSha: string): ScanBase { +function getScanBase(candidates: Release[], currentSha: string, syncingVersion?: string): ScanBase { if (baseRef) { let resolvedSha: string; try { @@ -566,7 +567,13 @@ function getScanBase(candidates: Release[], currentSha: string): ScanBase { return { kind: "base-ref", sha: resolvedSha, ref: baseRef }; } - const scanBase = selectAutomaticScanBase(candidates, currentSha, { verifyAncestorReachable }); + const scanBase = selectAutomaticScanBase( + candidates, + currentSha, + { verifyAncestorReachable }, + undefined, + syncingVersion, + ); if (scanBase.kind !== "first-sync") { return scanBase; } diff --git a/src/scan-base.ts b/src/scan-base.ts index 576d4f2..8bff0ad 100644 --- a/src/scan-base.ts +++ b/src/scan-base.ts @@ -46,8 +46,9 @@ export function selectAutomaticScanBase( currentSha: string, deps: FindBaseShaDeps, cwd: string = process.cwd(), + syncingVersion?: string, ): ScanBase { - const result = findBaseSha(candidates, currentSha, deps); + const result = findBaseSha(candidates, currentSha, deps, syncingVersion); if (result.kind === "found") { return { kind: "release", sha: result.sha }; }