Releasing¶
Branches¶
| Branch | Is |
|---|---|
main |
the release branch |
develop |
integration; feature branches land here |
feature/<name> |
feature work, branched from develop |
Tags¶
| Tag | Means | npm dist-tag |
|---|---|---|
v<version>-dev.<n> |
a snapshot, tagged on any branch | dev |
v<version>-rc.<n> |
a release candidate, tagged on develop |
next |
v<version> |
a final release, tagged on main only |
latest |
Only a final tag moves latest, so npm install ailoud never returns a
pre-release -- except for a package's very first publish, where npm sets
latest whatever --tag says. latest can be moved but not removed, so that
first pre-release answers npm install ailoud until a final version exists.
Changelog limits¶
CHANGES.md has a soft limit of 10 entries per version and a hard limit of 50.
scripts/release-notes.mjs warns past the first and refuses past the second.
Entries describe what a user can now do, or what changed under them. Anything fixed before it shipped is left out entirely -- if no released version had the bug, there is nothing to say. See AGENTS.md.
Steps¶
-
Run the
pre-release-checkskill. It runs the whole gate plus the documentation, dependency, changelog and version checks. -
Run the
bump-versionskill. It sets the version across everypackage.jsonand promotes the CHANGES.md Development section. It does not tag or push. -
Fold the pre-release sections in.
bump-versioncomes first:
node scripts/fold-prereleases.mjs 1.2.3
node scripts/check-changelog.mjs v1.2.3
Folding first would leave bump-version an empty Development section to
promote, giving a second ## Version 1.2.3 heading that fails the check.
To read the notes as they will appear, node scripts/release-notes.mjs
v1.2.3 writes them to RELEASE_NOTES.md. The release itself does not need
this -- publish.yml runs the same script.
- Commit, tag and push:
git commit -am "chore: 1.2.3"
git tag -s v1.2.3 -m "v1.2.3"
git push origin main
git push origin v1.2.3
The tag is what starts everything else: publish.yml publishes the three
packages, creates the GitHub release from CHANGES.md, and retires the
superseded snapshots; docs.yml then publishes the site.
Publishing to npm¶
Pushing a final tag also runs
.github/workflows/publish.yml,
which publishes @ailoud/core, @ailoud/providers and ailoud in that order.
No npm token is involved. The workflow uses npm's trusted publishing: GitHub mints a short-lived OIDC token for the run, npm exchanges it for a credential good for minutes, and provenance is attached automatically. Nothing long-lived is stored, so there is no 90-day expiry to renew.
Except once, per package. A trusted publisher is attached to a package on
npmjs.com, and there is no page to attach it to until the package exists, so
the first version of each has to go out on a token in the NPM_TOKEN secret --
npm answers ENEEDAUTH without one however complete the OIDC setup is. The
workflow uses the secret when it is present and OIDC when it is not, so
deleting the secret is the whole of the switch.
It will not let that drift: a pre-release published on the token logs a
warning, and a final release with the secret still set fails before
publishing anything. Attaching the publisher (organization lorem-dev,
repository ailoud, workflow publish.yml, environment empty) on all three
package pages and deleting the secret clears it.
One-time setup on npmjs.com, per package -- Package, then Settings, then Trusted publisher, then GitHub Actions:
| Field | Value |
|---|---|
| Organization or user | lorem-dev |
| Repository | ailoud |
| Workflow filename | publish.yml |
| Environment | leave empty |
The workflow packs with pnpm and publishes with npm, because each tool has
half of what is needed: pnpm pack rewrites the workspace:* dependencies
into real versions, which npm requires and will not do itself, and
npm publish is the one with OIDC and provenance.
Before publishing, and before anything is built, it refuses a release that
should not happen: a changelog unfit to release, an open high or critical code
scanning alert, a manifest that disagrees with the tag, or the NPM_TOKEN
secret still set on a final tag. Then it runs the whole gate.
A code scanning finding blocks only while it is open. One that has been reviewed is dismissed with its reason and does not block.
Retiring pre-releases¶
After a final release, retire the snapshots it supersedes:
node scripts/retire-prereleases.mjs 1.0.0 # prints the plan
node scripts/retire-prereleases.mjs 1.0.0 --yes # carries it out
Deprecating, not unpublishing: a deprecated version keeps every pinned install
working and prints a notice on the next one. It also drops the dev dist-tag,
and deletes the tags -- but only those whose commit is reachable from main,
because the published provenance attests that commit. The rest are reported and
left in place.
This is a manual step, run under npm login. Automating it was tried and does
not work: trusted publishing authenticates npm publish and nothing else. The
OIDC exchange succeeds, but the token it returns is refused by npm deprecate
-- E404 ... or you do not have permission, then E401 ... token is invalid
on every call after. Measured on the 1.0.0 release.
If the npm side does not complete, the script leaves the tags alone and exits non-zero. The tags are what name which pre-releases to retire, so deleting them after a failed deprecation would destroy the only record of what was missed.
What a tag triggers¶
Pushing a tag runs publish.yml. When it succeeds,
.github/workflows/docs.yml
runs on its completion and publishes the documentation for that version to the
gh-pages branch with mike, moving the
latest alias that the site root redirects to. publish.yml also creates the
GitHub release, with the body taken from the ## Version <version> section of
CHANGES.md by scripts/release-notes.mjs.
The order matters: the two used to start together on the tag push, so a publish that then refused left the site advertising a version npm did not have.
Nothing else publishes documentation. A push to a branch publishes nothing, so what is online always describes a version someone can install.
A pre-release publishes nothing either. It reaches docs.yml -- publish.yml
runs for pre-releases too -- and the job stops once it reads the version from
the published commit's manifest and finds a - in it.
One-time repository setup¶
Settings -> Pages -> Source: Deploy from a branch, branch gh-pages,
folder / (root).