Skip to content

ci(release): fail a consumer-visible change that ships no version bump - #702

Merged
drewstone merged 1 commit into
mainfrom
ci/version-bump-guard
Aug 1, 2026
Merged

ci(release): fail a consumer-visible change that ships no version bump#702
drewstone merged 1 commit into
mainfrom
ci/version-bump-guard

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

The defect, twice in two days

What happened Why nothing went red
v0.118.0 version bumped on main, tag never cut registry went 0.117.0 -> 0.119.0; no check reads the registry
0.119.0 #697 moved the agent-eval peer floor >=0.139.2 -> >=0.140.1 with no bump main declared a version npm already held under the OLD range

publish.yml skips a version already on the registry, so neither could be fixed by re-tagging. In the 0.119.0 case the journal-envelope fix could not reach a single consumer until #700 cut 0.120.0.

Root cause: nothing asserted that a consumer-visible change arrives with a version bump.

The check

scripts/check-version-bump.mjs compares every publishable manifest against the merge base and fails when any field npm copies into the published manifest moved while version did not go upname, private, type, main, module, types, typings, browser, exports, imports, typesVersions, bin, files, directories, engines, os, cpu, license, sideEffects, publishConfig, dependencies, optionalDependencies, peerDependencies, peerDependenciesMeta, bundleDependencies, and the install-lifecycle scripts (preinstall/install/postinstall, which run on a consumer's machine). version is not part of the compared surface; it is the payment.

catalog: specifiers are compared by what they RESOLVE to, through pnpm-workspace.yaml. That indirection is how the drift stayed invisible: "@tangle-network/agent-knowledge": "catalog:" is byte-identical across the change that moved the installed version 7.0.3 -> 7.0.4, so a manifest diff shows nothing at all. Only the resolved version is compared, so moving a dependency between the default catalog, a named catalog, and an identical literal pin stays silent.

Packages are keyed by name, not path, so relocating a directory still compares against what that name already published.

Resolving the pin also covers bench. Worth knowing: @tangle-network/agent-bench@0.4.9 is published against agent-eval 0.135.2 while the catalog now says 0.140.1 — the same drift, five minors wide, still unshipped. This check would have caught it; fixing it is a separate release.

Wiring

In the ci job beside check:publish-workflow. That job's checkout gains fetch-depth: 0 so the merge base exists.

The base is origin/$GITHUB_BASE_REF — the base branch, deliberately not the webhook's pull_request.base.sha. A pull_request run checks out a merge ref that GitHub recomputes against the current base tip, so the frozen sha can name a commit that is no longer the merge parent; a release someone else merged in that window would then pay for this pull request's unpaid change.

Everything unknown fails closed: a base that will not resolve, a base-carrying event with no base at all (merge queue, workflow edit), and an unresolvable catalog: pin are all errors, never silent passes.

Two scope boundaries, stated so neither reads as an oversight

  • workspace: specifiers are not resolved. One resolves to a sibling in the same commit, so it moves only when that sibling's version moves — which this check already requires to be paid for, on that sibling. Resolving it here would demand a second bump for a change already accounted for, and would fire on every routine release.
  • Only the manifest is compared, never built output. A src/ change that rewrites all of dist/ is allowed with no bump; releases, not this check, decide when code ships.

Calibration — a guard that cannot fail is not a guard

Proven by construction against this repo, then frozen into tests/version-bump-check.test.ts (18 cases over throwaway git repos).

Fires — peer range moved, version stood still:

$ node scripts/check-version-bump.mjs
A consumer-visible change must ship under a higher version.

package.json (@tangle-network/agent-runtime) still declares 0.120.0, but against origin/main (a9d806cb192b) it changes:
  peerDependencies.@tangle-network/agent-eval: ">=0.140.1 <0.141.0" -> ">=0.141.0 <0.142.0"
EXIT=1

Passes — same change, paid for:

Consumer-visible package surfaces carry their version bumps (against origin/main (a9d806cb192b)).
  bench/package.json: consumer surface unchanged at 0.4.9
  package.json: 1 consumer-visible change(s), paid for by 0.120.0 -> 0.121.0
EXIT=0

Fires on a downgrade — a version change is not payment if it lands somewhere the registry may already hold:

package.json (@tangle-network/agent-runtime) moves 0.120.0 -> 0.119.0, which is not higher, but against origin/main (a9d806cb192b) it changes:
  peerDependencies.@tangle-network/agent-eval: ">=0.140.1 <0.141.0" -> ">=0.141.0 <0.142.0"
EXIT=1

Fires on the real #697 shape — only the catalog pin moved; package.json and bench/package.json are byte-identical:

bench/package.json (@tangle-network/agent-bench) still declares 0.4.9, but against origin/main (a9d806cb192b) it changes:
  dependencies.@tangle-network/agent-knowledge: "7.0.4" -> "7.0.5"

package.json (@tangle-network/agent-runtime) still declares 0.120.0, but against origin/main (a9d806cb192b) it changes:
  dependencies.@tangle-network/agent-knowledge: "7.0.4" -> "7.0.5"
EXIT=1

Does not fire on an ordinary source-only edit (nor on a devDependency or non-install script change):

Consumer-visible package surfaces carry their version bumps (against origin/main (a9d806cb192b)).
  bench/package.json: consumer surface unchanged at 0.4.9
  package.json: consumer surface unchanged at 0.120.0
EXIT=0

The tests are load-bearing, not decorative: dropping peerDependencies from the compared fields and emptying the catalog: resolver turns 4 of them red; restoring the guard returns them all green.

Reviewed adversarially before opening

A second model was pointed at the first draft with instructions to REFUTE it, and produced twelve demonstrated defects. Every false-pass it found is fixed and now has a test: workspace:/frozen-base-sha comparison points, a downgrade counting as payment, an empty GITHUB_BASE_SHA falling through to a green no-op, typesVersions/directories/bundleDependencies/install-scripts absent from the compared fields, a moved package treated as new, and private: true hiding a package from inspection. So are the false-fails most likely to get the check disabled: a catalog/literal refactor at the same version, a reordered files, a !-excluded workspace directory, a package nested deeper than one level under **, and a missing pnpm-workspace.yaml crashing with a raw stack.

Twice in two days a change reached main declaring a version the registry
already held: v0.118.0's bump was never tagged, and 0.119.0's agent-eval peer
floor moved from >=0.139.2 to >=0.140.1 with no bump at all. publish.yml skips a
version already on the registry, so neither could be corrected by re-tagging and
neither turned a build red. Nothing asserted that a consumer-visible change
arrives with a version bump.

check:version-bump compares every publishable manifest against the merge base
and fails when any field npm copies into the published tarball moved while
version stood still. version is not part of the compared surface — it is the
payment.

catalog: specifiers are resolved through pnpm-workspace.yaml first. That
indirection is how the drift stayed invisible: "@tangle-network/agent-knowledge":
"catalog:" is byte-identical across the change that moved the installed version
7.0.3 -> 7.0.4, so no manifest diff shows anything. Resolving it also covers
bench, whose published 0.4.9 still carries agent-eval 0.135.2 against a catalog
that now says 0.140.1.

Wired into the ci job beside check:publish-workflow. That job's checkout gains
fetch-depth: 0 so the merge base exists, and the step passes the pull request's
base sha. A named base that will not resolve is an error, never a silent pass.

tests/version-bump-check.test.ts holds the calibration: seven cases over a
throwaway git repo covering both rejection directions, the byte-identical
catalog move, and the two shapes that must NOT fire — a source-only edit and a
devDependency/script change.
@drewstone
drewstone merged commit 5255bc9 into main Aug 1, 2026
4 checks passed
@drewstone
drewstone deleted the ci/version-bump-guard branch August 1, 2026 23:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant