Files
2026-09-15 21:46:41 +00:00

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

  1. All intended changes are merged to main and passing CI.
  2. 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.
  3. scripts/release.sh creates one release commit (graduated changelog + package.json bump, amended together), tags it, and pushes.
  4. CI fires on both pushes. publish runs on the tag (build + publish checks + release page + npm publish), while release-gate recognizes the release commit and skips build/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 before npm publish, so a broken page fails CI without consuming a version and npm publish stays 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:attw validate the publishable artifact, which needs a fresh build; they are not source-correctness checks and do not belong in check.

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] - DATE graduation, and a tag on the exact commit that gets published — and no single tool did both the graduation and the package.json bump.
  • Split by strength: pubv (tiny, changelog-driven) owns preflight, the interactive major/minor/patch heuristic, and graduating and committing CHANGELOG.md (no tag, no push); npm version syncs package.json + the lockfile; --amend folds 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 (no package.json bump) and 5-year / 2-year / brand-new maintenance.
  • pubv alone: it never writes package.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.sh reads the version from the changelog, so the changelog is the input and package.json the derived copy — one direction, no drift.

Rejected

  • A ## Version line in the README: it makes release.sh responsible for a third file.
  • Linking package.json from 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 v is tolerated so both v1.2.3 and 1.2.3 match ## [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.
  • secrets is not allowed in a step if, so NPM_TOKEN must be lifted into job-level env; 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.