mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
docs(ci): describe the two release pipelines
Claude-Session: https://claude.ai/code/session_01ShuAy8q8ZfPi8KHxwc8JpQ
This commit is contained in:
1 parent
f7465eed16
commit
29ba1d34be
1 file changed
+56
-35
+56
-35
@@ -253,50 +253,71 @@ window looked like and not only that the conviction happened.
|
||||
|
||||
## Releases
|
||||
|
||||
Every merge to master cuts a release. `release.yml` watches ci with a
|
||||
`workflow_run` trigger, and when a push to master finishes green it advances the
|
||||
patch: `0.1.3` becomes `0.1.4`. That publishes `@sanderling/spec` to npm and a
|
||||
GitHub Release carrying the CLI binaries, both from the commit ci went green on
|
||||
rather than from whatever master has drifted to since.
|
||||
Two pipelines, and they are independent of each other.
|
||||
|
||||
For a minor or major release, Actions -> release -> Run workflow. The `bump`
|
||||
dropdown picks which part of `MAJOR.MINOR.PATCH` to advance, and `version`
|
||||
overrides it with a version named outright, which is how a pre-release like
|
||||
`1.0.0-rc1` gets cut. A pre-release publishes under npm's `next` tag so
|
||||
`npm install @sanderling/spec` keeps resolving the latest stable.
|
||||
**Every merge to master cuts a patch.** The `Release` jobs sit in `ci.yml`
|
||||
alongside everything else, waiting on `Checks`, `Folio` and `Replay UI`, so
|
||||
nothing reaches a registry that the emulators and the simulator have not agreed
|
||||
on. `0.1.4` becomes `0.1.5`: published to npm, and to GitHub Releases with the
|
||||
CLI binaries. It releases the commit that triggered the run rather than master's
|
||||
head, because master can move in the hour the device legs take.
|
||||
|
||||
**The tags are the version.** Nothing in the tree holds it: `pkg/spec/package.json`
|
||||
stays at `0.0.0-dev` and CI stamps the real version in before it publishes. So
|
||||
there is no version-bump commit to land on master, nothing to conflict on, and
|
||||
no second record to hold in step with the tags. `.github/scripts/next-version.sh`
|
||||
is the whole rule, and it counts off stable tags only, because `v0.0.1-rc4` is a
|
||||
candidate for `0.0.1` and a patch counted off it would skip the version it was a
|
||||
candidate for. Run it anywhere to see what the next release would be:
|
||||
**`release.yml` promotes that to a milestone.** Actions -> release -> Run
|
||||
workflow, pick `minor` or `major`, and the version you have been running as
|
||||
`0.1.6` is republished as `0.2.0`. The `version` box overrides the dropdown with
|
||||
a version named outright, which is how a pre-release like `1.0.0-rc1` gets cut;
|
||||
a pre-release publishes under npm's `next` tag so `npm install @sanderling/spec`
|
||||
keeps resolving the latest stable.
|
||||
|
||||
It runs no checks and needs none. The commit it releases is the one the last
|
||||
release was cut from, and that commit only carries a tag because a whole ci run
|
||||
went green on it. Re-running the device legs to republish bytes that already
|
||||
passed them would prove nothing. Afterwards the patch line continues from the
|
||||
milestone: the next merge counts off `0.2.0` and cuts `0.2.1`.
|
||||
|
||||
Both call `release-publish.yml`, which is where the tagging, the npm publish and
|
||||
GoReleaser actually live. Two copies of a publish drift, and the drift only
|
||||
shows up on a release.
|
||||
|
||||
**The tags are the version.** Nothing in the tree holds it:
|
||||
`pkg/spec/package.json` stays at `0.0.0-dev` and CI stamps the real version in
|
||||
before it publishes. So there is no version-bump commit to land on master,
|
||||
nothing to conflict on, and no second record to hold in step with the tags.
|
||||
`.github/scripts/next-version.sh` is the whole rule, and it counts off stable
|
||||
tags only, because `v0.0.1-rc4` is a candidate for `0.0.1` and a patch counted
|
||||
off it would skip the version it was a candidate for. Run it anywhere to see
|
||||
what the next release would be:
|
||||
|
||||
```
|
||||
BUMP=minor .github/scripts/next-version.sh
|
||||
```
|
||||
|
||||
The tag is pushed before anything is published, because npm is the half of a
|
||||
release that cannot be taken back and a tag is the half that can.
|
||||
release that cannot be taken back and a tag is the half that can. Both pipelines
|
||||
resolve their version under one `release-tag` concurrency group, so a promotion
|
||||
and a merge can never count off the same tag at once.
|
||||
|
||||
### Why the release is its own workflow
|
||||
### The npm credential
|
||||
|
||||
npm authenticates the publish over OIDC, against a trusted publisher configured
|
||||
for `@sanderling/spec` on npmjs.com. There is no token in the repo and none to
|
||||
expire, which is what took the pipeline down in August 2026: the `NPM_TOKEN`
|
||||
secret behind the old tag-driven release had expired, and npm answers an
|
||||
unauthorised publish with a 404 that reads as "no such package".
|
||||
`NPM_TOKEN` is a classic automation token. Those do not expire, which is the
|
||||
whole point: what took the pipeline down in August 2026 was a *granular* token,
|
||||
and granular tokens default to a 30-day life. npm answers a publish it will not
|
||||
authorise with `404`, so the failure read as "package does not exist" while
|
||||
`@sanderling/spec` was sitting in the registry the whole time.
|
||||
|
||||
npm matches that trusted publisher against the filename of the workflow that
|
||||
*starts* the run, and a package carries only one. So the publish has to live in
|
||||
one entry-point workflow: a job inside ci.yml would present `ci.yml`, and a
|
||||
reusable workflow called from ci.yml would present the caller's name too. That
|
||||
is why the merge path arrives at `release.yml` as a `workflow_run` rather than
|
||||
as a job at the end of ci.
|
||||
Trusted publishing over OIDC would remove the token, and it is the better
|
||||
mechanism, but it cannot express this shape: a package carries exactly one
|
||||
trusted publisher, matched against the filename of the workflow that *starts*
|
||||
the run, and there are two workflows here that publish. A reusable workflow does
|
||||
not help, because npm sees the caller's name. Collapsing the two pipelines into
|
||||
one is the price of OIDC, and it is not worth paying.
|
||||
|
||||
Setting that up again, or moving the package, means npmjs.com -> the package ->
|
||||
Settings -> Trusted publisher: repository `priyanshujain/sanderling`, workflow
|
||||
`release.yml`. It needs npm 11.5.1 or newer, which the job installs, because
|
||||
`actions/setup-node` writes an empty `_authToken` line into `.npmrc` and an
|
||||
older npm reads that as "auth is configured" and never asks for an OIDC token.
|
||||
### A promoted release has thin release notes
|
||||
|
||||
GoReleaser builds its changelog from the commits between the previous tag and
|
||||
this one. A promotion tags a commit that is already tagged, so `v0.2.0` and
|
||||
`v0.1.6` sit on the same commit and there is nothing between them to list. The
|
||||
binaries and the npm tarball are correct; only the generated notes are empty.
|
||||
Setting `GORELEASER_PREVIOUS_TAG` to the previous milestone would make the notes
|
||||
span the patches being consolidated, and is the obvious thing to add if those
|
||||
notes start mattering.
|
||||
Reference in new issue
Block a user