Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions src/base-sha.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
};
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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,
});
});
});

/**
Expand Down
21 changes: 20 additions & 1 deletion src/base-sha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
13 changes: 10 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -542,6 +542,7 @@ async function getRecentReleases(): Promise<Release[]> {
recentReleasesByAccessKey(limit: $limit) {
id
name
version
createdAt
commitSha
}
Expand All @@ -553,7 +554,7 @@ async function getRecentReleases(): Promise<Release[]> {
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 {
Expand All @@ -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;
}
Expand Down
3 changes: 2 additions & 1 deletion src/scan-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
Expand Down
Loading