132 lines
5.3 KiB
Markdown
132 lines
5.3 KiB
Markdown
# 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](../scripts/release.sh) and
|
|
[scripts/release-notes.sh](../scripts/release-notes.sh); the job graph is
|
|
[.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, 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](./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](../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.
|