5.3 KiB
Publishing
Publishing is CI-only: local npm publish is not supported, and the maintainer
triggers releases from main. The mechanics are in
scripts/release.sh and
scripts/release-notes.sh; the job graph is
.gitea/workflows/ci.yml.
Release steps
- All intended changes are merged to
mainand passing CI. - The maintainer runs
npm run create:release. VS Code opens CHANGELOG.md to finalize the[Unreleased]notes; because pubv refuses a dirty tree, the edit is committed first (then folded into the release commit), and pubv suggests a version from those notes to confirm or edit. scripts/release.shcreates one release commit (graduated changelog +package.jsonbump, amended together), tags it, and pushes.- CI fires on both pushes.
publishruns on the tag (build + publish checks + release page +npm publish), whilerelease-gaterecognizes the release commit and skipsbuild/maintain: the tag verifies the identical SHA, so no work is duplicated. The publish checks pass before the artifact is published, and the release page is created from the matching Keep-a-Changelog section beforenpm publish, so a broken page fails CI without consuming a version andnpm publishstays the last step.
CI-only publishing
Decision (2026-09)
Releases are cut by CI from main; there is no local npm publish and no
publish:* script in the check chain.
Why
- The tag is the artifact marker: CI verifies the exact commit it points at, so a local publish could ship something the tag does not describe.
publish:publint/publish:attwvalidate the publishable artifact, which needs a fresh build; they are not source-correctness checks and do not belong incheck.
The release commit is assembled from two tools
Decision (2026-09)
create:release uses pubv for the changelog graduation and bump heuristic,
then npm version for the package.json + lockfile bump, amended into a single
release commit.
Why
- We want hand-written Keep-a-Changelog notes, an
[Unreleased]->## [x.y.z] - DATEgraduation, and a tag on the exact commit that gets published — and no single tool did both the graduation and thepackage.jsonbump. - Split by strength:
pubv(tiny, changelog-driven) owns preflight, the interactive major/minor/patch heuristic, and graduating and committingCHANGELOG.md(no tag, no push);npm versionsyncspackage.json+ the lockfile;--amendfolds them into one commit; the tag is created after the amend so it is never orphaned. - The notes are finalized before pubv because its bump heuristic reads the
[Unreleased]body — editing afterwards would inform the changelog only, not the version. The staging commit that satisfies pubv's clean-tree check is folded back into the single release commit.
Rejected
- The conventional-commits family: the history is gitmoji, not Conventional, and the notes are hand-written (see workflow.md § Commit messages).
changesets/rtk: config plus a heavier flow that fights the CI-only publish.knope/kacl/bestikk: changelog-only (nopackage.jsonbump) and 5-year / 2-year / brand-new maintenance.pubvalone: it never writespackage.json.versions(silverwind): good Gitea support, but pairing it with a hand-rolled promote became a ~180-line script, which this ~30-line version replaces.
The version has one source of truth
Decision (2026-09)
The version is derived from the graduated ## [x.y.z] heading in
CHANGELOG.md and written to package.json +
package-lock.json by npm version.
Why
release.shreads the version from the changelog, so the changelog is the input andpackage.jsonthe derived copy — one direction, no drift.
Rejected
- A
## Versionline in the README: it makesrelease.shresponsible for a third file. - Linking
package.jsonfrom the README: it invites a hand-maintained duplicate the link does not keep in sync.
Release notes are extracted from the changelog
Decision (2026-09)
scripts/release-notes.sh <tag> prints the Keep-a-Changelog section for the tag
and exits non-zero when it is missing.
Why
- CI reuses the release body from the same file that drove the version, so the page and the changelog cannot disagree.
- Failing on a missing section means a release can never publish an empty body.
A leading
vis tolerated so bothv1.2.3and1.2.3match## [1.2.3].
Token gates
Decision (2026-09)
The Gitea release page uses the run's automatic token (github.token).
npm publish is gated on NPM_TOKEN, lifted into job-level env. A final
always() step fails the job unless both halves reported success.
Why
- The automatic token needs only
contents: write, so the release page needs no secret gate. secretsis not allowed in a stepif, soNPM_TOKENmust be lifted into job-levelenv; an unset secret then skips the publish instead of attempting an unauthenticated one.- A tag is all-or-nothing: without the
always()guard a skipped or failed npm half would leave the job silently green. The guard turns it red.
Set NPM_TOKEN (npm publish rights) under Settings -> Actions -> Secrets.