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.
141 lines
6.0 KiB
Markdown
141 lines
6.0 KiB
Markdown
# 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](../scripts/release.sh) and
|
|
[scripts/release-notes.sh](../scripts/release-notes.sh), and the job graph lives
|
|
in [.gitea/workflows/ci.yml](../.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](../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](./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](../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.
|