ci: set up preview publishing (#1084)

Today nothing reaches npm between two release-please releases, so trying
a merged
but unreleased change means building from source. This adds a
continuously
published preview channel.

Every CI-green push to `main` that is not a release merge publishes
`<package.json version, patch bumped>-preview.N` under the `preview`
dist-tag,
tags the commit `v<version>`, and dispatches the same agent registry
update a
stable release does. With `main` at 0.73.0 that gives
`0.73.1-preview.1`,
`0.73.1-preview.2`, and so on; `N` restarts at 1 whenever a release
moves the
base. `latest` is untouched.

## Approach

- **Added to `publish.yml` rather than a new workflow.** npm binds a
trusted
publisher to one repo + workflow filename, and a package gets only one.
A
  separate file would fail OIDC and cost the stable path its publisher.
- **Gated on the `CI` workflow succeeding** (`workflow_run`), so a red
commit is
never published. release-please's release merge is excluded by the head
commit's author and subject — its `releases_created` output is not
reachable
  from a different workflow run.
- **`N` is the max of the npm registry and local git tags.** npm
reserves
versions forever, even after `unpublish`, so a reused `N` is a hard
failure;
but the registry is CDN-served and lags, which the tags cover. A
registry read
  failure aborts rather than guessing.
- **Base is patch+1, not release-please's prediction**, which moves
mid-flight
  and would reset `N` under already-published previews.
- **The version is applied to the CI working tree only**, never
committed —
`package.json` and the release-please manifest stay release-please's to
own.
- **`--tag preview` is mandatory:** `npm publish` defaults to `latest`
even for a
  semver prerelease.
- **Three jobs.** `publish-npm-preview` mirrors `publish-npm` and only
publishes
(no repo write access); `publish-tag-preview` only tags, so a failed tag
is
retryable with _Re-run failed jobs_ without re-publishing an immutable
version;
`trigger-registry-update` is shared with the stable path. Both
downstream jobs
gate on a `published` output the publish step sets, not on job status —
the tag
push happens after the publish, so a red job can still mean npm has the
  version.

## Next step

The publish is `npm publish --dry-run` for now. The `workflow_run`
trigger and
the `release` environment's protected-branch policy mean this job cannot
run off
the default branch at all, so merging this is the only way to exercise
the
trigger, the gate and the version calculation — and with `published`
unset, the
rehearsal neither tags nor touches the registry.

Once one run looks right, a follow-up drops `--dry-run` and adds
`echo "published=true" >> "$GITHUB_OUTPUT"` to that step. Nothing else
changes.
This commit is contained in:
Andrey Bragin
2026-09-08 14:16:37 +02:00
committed by GitHub
parent d870ebd2fd
commit 0dd58685b1
8 changed files with 768 additions and 8 deletions

View File

@@ -28,7 +28,7 @@ patch, and `chore:`/`ci:`/`test:` and friends do not release at all.
## Releasing
Releases are fully automated by release-please. There is no manual release
Stable releases are fully automated by release-please. There is no manual release
workflow, and the version is never chosen by hand — it follows from the commit
history.
@@ -38,5 +38,11 @@ gh pr merge <pr-number> --squash
```
The preflight is the guard-list as code; if it exits non-zero, follow what it
prints rather than merging. Full runbook, including how to recover a stalled
release: [`docs/RELEASES.md`](docs/RELEASES.md).
prints rather than merging.
Every _other_ push to `main` publishes a preview to npm — `0.73.1-preview.4` and
so on — under the `preview` dist-tag, tags the commit it came from, and updates
the agent registry the same way a release does. Only `latest` is reserved for
real releases. So anything merged to `main` is published within minutes; there is
no staging branch. Full runbook, including how to recover a stalled release:
[`docs/RELEASES.md`](docs/RELEASES.md).