From 4e9ca035206f8ee4be8c033b6cc96e186b8d4778 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 26 Aug 2026 08:42:03 -0700 Subject: [PATCH 1/2] ci(version-guard): require an increase, not merely a difference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The check compared base_v to head_v for INEQUALITY. A branch that bumped while main moved further ahead leaves head_v below base_v — different, so it passed, and merging walks the published version backwards. Not hypothetical: with main's cli at 0.1.21, #1217 (head 0.1.20) and #1215 (head 0.1.19) both show this check SUCCESS right now. Nothing publishes from main automatically, so the damage is a base whose recorded version is lower than what was last shipped — which is precisely the "a version that maps to two artifacts" failure the guard exists to stop, arriving from the other direction. Uses `sort -V` rather than a lexical compare, so 0.1.9 -> 0.1.10 is an increase. Truth table exercised under bash across same / lower / higher / 0.1.9-vs-0.1.10 / empty-head. Co-Authored-By: Claude Opus 5 --- .github/workflows/package-version-guard.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/package-version-guard.yml b/.github/workflows/package-version-guard.yml index 78a35b64f..b9b40a3d3 100644 --- a/.github/workflows/package-version-guard.yml +++ b/.github/workflows/package-version-guard.yml @@ -84,11 +84,21 @@ jobs: base_v=$(git show "$BASE:$pkg/package.json" 2>/dev/null | sed -n 's/.*"version": "\([^"]*\)".*/\1/p' | head -1) head_v=$(sed -n 's/.*"version": "\([^"]*\)".*/\1/p' "$pkg/package.json" | head -1) + # An INCREASE, not merely a difference. A long-lived branch that + # bumped while the base moved further ahead leaves head_v BELOW + # base_v — different, so an equality test passes it, and merging + # then walks the version backwards on the base. Observed on two + # open PRs at once (base 0.1.21, heads 0.1.20 and 0.1.19), both + # showing this check green. + newest=$(printf '%s\n%s\n' "$base_v" "$head_v" | sort -V | tail -1) if [ "$base_v" = "$head_v" ]; then echo "::error file=$pkg/package.json::$pkg/src changed ($changed file(s)) but version is still $head_v. A published version that maps to two different artifacts defeats the only check available from outside this repo — bump it, or move the change out of $pkg/src." fail=1 + elif [ "$newest" != "$head_v" ]; then + echo "::error file=$pkg/package.json::$pkg/src changed ($changed file(s)) and $head_v is BELOW the base's $base_v. Merging would walk the published version backwards. Rebase and bump above $base_v." + fail=1 else - echo "✓ $pkg: src changed and version moved $base_v → $head_v" + echo "✓ $pkg: src changed and version rose $base_v → $head_v" fi done From 3d9b4fea3473ba0172d4dcdb31b4361555a108fb Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:06:47 -0700 Subject: [PATCH 2/2] ci(version-guard): refuse to judge a prerelease rather than fail open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sort -V` is not semver. It orders 1.0.0 before 1.0.0-beta.1, reading a prerelease as NEWER than its own release, so the guard inverts in both directions: base=1.0.0 head=1.0.0-beta.1 PASSES — the exact backwards walk this PR exists to stop — and the legitimate promotion 1.0.0-beta.1 → 1.0.0 FAILS. Latent today: zero of the 41 versions ever committed to cli/package.json and commonly-mcp/package.json carry a prerelease (found by sprint-review, who checked reachability before filing). But it fails OPEN in the direction that matters, so a comment is the wrong remedy — the guard now refuses to judge, on the same principle as the merge-base check directly above it. Truth table run under GNU coreutils 9.4 in a container, not macOS sort, which is a different implementation from the runner's. Six rows; the control without this branch reproduces both inversions and leaves the other four rows unchanged. Co-Authored-By: Claude Opus 5 --- .github/workflows/package-version-guard.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/package-version-guard.yml b/.github/workflows/package-version-guard.yml index b9b40a3d3..b0f44f388 100644 --- a/.github/workflows/package-version-guard.yml +++ b/.github/workflows/package-version-guard.yml @@ -84,6 +84,24 @@ jobs: base_v=$(git show "$BASE:$pkg/package.json" 2>/dev/null | sed -n 's/.*"version": "\([^"]*\)".*/\1/p' | head -1) head_v=$(sed -n 's/.*"version": "\([^"]*\)".*/\1/p' "$pkg/package.json" | head -1) + # `sort -V` is not semver. It orders 1.0.0 BEFORE 1.0.0-beta.1, + # reading a prerelease as NEWER than its own release, and it + # inverts in both directions: base=1.0.0 head=1.0.0-beta.1 would + # PASS — the exact backwards walk this check exists to stop — and + # the legitimate promotion 1.0.0-beta.1 → 1.0.0 would FAIL. + # No version ever committed to either package carries a + # prerelease, so this is latent today. It fails OPEN in the + # direction that matters, so the guard refuses to judge rather + # than guessing, on the same principle as the merge-base check + # above: a guard that cannot compare its inputs must say so. + case "$base_v$head_v" in + *-*) + echo "::error file=$pkg/package.json::$pkg version comparison involves a prerelease ($base_v → $head_v). \`sort -V\` orders a prerelease as NEWER than its release, so this guard reaches the wrong answer in BOTH directions. Teach it semver before landing a prerelease; do not merge on this check alone." + fail=1 + continue + ;; + esac + # An INCREASE, not merely a difference. A long-lived branch that # bumped while the base moved further ahead leaves head_v BELOW # base_v — different, so an equality test passes it, and merging