fix(release): make publication retry, space out, and skip what landed

A landlock publication failed with `E409 Failed to save packument` on the
second of three packages. The registry answers a write it could not commit that
way, and publishing several packages back to back is what provokes it.

Neither publish path could recover. The native sequence published from a shell
loop of bare `npm publish` calls: no retry, and no way to resume, because the
registry rejects a repeat of an existing version permanently — so a failure
partway through left the release stuck. publish.ts skipped versions already
present, which made a re-run safe, but had no retry either.

Both paths now attempt a tarball up to four times, space writes at least two
seconds apart, and back off 2s/4s/8s between attempts. Every retry re-reads the
registry first, because a reported failure can answer a write that landed
anyway: a version that now exists with this tarball's integrity counts as
published rather than as one to place again. That same re-read is what turns a
mid-run `E403 cannot publish over the previously published versions` into a
skip when the bytes match, and leaves it a hard failure when they do not.

The native sequence gets the registry comparison publish.ts already had, through
its own script rather than shared code — the two sequences keep separate
publication paths. Its publish job now checks out the repository, which the
shell loop did not need.

Verified against a scripted registry: a clean publish, one E409 then success, an
E409 whose write landed anyway, E409 on every attempt (fails after four), and a
version already present with matching integrity (publishes nothing).
This commit is contained in:
imccyu
2026-08-13 15:18:03 +08:00
parent fc4405723f
commit 8d6f5164ab
6 changed files with 263 additions and 21 deletions

View File

@@ -147,6 +147,12 @@ jobs:
contents: read
id-token: write
steps:
# The publish script is the only repository file this job needs, and it
# imports nothing outside Node's builtins, so there is no install step.
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 24
@@ -167,12 +173,8 @@ jobs:
fi
- name: Publish tarballs
run: |
version="${GITHUB_REF#refs/tags/landlock-run-v}"
tag_args=()
case "$version" in *-*) tag_args=(--tag next);; esac
while IFS= read -r tarball; do
# No --access: publishConfig.access in each manifest decides, and a
# command-line flag would override it.
npm publish "dist/npm/${tarball}" "${tag_args[@]}"
done < dist/npm/publish-order.txt
# Publication is decided per package against the registry, so re-running
# this job over the same artifact skips what already landed instead of
# failing on it. A bare `npm publish` loop could not be retried: the
# registry answers a repeat of an existing version permanently.
run: node ./scripts/publish-release.mjs dist/npm