A release pushes main and then a tag pointing at the same commit, so
the branch run re-verifies the identical SHA the tag run already
verifies (and publishes). Add a cheap release-gate job that recognizes
the '🔖 Release x.y.z' commit message on main and skips the
full build/maintain jobs; tag, PR, and ordinary main pushes are
unaffected, and the gate fails open (runs CI) if it errors.
119 lines
5.0 KiB
Bash
Executable File
119 lines
5.0 KiB
Bash
Executable File
#!/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."
|