From 371c8c0f779809bafce415ce79a0106e4388a19f Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:43:45 +0200 Subject: [PATCH] fix(ci): catch an unbumped desktop change on the PR, not on main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Today a dependabot bump turned main red and stopped the merge queue. The gate was right; its TIMING was racy, and the race is the whole bug. What happened, in order: #482 merges, mints fleet-runner-v0.8.16. #411 (dependabot, touches ONLY desktop/package-lock.json) had already run its CI while v0.8.15 was newest. Against v0.8.15 its base ALREADY read 0.8.16, so "version is ahead of released" held — CI PASSED. #411 merges. Newest tag is now v0.8.16, the only desktop change since it is that lockfile, and the identical check goes RED on main. Both evaluations were correct. The gap is between them, and everyone else pays: the sweep merges nothing onto a red base, so every unrelated PR stalls behind a bot's dependency bump. #483 failed CI on a diff that touched no desktop file. The existing question is cumulative — "has anything changed since the last tag" — and its answer moves when a tag is minted, i.e. for reasons outside the branch. So ask a second question that has no race in it: does the diff of THIS BRANCH touch desktop/, and if so does THIS BRANCH bump desktop/package.json? Decided entirely within the branch, identically before and after any tag appears. It fires on the PR — one blocked bot PR a human can see — instead of after the merge, where it is a red main and a stopped queue. On main itself the branch diff is empty and the rule does not apply; the cumulative check still guards there. Mutation-proven: desktop change, no bump -> exit 1, "this branch changes desktop/ but does not bump desktop/package.json", naming the file + bump, no changelog -> exit 1 on the pre-existing changelog rule (the two compose; a bump alone is still not publishable) neither -> passes, rule correctly silent This is the fix I would rather have than the automation I had offered — opening a bump PR after the fact repairs the outage; this prevents it. pnpm run verify passes. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UvjGNAS9CMfEGNW26tUR4P --- scripts/test/desktop-release-drift.ts | 76 +++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/scripts/test/desktop-release-drift.ts b/scripts/test/desktop-release-drift.ts index 57f6f882..73566848 100644 --- a/scripts/test/desktop-release-drift.ts +++ b/scripts/test/desktop-release-drift.ts @@ -198,6 +198,82 @@ assert( `from this version again.`, ); +/** + * THIS BRANCH'S OWN desktop change must carry its own bump. + * + * Everything above is cumulative — "has anything changed since the last tag" — + * and that is RACY across a merge queue. Observed 2026-09-04: + * + * #482 merges, mints fleet-runner-v0.8.16 + * #411 (dependabot, touches ONLY desktop/package-lock.json) had run its CI + * while v0.8.15 was still newest. Against v0.8.15 the base already read + * 0.8.16, so "version is ahead of released" held and the check PASSED. + * #411 merges. Now the newest tag is v0.8.16, the only desktop change since + * it is that lockfile, and the same check goes RED — on main. + * + * Both evaluations were correct; the gap is between them. The cost is paid by + * everyone else: main red, and the sweep merges nothing onto a red base, so + * every unrelated PR stalls behind a bot's dependency bump. + * + * So also ask a question that has no race in it: does the diff of THIS BRANCH + * touch desktop/, and if so does THIS BRANCH bump the version? That is decided + * entirely within the branch, identically before and after any tag is minted. + * + * It fires on the PR, where it is one blocked bot PR that a human can see, + * instead of after the merge, where it is a red main and a stopped queue. + */ +function baseRef(): string | null { + for (const ref of ["origin/main", "origin/master"]) { + try { + git("rev-parse", "--verify", "--quiet", ref); + return ref; + } catch { + /* try the next one */ + } + } + return null; +} + +const base = baseRef(); +if (base) { + let mergeBase = ""; + try { + mergeBase = git("merge-base", "HEAD", base); + } catch { + mergeBase = ""; + } + // On main itself the diff is empty and this rule simply does not apply — the + // cumulative check above is what guards there. + if (mergeBase && mergeBase !== git("rev-parse", "HEAD")) { + const branchDesktopFiles = git("diff", "--name-only", mergeBase, "HEAD", "--", "desktop") + .split("\n") + .map((f) => f.trim()) + .filter(Boolean) + .filter((f) => !f.endsWith(".md")); + + if (branchDesktopFiles.length > 0) { + let baseVersion = ""; + try { + baseVersion = readVersionFrom(git("show", `${mergeBase}:desktop/package.json`)); + } catch { + baseVersion = ""; + } + assert( + baseVersion !== "" && compareVersions(currentVersion, baseVersion) > 0, + `this branch changes desktop/ but does not bump desktop/package.json ` + + `(still ${currentVersion}, same as its base). A desktop change that ` + + `merges without a bump goes red on MAIN once the next release tag is ` + + `minted, and the sweep will not merge anything onto a red base — so ` + + `one unbumped dependency PR stalls the whole queue.\n` + + `Fix: bump desktop/package.json and add the matching ` + + `FLEET_RUNNER_RELEASES entry in src/config/changelog.ts.\n` + + `Changed here: ${branchDesktopFiles.slice(0, 8).join(", ")}` + + (branchDesktopFiles.length > 8 ? `, +${branchDesktopFiles.length - 8} more` : ""), + ); + } + } +} + // ── The gate must not outlive the machinery it assumes ───────────────────── // // Everything above trusts two things about ci.yml: that tags are fetched (or