Files
tiny-pattern-ts/development/publishing.md
T
tmu dc7ce22fc5 📝 Add development/ docs for decisions and known issues
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.
2026-09-15 13:26:59 +00:00

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

  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, 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.
  3. scripts/release.sh creates a single release commit (graduated changelog + package.json bump, amended into one commit), tags it, and pushes everything to Gitea.
  4. CI fires on both pushes: the publish job runs on the tag (build + publish-tier checks + release page + npm publish), while the branch run's release-gate job recognizes the release commit and skips build/maintain — the tag verifies the identical SHA, so no work is duplicated. The publish-tier checks must pass before the artifact is published. The publish job also creates the Gitea release page from the matching Keep-a-Changelog section; it runs 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, so they 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 that marks the exact commit on main that gets published.
  • No single tool did both the [Unreleased] graduation and the package.json bump. Splitting 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 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 bump package.json — and they carry 5-year / 2-year / brand-new maintenance.
  • pubv alone: verified it never writes package.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.sh reads the version from the changelog, so the changelog is the input and package.json is 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 ## Version line in the README: it would make release.sh responsible for a third file.
  • Linking package.json from 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 v is tolerated so both v1.2.3 and 1.2.3 match 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.
  • secrets is not an allowed context 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.