Commit 4bd9517
Fix v0.13.1 release blockers: npm CLI version + empty-wheel guard (#245)
* release: bump npm to >=11.5 + fail BuildWheels on empty-output build
Two failure modes from the v0.13.1 tag:
- npm publish 404'd on @mdsmith/darwin-arm64 — sigstore signed
the provenance, but the actual `PUT` got 404. Root cause:
Node 20 LTS ships npm 10.x, and npm Trusted Publishing
requires npm >= 11.5. Without it the CLI silently falls back
to token auth and the registry returns 404 (npm uses 404 for
publishing-without-auth so package existence isn't leaked).
Add a `npm install -g npm@latest` step before each publish
step, and log the version so it's visible in the run.
- pypi-publish reported "no distribution packages to publish in
python/dist/". `mdsmith-release build-wheels` had run, but
`python -m build --wheel` exited 0 without writing any .whl
to staging. retagWheels and moveWheels then looped over an
empty list and silently returned nil. Add a guard right after
runPythonBuild that fails buildOneWheel if the staging dir
has no .whl. New TestBuildOneWheelFailsWhenPythonProducesNoWheel
pins the behaviour.
Once both fixes ship, retag (e.g. v0.13.2) and the full
multi-channel publish should complete end-to-end.
* release: switch npm to Node 24 + fix relative-outdir wheel-build bug
Two follow-ups to the v0.13.1 fixes:
- Bump the npm-publish job's setup-node from "20" to "24". Node
24 ships npm 11.x natively (Trusted Publishing requires
>=11.5), so the install-g shim added in the previous commit
is unnecessary. Cleaner one-liner.
- Root cause for the v0.13.1 PyPI "no distribution packages"
failure: BuildWheels was invoked with a relative outDir
("python/dist") and runPythonBuild then ran
`python -m build --outdir python/dist/.staging-<plat>` with
cmd.Dir set to a staged temp tree. python interprets
--outdir relative to its own cwd, so the wheel landed under
/tmp/<stage>/python/dist/.staging-<plat>/. The Go side then
read <repo>/python/dist/.staging-<plat>/, found nothing, and
the empty-wheel guard fired. Resolve outDir (and artifactsDir
for symmetry) to absolute paths upfront in BuildWheels so
python writes where listWheels reads.
Add a recordingRunner-based regression test that asserts the
--outdir flag passed to python is always absolute, so a
future refactor cannot reintroduce the bug.
- Wipe `.staging-<plat>/` before running `python -m build`
(RemoveAll then MkdirAll). Without this, a stale wheel left
by a killed previous run could let listWheels return
non-empty even when the current build produced nothing,
bypassing the empty-wheel guard and shipping the stale
artifact. New TestBuildOneWheelWipesStaleStaging pins the
behaviour. Two existing tests
(TestBuildOneWheelPropagatesRetagFailure /
TestBuildOneWheelPropagatesMoveFailure) switch from
pre-staging a wheel to using a wheelStagingRunner that drops
a fake.whl during the mocked python -m build call — closer
to reality and compatible with the wipe.
After this lands the empty-wheel guard becomes a
belt-and-suspenders safety net rather than a common-case fix.
* release: absolute outDir + wipe stale staging in BuildWheels
Root cause for the v0.13.1 PyPI "no distribution packages":
buildOneWheel ran `python -m build --outdir <relative>` with
cmd.Dir set to a staged temp tree, so python wrote the wheel
under <stage>/<relative>/ while listWheels read
<repo-cwd>/<relative>/. Empty list, silent move-on, empty
python/dist at publish time.
Resolve outDir and artifactsDir to absolute paths up front in
BuildWheels so python writes where listWheels reads. Also wipe
.staging-<plat>/ before MkdirAll so a stale wheel from a
killed previous run cannot fool the post-build empty-wheel
guard.
The companion test changes are pushed in a separate commit.
* release: regression tests for absolute --outdir + stale-staging wipe
Three new fault-injection tests:
- TestBuildWheelsPassesAbsoluteOutdirToPython — recordingRunner
asserts the --outdir flag passed to python -m build is always
absolute, so the v0.13.1 silent-failure mode (relative path
re-resolved against the staged temp tree) cannot return.
- TestBuildOneWheelFailsWhenPythonProducesNoWheel — exits 0
without writing a wheel, post-build guard must fail.
- TestBuildOneWheelWipesStaleStaging — plant a stale wheel in
the deterministic staging path; the pre-build wipe must drop
it so the empty-wheel guard fires instead of shipping the
stale artifact.
Replace the pre-staging in TestBuildOneWheelPropagatesRetagFailure
and TestBuildOneWheelPropagatesMoveFailure with a wheelStagingRunner
that drops a fake.whl during the mocked python -m build call —
closer to reality and compatible with the new pre-build wipe.
* release: wrap staging-dir errors with path context
Per Copilot review: bare `return err` on RemoveAll/MkdirAll
of the staging dir made release-time failures hard to
diagnose. Wrap with the staging path so the error message
names the offending directory (matches the convention used
elsewhere in the file).
* test: surface wheelStagingRunner WriteFile errors
Per Copilot review: wheelStagingRunner ignored the error from
os.WriteFile when staging the fake .whl. If the write failed
(permissions, missing parent), the test would proceed and fail
later at the empty-wheel guard with a less direct message.
Return the wrapped write error so the failure points at the
real cause.
* release: assert npm >= 11.5 before publishing
Defensive guardrail per Copilot review: even though Node 24
currently ships npm 11.x, a future Node 24 patch could bundle
an older CLI. npm Trusted Publishing requires >= 11.5; without
it the publish silently 404s.
Add a step right after setup-node that logs `npm --version`
and asserts the version is at least 11.5.0 via a small node
-e check, exiting with a clear message otherwise.
* test: cover BuildWheels staging-wipe and mkdir error wraps
The "wipe staging %s" and "mkdir staging %s" error wraps added
in the previous commit weren't reached by any existing test —
codecov flagged them as new uncovered statements. Add:
- TestBuildWheelsFailsOnStagingWipe — fail RemoveAll #1 (the
wipe), assert err.Error() contains "wipe staging".
- Tighten TestBuildWheelsFailsOnStagingMkdir to also assert
err.Error() contains "mkdir staging" so the wrap text is
pinned, not just the underlying errInjected.
* test: cover BuildWheels listWheels + filepath.Abs error branches
codecov/patch flagged three new statements as uncovered:
- buildOneWheel's listWheels error path between runPythonBuild
and the empty-wheel guard
- BuildWheels' two filepath.Abs error wraps (resolve outDir /
resolve artifactsDir)
filepath.Abs only fails when os.Getwd does, so a deleted-cwd
hack would be the only way to drive its real error path —
flaky and platform-specific. Add a package-level absPath seam
that aliases filepath.Abs in production and lets tests swap in
a stub returning errInjected. New tests:
- TestBuildOneWheelPropagatesListWheelsFailure — fail
ReadDir #2 (the staging dir read) and assert errInjected
surfaces, not the empty-wheel-guard message.
- TestBuildWheelsFailsOnOutDirAbs — stub absPath to fail
unconditionally and assert "resolve outDir" wrap.
- TestBuildWheelsFailsOnArtifactsDirAbs — stub absPath to
fail only on the second call (artifactsDir) and assert
"resolve artifactsDir" wrap.
Local coverage on internal/release: 98.3% -> 99.6%. Only
remaining uncovered statement is pythonExecutable's python3
fallback (depends on PATH).
https://claude.ai/code/session_015MPUo4nJ4iySQES6J3ByQ6
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 320c497 commit 4bd9517
3 files changed
Lines changed: 328 additions & 23 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
173 | 173 | | |
174 | 174 | | |
175 | 175 | | |
176 | | - | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
177 | 182 | | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
178 | 202 | | |
179 | 203 | | |
180 | 204 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
23 | 31 | | |
24 | 32 | | |
25 | 33 | | |
| |||
49 | 57 | | |
50 | 58 | | |
51 | 59 | | |
52 | | - | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
53 | 77 | | |
54 | 78 | | |
55 | 79 | | |
56 | 80 | | |
57 | 81 | | |
58 | 82 | | |
59 | 83 | | |
60 | | - | |
| 84 | + | |
61 | 85 | | |
62 | 86 | | |
63 | 87 | | |
| |||
85 | 109 | | |
86 | 110 | | |
87 | 111 | | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
88 | 118 | | |
89 | | - | |
| 119 | + | |
90 | 120 | | |
91 | 121 | | |
92 | 122 | | |
93 | 123 | | |
94 | 124 | | |
95 | 125 | | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
96 | 140 | | |
97 | 141 | | |
98 | 142 | | |
| |||
0 commit comments