Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,10 @@ jobs:
publish:
needs: [seal, browser_e2e]
runs-on: ubuntu-latest
timeout-minutes: 15
# Read-back after publish is minutes per package on npm, so five packages
# do not fit in fifteen. The publisher is resumable, but a job killed
# mid-set is still a partial publication to reconcile by hand.
timeout-minutes: 45
environment: npm-production
permissions:
id-token: write
Expand Down Expand Up @@ -289,7 +292,7 @@ jobs:

const pinned = {
"config.mjs": "17f2f25fd40aea7d99024c0da5c7fc8b137dc272646fa1b8d47947bfb3735866",
"release-publisher.mjs": "3ddab82fbf3ef5479f823618b8d1a79a73a1862b297459120d3596a2534a3f3b",
"release-publisher.mjs": "e78adc75f57feec3b003b0f4929b55a69b6e873908f7850a4e875fd77315dcc7",
"release-line.json": "d650e2ac9707867db77b5f7b8fd2dd97f8fd0399f578a991091c73ee791dca73",
};
const expected = ["release-seal.json"];
Expand Down
22 changes: 16 additions & 6 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,20 @@ npm pkg set exports='./index.js'
npm publish --access public --tag bootstrap --otp='<current-2fa-code>'
```

Use a fresh temporary directory for each package. Confirm that only `bootstrap`
points to the inert version and that `latest` is absent:
Use a fresh temporary directory for each package. Confirm that `bootstrap`
points to the inert version:

```sh
npm view @full-self-browsing/concierge dist-tags --json
```

npm also points `latest` at that first version even though `--tag bootstrap`
was the only tag requested, so expect `latest` to be present rather than
absent. It is corrected when the real release publishes with `--tag latest`,
and the publisher's `[REGISTRY_TAG]` check refuses to finish until every
package's `latest` is the shared release version. A record whose only version
is the inert bootstrap is the state this step is meant to produce.

Do not publish any repository-built `0.1.0`, assign `latest`, or use an
automation token for bootstrap. Do not unpublish the inert version after
launch; registry history is immutable evidence.
Expand Down Expand Up @@ -125,12 +132,15 @@ done

The workflow field is the filename `release.yml`, not its full path. All names
are case-sensitive. `--allow-publish` is required by current npm trust
configuration and grants only the command used by this release workflow; do
not also grant `--allow-stage-publish`. npm currently permits one trusted
configuration; pass it and nothing else. npm currently permits one trusted
publisher per package.

Verify each relationship and its publish-only permission with
`npm trust list <package>`. In each package's npm settings, set publishing
Verify each relationship with `npm trust list <package>`. It reports
`permissions: publish, stage publish` even when only `--allow-publish` was
passed — npm grants both, and the extra grant cannot be declined from the CLI.
Treat that output as expected rather than as a misconfigured relationship. The
workflow only ever runs `npm publish`, and the sealed publisher is the only
tool the protected environment can reach. In each package's npm settings, set publishing
access to require 2FA and disallow traditional tokens. No `NPM_TOKEN` or write
token belongs in the repository or GitHub secrets. See npm's
[trusted-publisher requirements](https://docs.npmjs.com/trusted-publishers/)
Expand Down
26 changes: 23 additions & 3 deletions scripts/release/publisher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -516,9 +516,28 @@ function wait(milliseconds) {
return new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
}

/**
* Read a just-published version back until the registry serves it.
*
* **The window is minutes, because npm's is.** This waited 6 attempts at 1.5s
* — about nine seconds — and the 0.4.0 release measured roughly two and a half
* minutes between a successful `npm publish` and the version becoming readable.
* Every package published correctly and then failed its own verification, so a
* five-package set took six runs of this job instead of one, advancing one
* package per run. The check was not wrong to insist on reading the bytes back;
* it was wrong about how long that takes.
*
* Nothing here relaxes what is verified. A version that never appears, or that
* appears with foreign bytes, provenance, or tag, still raises
* `[PUBLISH_AMBIGUOUS]` and stops the set. Waiting longer only stops the
* publisher from reporting a lagging read replica as an ambiguous publish.
*/
const VERIFY_ATTEMPTS = 60;
const VERIFY_INTERVAL_MS = 5_000;

async function validateAfterPublish(archive, registry, inputs) {
let lastError = null;
for (let attempt = 0; attempt < 6; attempt += 1) {
for (let attempt = 0; attempt < VERIFY_ATTEMPTS; attempt += 1) {
try {
const state = registry.query(archive);
if (state.kind === "present") {
Expand All @@ -528,10 +547,11 @@ async function validateAfterPublish(archive, registry, inputs) {
} catch (error) {
lastError = error;
}
if (attempt < 5) await wait(1_500);
if (attempt < VERIFY_ATTEMPTS - 1) await wait(VERIFY_INTERVAL_MS);
}
const waited = Math.round((VERIFY_ATTEMPTS * VERIFY_INTERVAL_MS) / 1000);
throw new Error(
`[PUBLISH_AMBIGUOUS] ${archive.name}@${archive.version} could not be verified after publish: ${String(lastError ?? "not visible")}`,
`[PUBLISH_AMBIGUOUS] ${archive.name}@${archive.version} could not be verified ${waited}s after publish: ${String(lastError ?? "not visible")}`,
);
}

Expand Down
Loading