Category files under development/ replace the decision prose that was scattered through README.md and CONTRIBUTING.md. Each decision is a block with Decision (YYYY-MM) / Why / Rejected / Known issue; the new library.md records the public API contract and its limitations. AGENTS.md and backlog.tasks now point at the new home.
6.0 KiB
Publishing
Publishing is CI-only by policy. Local npm publish is not supported. The
maintainer triggers releases from main; the mechanics live in
scripts/release.sh and
scripts/release-notes.sh, and the job graph lives
in .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, any edit is committed first (then folded into the release commit), and pubv's interactive prompt suggests a version from those notes — the maintainer confirms or edits it. scripts/release.shcreates a single release commit (graduated changelog +package.jsonbump, amended into one commit), tags it, and pushes everything to Gitea.- CI fires on both pushes: the
publishjob runs on the tag (build+ publish-tier checks + release page +npm publish), while the branch run'srelease-gatejob recognizes the release commit and skipsbuild/maintain— the tag verifies the identical SHA, so no work is duplicated. The publish-tier checks must pass before the artifact is published. Thepublishjob also creates the Gitea release page from the matching Keep-a-Changelog section; it runs beforenpm publishso 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, so they 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 that marks the exact commit onmainthat gets published. - No single tool did both the
[Unreleased]graduation and thepackage.jsonbump. Splitting 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 pubv's single commit; the tag is created after the amend so it is never orphaned. - The notes are finalized in VS Code before pubv: the
[Unreleased]body is what pubv's bump heuristic reads, so editing afterwards would inform the changelog only, not the version choice. The staging commit that makes pubv accept the tree is folded back into the single release commit.
Rejected
- The conventional-commits family: the history is gitmoji, not Conventional, and we want hand-written notes (see workflow.md § Commit messages).
changesets/rtk: config plus a heavier version/publish flow that fights the CI-only publish.knope/kacl/bestikk: changelog-only — they don't bumppackage.json— and they carry 5-year / 2-year / brand-new maintenance.pubvalone: verified it never writespackage.json.versions(silverwind): great Gitea support, but pairing it with a hand-rolled promote became a ~180-line script to maintain, which is exactly what 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. The README carries no version line and
does not link package.json.
Why
release.shreads the version from the changelog, so the changelog is the input andpackage.jsonis the derived copy — one direction, no drift.- npm and Gitea render the version from package metadata, so a README copy would be a third place to update for no reader benefit, and a link would only move the lookup without removing the copy.
Rejected
- A
## Versionline in the README: it would makerelease.shresponsible for a third file. - Linking
package.jsonfrom the README: it invites a hand-maintained duplicate that 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 the section 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 with an empty
body. A leading
vis tolerated so bothv1.2.3and1.2.3match the## [1.2.3]heading.
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 only needs
contents: write, so no secret gate is needed for the release page. secretsis not an allowed context 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.