Skip to content

Playbook: cut a release

Habitus uses Changesets for intent and changelog, and a v* tag to trigger publishing.

Why versioning is done locally

The usual Changesets automation (changesets/action, which opens a "Version Packages" PR) is GitHub-API-specific and does not work against Gitea. So the version bump happens on your machine and CI only reacts to the tag. Do not try to wire the action up; it will not work.

During development

Every PR that changes anything a consumer can observe adds a changeset:

sh
npx changeset

Pick the bump and write the note. Write it for someone deciding whether to upgrade, not as a summary of your diff:

  • Good: HabButton now renders a real <a> when given as="a", so links appear in browser history.
  • Bad: Refactored button internals.

Bump levels:

LevelWhen
patchBug fix, no API change
minorNew component, new optional prop, additive behaviour
majorRenamed or removed prop/slot/event/type, changed default, changed markup others may select on, changed token meaning

A PR that only touches tests, docs prose or CI needs no changeset. CI runs changeset status as a prompt, not a hard gate — but if it flags you and you believe no changeset is needed, say why in the PR.

Releasing

  1. Start clean on an up-to-date main, with npm run verify passing.

  2. Apply the changesets. This bumps package.json and writes CHANGELOG.md:

    sh
    npx changeset version
  3. Read the changelog. This is the one moment to catch a mis-levelled changeset. If something additive was marked major, or a breaking change slipped in as minor, fix it now — git checkout . and correct the changeset files rather than editing the generated changelog.

  4. Commit and tag. The tag must match the new version exactly; CI verifies this and fails the release if it does not.

    sh
    git add .
    git commit -m "chore: release v$(node -p "require('./package.json').version")"
    git tag "v$(node -p "require('./package.json').version")"
    git push --follow-tags
  5. Watch the release workflow. .gitea/workflows/release.yml re-verifies the tag against the manifest, runs lint, typecheck, tests and the build, then publishes to the Gitea registry and creates a release.

Docs deployment

Separate from the npm release and not tied to it: every push to main runs .gitea/workflows/docs-deploy.yml, which builds the workbench into an nginx image, pushes :latest and :<commit-sha> to git.livereader.com/livereader/habitus-docs, and notifies Watchtower.

To roll the docs back, retag a known-good SHA as :latest and poke Watchtower:

sh
docker pull git.livereader.com/livereader/habitus-docs:<sha>
docker tag  git.livereader.com/livereader/habitus-docs:<sha> \
            git.livereader.com/livereader/habitus-docs:latest
docker push git.livereader.com/livereader/habitus-docs:latest
curl -fsS -X POST -H "Authorization: Bearer $WATCHTOWER_HTTP_API_TOKEN" \
  "$WATCHTOWER_URL/v1/update?image=git.livereader.com/livereader/habitus-docs"

If publishing fails

  • 403/401 — the LR_PUBLISH_TOKEN repository secret is missing or lacks package:write.
  • EPUBLISHCONFLICT / version already exists — that version was already published. Registries do not allow republishing; bump again.
  • Tag/manifest mismatch — the guard did its job. Delete the tag, fix the version, re-tag.

Prereleases

For something consumers should try but not depend on:

sh
npx changeset pre enter next
npx changeset version   # -> 1.2.0-next.0

Tag and push as normal, then npx changeset pre exit when stabilising.

Proprietary — internal use only.