#!/bin/sh set -eu # Release front-door. Run as `npm run create:release`. # # How we got here (short): 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. So split by strength: `pubv` (tiny, # changelog-driven) owns preflight + the interactive major/minor/patch heuristic # + graduating/committing CHANGELOG.md (no tag, no push); `npm version` syncs # package.json + the lockfile; `--amend` folds them into pubv's single commit; # tag AFTER the amend (so the tag is never orphaned) and push. # # 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. pubv refuses a dirty tree, so that # edit is committed as a staging commit and folded back into the single release # commit below. # # Rejected: the conventional-commits family (our history is gitmoji, not # Conventional; and we want hand-written notes); changesets/rtk (config + a # heavier version/publish flow that fights our CI-only publish); knope/kacl/ # bestikk (changelog-only — don't bump package.json; plus 5yr/2yr/brand-new # maintenance); pubv alone (verified it never writes package.json). We also # tried `versions` (silverwind) — great Gitea support — but pairing it with a # hand-rolled promote became a ~180-line script we'd have to maintain, which is # exactly what this ~30-line version replaces. CHANGELOG="CHANGELOG.md" BASE="main" if ! command -v code >/dev/null 2>&1; then echo "Error: 'code' (VS Code CLI) not found; install it or remove the editor step." >&2 exit 1 fi # Releases are cut from `main` (see CONTRIBUTING § Publishing workflow). Make # that explicit rather than relying on pubv's default-branch check, so the # error names `main` even when the remote's default is configured differently. CURRENT=$(git symbolic-ref --quiet --short HEAD || true) if [ "${CURRENT}" != "${BASE}" ]; then echo "Error: releases are cut from '${BASE}', but HEAD is '${CURRENT:-detached}'." >&2 exit 1 fi # pubv decides the "default branch" by reading the *local* # `refs/remotes/origin/HEAD`, not by asking the remote, and `git fetch` never # updates that ref. After a default-branch change — or a clone from when the # default was different — it goes stale and pubv warns/fails because the # current branch (main) does not match it, even though main *is* the remote # default. Refresh it from the remote first, so pubv's branch preflight # compares against reality. (Without a network this fails, but so would the # push pubv is about to do, so it is a real error rather than one to swallow.) if ! git remote set-head origin --auto >/dev/null 2>&1; then echo "Error: could not refresh origin/HEAD; check connectivity to origin." >&2 exit 1 fi # The [Unreleased] body drives pubv's bump heuristic, so finalize it first. echo "Opening ${CHANGELOG} in VS Code to finalize the release notes..." code --wait "${CHANGELOG}" # pubv refuses a dirty tree (its "continue with a dirty tree?" prompt defaults # to No), so a changed changelog must be committed before it runs. That commit # is staging only — the fold below rewrites it into the single release commit. NOTES_MSG=":memo: Finalize release notes" if [ -n "$(git status --porcelain -- "${CHANGELOG}")" ]; then echo "Committing finalized release notes..." git add "${CHANGELOG}" git commit -m "${NOTES_MSG}" fi echo "Running pubv..." pubv --no-tag --no-push --tag-prefix=none echo "Reading version from ${CHANGELOG}..." VERSION=$( sed -nE 's/^## \[([0-9]+\.[0-9]+\.[0-9]+)\].*/\1/p' "${CHANGELOG}" | head -n 1 ) if [ -z "${VERSION}" ]; then echo "Error: Could not determine release version from ${CHANGELOG}." >&2 exit 1 fi echo "Release version: ${VERSION}" echo "Updating package.json and package-lock.json..." npm version "${VERSION}" --no-git-tag-version # If pubv's graduation commit sits on top of our staging notes commit, drop it # back into the index so the amend below rewrites the notes commit into the one # release commit. A message check, not a flag, so a re-run after pubv aborted # still folds a notes commit left behind by the earlier attempt. if [ "$(git log -1 --format=%s HEAD~1 2>/dev/null || true)" = "${NOTES_MSG}" ]; then git reset --soft HEAD~1 fi echo "Amending release commit..." git add package.json package-lock.json "${CHANGELOG}" # The exact message format is load-bearing: the `release-gate` job in # .gitea/workflows/ci.yml recognizes `:bookmark: Release x.y.z` on main and # skips the full CI run, since the tag push immediately after verifies the # identical SHA (and publishes). Keep the two in sync. git commit --amend -m ":bookmark: Release ${VERSION}" echo "Creating tag ${VERSION}..." git tag "${VERSION}" echo "Pushing release..." git push git push --tags echo "Release ${VERSION} completed."