The workflow passes a relative release path: --release "$(find package-train -mindepth 1 -maxdepth 1 -type d)", which expands to package-train/ekklesia-packages-0.2.0.
Validation passed. The tool validated the artifacts relative to the job's working directory, where they exist.
npm then looked in the wrong place. npm runs in the tool's private temporary home, and was given the same relative path:
Why tests missed it. The disposable-registry suite always made the release path absolute first, so only the real workflow passed a relative one.
Reproduced locally. The same failure appears from the same commit, with the Actions layout, the workflow's exact command, and an npm shim recording argv and cwd.
Fix
Absolute paths from validation onwards.loadReleaseTrain resolves the release directory once.
Guard at the npm boundary. npm is only ever given the artifact's absolute path; anything else is refused.
Re-check the file before upload. Just before each upload, the file is checked against the validated bytes again. It's the same file, not copied, repacked or rebuilt.
Diagnostics:
the tool logs @ekklesia/<name>@<version>: publishing <absolute path> (sha256 …);
a failure names that path and includes npm's code/path/syscall lines;
npm's temporary home is masked, and the token is never shown.
After the fix, the reproduction shows npm receiving /…/package-train/ekklesia-packages-0.2.0/ekklesia-contracts-0.2.0.tgz. Its SHA-256 (349e2cf2…) equals SHA256SUMS, and npm reads it and gets as far as the upload.
Tests
New unit tests use a recording fake npm. All three fail on the previous tool and pass now:
a release nested under package-train/ and named by a relative path from another directory loads with absolute artifact paths;
npm's argv is exactly publish <absolute validated path>, the file exists, npm runs in its own directory, and the error names the path without the token;
an artifact changed after validation is refused before npm runs.
New disposable-Gitea test: the CLI runs exactly as the workflow does (cwd holding package-train/<release>, relative --release, --manifest-sha256, --record, --require-public-read). All nine packages publish and round-trip byte for byte. The suite passes 11 of 11, and the partial-train, burned-version and tag-retry scenarios are unchanged.
Full gates:
pnpm verify passes (2,588 tests);
pnpm test:package-release passes strict, and the 0.2.0 tarballs are byte-identical to the release's (contracts349e2cf2…, module-pack2da58cf7…).
Real registry untouched
An anonymous search returns {"objects":[],"total":0}, and every @ekklesia/* lookup is 404.
No tags beyond v0.1.2.
Nothing was published, and the Publish packages workflow wasn't run.
Fixes the first real 0.2.0 **Publish packages** run (run 219, job `publish`), which failed on its first package before anything was published:
```
Package train 0.2.0 (debb8df…) validated: @ekklesia/contracts, …
Publishing ekklesia-contracts-0.2.0.tgz failed: npm error code ENOENT
```
## Root cause
The workflow passes a **relative** release path: `--release "$(find package-train -mindepth 1 -maxdepth 1 -type d)"`, which expands to `package-train/ekklesia-packages-0.2.0`.
- **Validation passed.** The tool validated the artifacts relative to the job's working directory, where they exist.
- **npm then looked in the wrong place.** npm runs in the tool's private temporary home, and was given the same relative path:
| | Value |
|---|---|
| npm's cwd | `/tmp/ekklesia-publish-XXXXXX` |
| argument | `package-train/ekklesia-packages-0.2.0/ekklesia-contracts-0.2.0.tgz` |
| resolved to | `/tmp/ekklesia-publish-XXXXXX/package-train/…`, which doesn't exist: **ENOENT** |
| the file is really at | `<workspace>/package-train/ekklesia-packages-0.2.0/ekklesia-contracts-0.2.0.tgz` |
- **Why tests missed it.** The disposable-registry suite always made the release path absolute first, so only the real workflow passed a relative one.
- **Reproduced locally.** The same failure appears from the same commit, with the Actions layout, the workflow's exact command, and an npm shim recording argv and cwd.
## Fix
- **Absolute paths from validation onwards.** `loadReleaseTrain` resolves the release directory once.
- **Guard at the npm boundary.** npm is only ever given the artifact's **absolute** path; anything else is refused.
- **Re-check the file before upload.** Just before each upload, the file is checked against the validated bytes again. It's the same file, not copied, repacked or rebuilt.
- **Diagnostics:**
- the tool logs `@ekklesia/<name>@<version>: publishing <absolute path> (sha256 …)`;
- a failure names that path and includes npm's `code`/`path`/`syscall` lines;
- npm's temporary home is masked, and the token is never shown.
After the fix, the reproduction shows npm receiving `/…/package-train/ekklesia-packages-0.2.0/ekklesia-contracts-0.2.0.tgz`. Its SHA-256 (`349e2cf2…`) equals `SHA256SUMS`, and npm reads it and gets as far as the upload.
## Tests
- **New unit tests** use a recording fake npm. All three fail on the previous tool and pass now:
- a release nested under `package-train/` and named by a relative path from another directory loads with absolute artifact paths;
- npm's argv is exactly `publish <absolute validated path>`, the file exists, npm runs in its own directory, and the error names the path without the token;
- an artifact changed after validation is refused before npm runs.
- **New disposable-Gitea test:** the CLI runs exactly as the workflow does (cwd holding `package-train/<release>`, relative `--release`, `--manifest-sha256`, `--record`, `--require-public-read`). All nine packages publish and round-trip byte for byte. The suite passes 11 of 11, and the partial-train, burned-version and tag-retry scenarios are unchanged.
- **Full gates:**
- `pnpm verify` passes (2,588 tests);
- `pnpm test:package-release` passes strict, and the 0.2.0 tarballs are byte-identical to the release's (`contracts` `349e2cf2…`, `module-pack` `2da58cf7…`).
## Real registry untouched
- An anonymous search returns `{"objects":[],"total":0}`, and every `@ekklesia/*` lookup is 404.
- No tags beyond `v0.1.2`.
- Nothing was published, and the Publish packages workflow wasn't run.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The first real 0.2.0 publish failed on its first package with ENOENT:
"Publishing ekklesia-contracts-0.2.0.tgz failed: npm error code ENOENT".
The publish workflow passes a relative release path,
--release "$(find package-train …)", which is
package-train/ekklesia-packages-0.2.0. The tool validated the artifacts
relative to the job's working directory, but ran npm in its own
temporary home (cwd /tmp/ekklesia-publish-XXXXXX) with the same relative
path, package-train/ekklesia-packages-0.2.0/ekklesia-contracts-0.2.0.tgz,
which there names nothing. The disposable-registry suite always passed
an absolute path, so it never saw this.
- loadReleaseTrain resolves the release directory once; every artifact
path is absolute from validation on, and npm is given only that exact
file's absolute path (anything else is refused at the npm boundary).
- Just before each upload the file is checked against the validated
bytes once more; the tool logs the absolute path it publishes, and a
failure names it with npm's path/syscall lines (npm's temporary home
masked, the token never shown).
- Regression tests: unit tests with a recording npm (relative nested
release from another cwd; npm's argv, cwd, and the file's existence; a
changed artifact refused before npm runs), which fail on the previous
tool; and a disposable-Gitea test that runs the CLI exactly as the
workflow does, from the Actions artifact layout, round-tripping all
nine packages byte for byte.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes the first real 0.2.0 Publish packages run (run 219, job
publish), which failed on its first package before anything was published:Root cause
The workflow passes a relative release path:
--release "$(find package-train -mindepth 1 -maxdepth 1 -type d)", which expands topackage-train/ekklesia-packages-0.2.0./tmp/ekklesia-publish-XXXXXXpackage-train/ekklesia-packages-0.2.0/ekklesia-contracts-0.2.0.tgz/tmp/ekklesia-publish-XXXXXX/package-train/…, which doesn't exist: ENOENT<workspace>/package-train/ekklesia-packages-0.2.0/ekklesia-contracts-0.2.0.tgzFix
loadReleaseTrainresolves the release directory once.@ekklesia/<name>@<version>: publishing <absolute path> (sha256 …);code/path/syscalllines;After the fix, the reproduction shows npm receiving
/…/package-train/ekklesia-packages-0.2.0/ekklesia-contracts-0.2.0.tgz. Its SHA-256 (349e2cf2…) equalsSHA256SUMS, and npm reads it and gets as far as the upload.Tests
package-train/and named by a relative path from another directory loads with absolute artifact paths;publish <absolute validated path>, the file exists, npm runs in its own directory, and the error names the path without the token;package-train/<release>, relative--release,--manifest-sha256,--record,--require-public-read). All nine packages publish and round-trip byte for byte. The suite passes 11 of 11, and the partial-train, burned-version and tag-retry scenarios are unchanged.pnpm verifypasses (2,588 tests);pnpm test:package-releasepasses strict, and the 0.2.0 tarballs are byte-identical to the release's (contracts349e2cf2…,module-pack2da58cf7…).Real registry untouched
{"objects":[],"total":0}, and every@ekklesia/*lookup is 404.v0.1.2.🤖 Generated with Claude Code