Skip to content

anchor guard: fail a PR that changes a lockstep-stamped tree without naming the anchor - #356

Merged
brentrager merged 1 commit into
mainfrom
th-54fc00-anchor-guard
Aug 14, 2026
Merged

anchor guard: fail a PR that changes a lockstep-stamped tree without naming the anchor#356
brentrager merged 1 commit into
mainfrom
th-54fc00-anchor-guard

Conversation

@brentrager

Copy link
Copy Markdown
Contributor

The problem

Every smooth-operator artifact — npm, NuGet, PyPI, crates.io — ships at one shared version, and that version lives in exactly one place: @smooai/smooth-operator (typescript/package.json). Changesets natively versions only npm packages, so scripts/sync-versions.mjs stamps that single number onto every other published manifest.

The consequence people miss: a changeset that doesn't name the anchor cannot republish anything outside npm. And one word away sits @smooai/smooth-operator-server — the TypeScript server. If you just changed the .NET server, that name is the intuitive pick and the wrong one.

It fails silently and late: CI green, PR merged, release workflow succeeds, artifact never republished.

PR changeset named outcome
#346 (ts) …-server + anchor shipped ✅
#348 (dotnet file transfer) …-server only stranded on main
#352 (dotnet skill) …-server only stranded on main

Nobody noticed until a downstream consumer read the published NuGet two days later and filed a request asking us to build a feature that had been on main the whole time. #354 unstuck the release; this PR stops it recurring.

The rule

Touching a lockstep-stamped tree with a changeset ⇒ at least one changeset must name @smooai/smooth-operator.

Conditioned on having a changeset at all, deliberately. Docs- and test-only PRs legitimately have none, and a guard that cries wolf on them gets ignored — at which point it protects nothing. The bug being caught is not "no changeset", it's "a changeset naming the wrong package".

No second source of truth

sync-versions.mjs is now importable — stamping runs only when it's invoked directly — so the guard derives the stamped trees from the very list that does the stamping. Add a target there and the guard learns about it for free; there's no path list to keep in sync.

typescript/ is deliberately not a stamped tree (changesets versions it natively), so ordinary TS PRs are never forced to bump the anchor.

Verification

Replayed against real history — the strongest check available:

#348 dotnet file transfer    → FAIL (guard fires)  touched=[dotnet/server/src] names=[@smooai/smooth-operator-server]
#346 ts file transfer        → PASS                touched=[]                 names=[…-server, @smooai/smooth-operator]

Plus scripts/check-changeset-anchor.test.mjs — 8 assertions run as the first CI step, so a broken guard fails loudly rather than silently passing everything. The load-bearing one is replays the #348 bug; if that ever goes green the guard is decorative. Also covers path-segment awareness (dotnet/server/srcgen must not match the dotnet/server/src tree) and : in changeset prose not being mistaken for a package line.

End-to-end failure path exercised on a scratch commit: exit 1 with a message that names the trees, lists what the changesets did name, and calls out the …-server confusion by name.

Not path-filtered, on purpose: the guard needs the whole diff to know which trees a PR touched, and a path filter would make it skip exactly the PRs it exists to judge.

🤖 Generated with Claude Code

https://claude.ai/code/session_012iM1Q8JC1H83H2FXQVQNs9

…ree without naming the anchor

Every artifact ships at one shared version held in @smooai/smooth-operator
(typescript/package.json). Changesets versions only npm packages, so
sync-versions.mjs stamps that number onto every other published manifest. A
changeset that does not name the anchor therefore republishes nothing outside
npm — silently, with a green release.

That is how #348 (.NET file transfer) and #352 (.NET skill resolution) both
landed on main and sat unpublished: their changesets named
@smooai/smooth-operator-server, which is the TYPESCRIPT server package sitting
one word away from the anchor. A downstream consumer found it two days later by
reading the NuGet and filed a request to build what we had already shipped.

Rule: touching a stamped tree WITH a changeset requires one changeset naming the
anchor. Conditioned on having a changeset at all, so docs/test-only PRs stay
quiet — a guard that cries wolf gets ignored, and then it protects nothing.

sync-versions.mjs is now importable (stamping runs only when invoked directly)
so the guard derives the stamped trees from the very list that does the
stamping. A new target cannot be added without the guard learning about it.

Verified by replay against real history: the guard FIRES on #348's actual commit
and PASSES #346's (the TS sibling that correctly named both packages).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012iM1Q8JC1H83H2FXQVQNs9
@changeset-bot

changeset-bot Bot commented Aug 14, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 104ee2c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@brentrager
brentrager merged commit 7557a3e into main Aug 14, 2026
1 check passed
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