Skip to content

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

  1. Run the pre-release-check skill. It runs the whole gate plus the documentation, dependency, changelog and version checks.

  2. Run the bump-version skill. It sets the version across every package.json and promotes the CHANGES.md Development section. It does not tag or push.

  3. Fold the pre-release sections in. bump-version comes 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.

  1. 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).