Skip to content

fix(release): three traps that report success while doing nothing - #82

Merged
brentrager merged 3 commits into
mainfrom
fix/release-traps
Aug 20, 2026
Merged

brentrager merged 3 commits into
mainfrom
fix/release-traps

Conversation

@brentrager

@brentrager brentrager commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

All three confirmed live in this repo, not theoretical. Flagged by the audit agent from a sibling repo; each reproduced here before fixing.

1. go test ./... served a cached pass against a corrupted fixture

Go's build cache doesn't invalidate on a file read from outside the package directory — exactly the shape of the shared contract fixtures in spec/. Reproduced:

$ python3 -c "...zero every sha256, set headBytes to 999..."
$ go test ./...
ok  github.com/SmooAI/file/go/file/v2   (cached)

So the Go quarter of the five-port lazy contract was proving nothing. go:test now passes -count=1, and the same corruption fails immediately (streamHeadBytes = 65536, contract says 999). Verified end to end through pnpm go:test: exit 1 corrupted, exit 0 restored.

The other four ports were never affected — vitest, pytest, cargo and dotnet all re-read the fixture. -count=1 went in the package.json script rather than the workflow, so pnpm test, pnpm check-all and the husky pre-commit all get it.

2. release.yml ran pnpm format in write mode

Whatever it rewrote was either swept into the release commit unreviewed, or — in publish mode, where the changesets action commits nothing — left the tree dirty for cargo publish --locked, which would fail every release now that --allow-dirty is gone (dropped in #66). #68 widened the blast radius by adding dotnet format to pnpm format.

Now format:check, with the write moved into pnpm run version, before the action commits.

Sequencing confirmed necessary rather than assumed: ran changeset version locally and checked its output — the generated CHANGELOG.md is not oxfmt-clean. Without formatting inside version, format:check would redden every future release PR. With it, pnpm run version leaves a format-clean tree.

3. Four registries gated on npm succeeding in the same run

if: steps.changesets.outputs.published == 'true' on PyPI, crates.io, the Go tag and NuGet. npm succeeds, a later step fails, the retry finds nothing new for npm → published is false → all four skip, the run goes green, and nothing was published.

Each step is now gated on whether its own registry carries package.json's version, so a retry ships exactly what's missing, plus a final step that fails the run on a real strand.

Two corrections found while verifying this, not after merging it

  • A fail-open in the fix itself. A botched edit left the npm-probe result being overwritten by a later branch, so --expect-npm exited 0 on a version npm didn't have. Since every gate is !has && present.npm, a false npm reading would have switched all four publishes off and gone green having shipped nothing — the exact defect being removed, reintroduced one level up. Now exits 1 naming the disagreement.
  • NuGet is reported but not asserted. Its index takes minutes to tens of minutes to show a package it has already accepted: 2.2.19 logged Your package was pushed for both packages at 19:26 and the flat-container index still read 2.2.14 twenty minutes later. Asserting on it would have reddened every successful release, and a guard that cries wolf gets deleted. NuGet keeps its own protection — dotnet nuget push exits non-zero on a real failure, and the per-registry gate skips it only when the version is genuinely there.

Controls, all three run

control expected result
all five present (2.2.14) exit 0 ✅
npm present, Go tag missing, NuGet lagging exit 1 naming go only ✅
--expect-npm on an unpublished version exit 1 ✅

Status check the lead asked for — not stranded

At the time of checking, npm / PyPI / crates.io / both NuGet packages / the Go tag were all on 2.2.14, so the truncation fix reached every port. 2.2.19 has since published to npm, PyPI, crates.io and the Go tag, with NuGet accepted and indexing. Two earlier release runs did fail, both at "a pull request already exists for changeset-release/main" — a concurrency race before any publish step, so nothing was half-published.

🤖 Generated with Claude Code

https://claude.ai/code/session_0152bbE1veqfG1SVJdyLCBxC

@changeset-bot

changeset-bot Bot commented Aug 20, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 18b8e9a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@smooai/file Patch

Not sure what this means? Click here to learn what changesets are.

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

brentrager and others added 3 commits August 20, 2026 15:40
All three confirmed live in this repo, not theoretical.

1. `go test ./...` served a CACHED pass against a corrupted fixture. Go's build
   cache does not invalidate on a file read from outside the package directory —
   exactly the shape of the shared contract fixtures in spec/. Verified by
   zeroing every sha256 and setting headBytes to 999: still `ok (cached)`. So
   the Go quarter of the five-port lazy contract was proving nothing. `go:test`
   now passes `-count=1`; the same corruption fails immediately.

2. release.yml ran `pnpm format` in WRITE mode. Whatever it rewrote was either
   swept into the release commit unreviewed, or — in publish mode, where the
   changesets action commits nothing — left the tree dirty for
   `cargo publish --locked`, which would fail every release now that
   `--allow-dirty` is gone (dropped in #66). The step is now `format:check`, and
   the formatting the release genuinely needs moved into `pnpm run version`,
   before the action commits. Confirmed that changesets' generated CHANGELOG.md
   is NOT oxfmt-clean, so without that ordering `format:check` would redden
   every future release PR.

3. PyPI, crates.io, NuGet and the Go tag were gated on
   `steps.changesets.outputs.published == 'true'` — on npm having published in
   THAT run. npm succeeds, a later step fails, the retry finds nothing new for
   npm, all four skip: a GREEN run that published nothing, leaving four ports on
   the old version indefinitely. Each is now gated on whether its own registry
   carries package.json's version, so a retry ships exactly what is missing, and
   a final step fails the run if npm published a version the others did not.

`scripts/check-registries.mjs` does the detection, waits out index propagation
(reading "missing" during the lag would skip the publish this run just earned,
then report success), and doubles as a manual "are we stranded?" command.
Verified both directions: all five present on 2.2.14 exits 0; npm present with
the Go tag missing exits 1 naming it.

Checked and NOT stranded today: npm, PyPI, crates.io, both NuGet packages and
the Go tag are all on 2.2.14, so the truncation fix reached every port.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0152bbE1veqfG1SVJdyLCBxC
…ying wolf on NuGet

Two corrections found while verifying the previous commit rather than assuming it.

A botched edit had left the npm-probe result being overwritten by a later branch,
so `--expect-npm` returned exit 0 on a version npm did not have. Every downstream
gate is `!has && present.npm`, so a false npm reading would have switched all four
publishes off and let the run go green having shipped nothing — the exact
fail-open this script exists to remove, reintroduced one level up. Now exits 1
with a message naming the disagreement. Verified: exit 1 on an unpublished
version, exit 0 once npm has it.

NuGet is no longer asserted on. Its index takes minutes to tens of minutes to
show a package it has already accepted — 2.2.19 logged "Your package was pushed"
for both packages and the flat-container index still read 2.2.14 twenty minutes
later — so the guard would have reddened every successful release, and a guard
that cries wolf gets deleted. npm, PyPI, crates.io and the Go tag index in
seconds and stay strict. NuGet keeps its own protection: `dotnet nuget push`
exits non-zero on a real failure, and the per-registry gate skips it only when
the version is genuinely already there. Its state is still reported, just not
enforced, and the reason is in the code so it does not read as an oversight.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0152bbE1veqfG1SVJdyLCBxC
The stranding guard's comment still said "any of the other four" after NuGet was
dropped from the strict set, and the PyPI step still called the wheels it cleans
"pre-sync version" — which stopped being true when sync-versions moved into the
`version` lifecycle. A comment that describes behaviour the code no longer has is
worse than none, because the next reader trusts it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0152bbE1veqfG1SVJdyLCBxC
@brentrager
brentrager merged commit d4cb3a3 into main Aug 20, 2026
1 check passed
@brentrager
brentrager deleted the fix/release-traps branch August 20, 2026 20:07
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