A release ships two independent artifacts:
- The library — the
postboinpm package (published from this repo root). - The docs site — docs.postboi.app, the SvelteKit app at the repo root, deployed on push to
main. Each release is snapshotted so readers can switch to older versions.
Both come out of one merge. Label the PR, merge it, and
release.yml does the rest.
Label the release PR release:patch, release:minor or release:major, then
merge it. The label can go on at any point before the merge; merging an
unlabelled PR releases nothing, which is what you want for the PRs that aren't
releases.
Pre-1.0 the bump can't be inferred from the diff — a breaking change is a
minor here (the settings→config rename went 0.5.0 → 0.6.0) — so the
label is the one judgement left to a human.
On merge, release.yml runs, in order:
- Freezes the outgoing docs.
src/site/content/docs/always holds the latest docs, so the version being superseded is copied tosrc/site/content/v<prev>/and listed inversions.json, nav included. Both copies are read from the commit the PR landed on, so whatever docs the release PR itself changed can't leak into the archive. Committed asFreeze the <prev> docs before <version> goes out. - Bumps
package.jsonand commits the bare version (0.36.0). - Validates —
lint,check,test, the credential-pollution test run from ci.yml, andbuild(which runspublinton the packed output). This matters more than it looks: commits pushed withGITHUB_TOKENnever trigger ci.yml, so this run is the release commits' CI. It's also the last place a release can fail cheaply — everything after is a push, a publish or a tag. - Pushes
mainand the tagvX.Y.Z— atomically, so they land together or not at all, and rebasing over anything that merged to main mid-release. - Publishes to npm by dispatching
publish.ymland waiting for it — trusted publishing over OIDC, with provenance — and creates the GitHub release with generated notes. - Moves the examples' pins to the new version once npm is serving it, pushes
Examples: catch up with <version>, and runs every example'sciscript against the real published package.
So a release PR contains what it always did — the code, and the docs edits for the new version — and nothing about releasing. Don't hand-snapshot the docs in it: the workflow does that from the pre-merge commit, and it refuses to run if the snapshot directory already exists.
- The three labels:
release:patch,release:minor,release:major. GitHub will offer to create one the first time you type it into a PR's label dropdown. - A trusted publisher configured on npmjs.com so the Publish workflow can
publish without a token: package settings for
postboi→ Trusted Publisher → GitHub Actions, with organization/userpostboi-mail, repositorypostboi, and workflow filenamepublish.yml. That filename is whyrelease.ymldispatchespublish.ymlrather than calling or copying it: npm validates the run's top-level workflow, so a reusable-workflow call presentsrelease.ymland gets no token, and a tag pushed withGITHUB_TOKENtriggers no workflow at all. A dispatch is the one route that givespublish.ymla run of its own.
No local npm login or gh auth login needed anywhere.
Use "Re-run failed jobs" on the run — every job is safe to repeat: the
publish skips a version npm already has from that exact commit, the release
step skips a tag that already has one, and the examples job re-derives its
work from main. What you must not do is "Re-run all jobs" after the
release landed — that would bump again and cut a second version, so the
prepare job detects the case and refuses.
Both of these fail fast with the reason, rather than half-releasing:
- A PR from a fork.
GITHUB_TOKENis read-only on fork PRs, so the release job can't pushmain. Merge it unlabelled and release by dispatch instead. - A rebase merge. The docs freeze reads the merge's first parent, which is main's previous tip for a squash or a merge commit but not for a rebase of several commits. The workflow detects a multi-commit rebase merge and refuses. Squash, as every merged PR in the history has.
One more shape to avoid: releases queue one at a time (concurrency), and
GitHub keeps at most one waiting run per group — merging a third release PR
while one runs and one waits silently cancels the waiting one. Merge release
PRs one at a time; if a queued release did get cancelled, release it by
dispatch.
- patch — fixes, docs, anything that can't change a working call.
- minor — new API, or a breaking change. Pre-1.0 that's the same bump.
- major — not yet.
Only a minor moves the docs line. latest in versions.json names the docs
line, not the published package, so it stays put on a patch: 0.33.1 ships
with "latest": "0.33.0" and no v0.33.0 folder, exactly as 0.27.1 did. A
patch's doc edits belong in the live docs; freezing them would archive a version
nobody should be on (often, as with 0.33.1, one whose docs describe the bug the
patch just fixed). The v0.33.0 archive gets cut when 0.34.0 ships. The
version badge on the site reads package.json, so it stays correct across
patches regardless.
scripts/snapshot-docs.ts enforces this — a patch exits having done nothing.
Two escape hatches, in order of preference.
Run the Release workflow by hand. Actions → Release → Run workflow, with a
bump of patch, minor, major or an exact X.Y.Z. Identical to the merge
path, releasing main as it stands. It assumes the docs on main are still the
outgoing version's; if the new version's doc edits already landed, pass the last
commit before them as docs_before.
Release from a clean local main.
npm run release -- X.Y.Z # or: patch | minor | majorscripts/release.sh freezes the docs (the same
snapshot-docs.ts the workflow runs, so the two can't drift), bumps, validates,
commits, tags and pushes. The tag push triggers publish.yml, which publishes
and cuts the GitHub release. Set DOCS_BEFORE=<ref> if the new version's doc
edits are already on main. The examples aren't part of it — once npm is serving
the release:
npm run release:examples -- X.Y.Z
bash scripts/check-examples.shEach examples/*/package.json pins "postboi": "^X.Y.Z". CI doesn't resolve
those pins — the Examples job packs a tarball from the commit under test and
installs that — so the bump exists for the person copying an example folder: the
pin is what they actually install, and a stale one hands them a release that
predates the code beside it. check-examples.sh is the other half: it installs
the pins from npm, which is exactly the check the release workflow runs against
the freshly published package.
If tags can't be pushed from where you're releasing (e.g. a remote sandbox whose
git proxy only allows branch pushes), push main with the version-bump commit
and run the Publish workflow manually on main instead — it derives the tag
from package.json and creates both the tag and the release itself.
- The Release run is green all the way through the Examples job.
npm view postboi versionshowsX.Y.Z.- The GitHub release exists at
vX.Y.Z. - The docs site shows the new version as latest and archived versions still load.
On the manual path, double-check the docs after releasing: if the new version's
doc edits were already on main when you ran it, /vPREV must still render the
old version's pages — that's what DOCS_BEFORE exists for.
Snapshots are plain committed files under
src/site/content/v*/. There's no build-time git dependency — the site builds on a shallow clone. (The first snapshot,v0.5.0, was seeded once from git history; everything after is a copy of the tree at a commit.)
- Commit message for a release is the bare version (
0.7.0), matching history. - Tags are
vX.Y.Z. Pre-0.7.0releases predate the script and are untagged. - Pre-1.0, breaking changes are minor bumps.