mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
ffe96586c7
Introduce CHANGELOG.md (Keep a Changelog) as the single source of release notes, and scripts/release.sh (npm run release X.Y.Z) which promotes the Unreleased section, commits the bare version as a real release commit, tags it, and reopens the next -develop cycle. The Release workflow now verifies the tagged commit's version equals the tag and publishes the CHANGELOG section as the release notes instead of auto-generated commit lists. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
152 lines
5.4 KiB
Bash
Executable File
152 lines
5.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Cut a release. Usage:
|
|
#
|
|
# npm run release X.Y.Z [NEXT_DEV_BASE]
|
|
#
|
|
# X.Y.Z the version to release (must equal main's current X.Y.Z-develop base)
|
|
# NEXT_DEV_BASE optional base to open next (defaults to next minor, e.g. 0.4.0 -> 0.5.0)
|
|
#
|
|
# It guards, then in one shot:
|
|
# 1. promotes CHANGELOG "## [Unreleased]" -> "## [X.Y.Z] - <date>"
|
|
# 2. sets package.json to the bare X.Y.Z and commits the release commit
|
|
# 3. tags vX.Y.Z on that commit
|
|
# 4. opens the next "-develop" cycle (package.json + fresh Unreleased) and commits
|
|
# 5. pushes main + the tag (after an explicit confirmation) -> triggers the Release workflow
|
|
#
|
|
# The tag points at a commit whose package.json reads exactly X.Y.Z, so the
|
|
# published bundle and the git history agree on the version. CI verifies the
|
|
# match and publishes the promoted CHANGELOG section as the release notes.
|
|
|
|
set -euo pipefail
|
|
|
|
die() {
|
|
echo "release: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
semver_re='^[0-9]+\.[0-9]+\.[0-9]+$'
|
|
|
|
VERSION="${1:-}"
|
|
[ -n "$VERSION" ] || die "missing version. Usage: npm run release X.Y.Z [NEXT_DEV_BASE]"
|
|
[[ "$VERSION" =~ $semver_re ]] || die "version '$VERSION' is not X.Y.Z"
|
|
|
|
# Default next dev base: bump the minor, reset patch.
|
|
if [ -n "${2:-}" ]; then
|
|
NEXT_BASE="$2"
|
|
[[ "$NEXT_BASE" =~ $semver_re ]] || die "next dev base '$NEXT_BASE' is not X.Y.Z"
|
|
else
|
|
IFS='.' read -r MA MI _PA <<<"$VERSION"
|
|
NEXT_BASE="${MA}.$((MI + 1)).0"
|
|
fi
|
|
NEXT_DEV="${NEXT_BASE}-develop"
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
cd "$ROOT"
|
|
|
|
# --- Guards ----------------------------------------------------------------
|
|
|
|
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
|
[ "$BRANCH" = "main" ] || die "must be on 'main' (currently on '$BRANCH')"
|
|
|
|
[ -z "$(git status --porcelain)" ] || die "working tree is not clean — commit or stash first"
|
|
|
|
git fetch --quiet origin main || die "could not fetch origin/main"
|
|
LOCAL="$(git rev-parse @)"
|
|
REMOTE="$(git rev-parse '@{u}')"
|
|
[ "$LOCAL" = "$REMOTE" ] || die "local main is not in sync with origin/main — pull/push first"
|
|
|
|
PKG_BASE="$(node -p 'require("./package.json").version.split("-")[0]')"
|
|
[ "$PKG_BASE" = "$VERSION" ] || die "package.json base is $PKG_BASE, expected $VERSION — bump main to ${VERSION}-develop first (or release $PKG_BASE)"
|
|
|
|
git rev-parse -q --verify "refs/tags/v$VERSION" >/dev/null && die "tag v$VERSION already exists"
|
|
|
|
[ -f CHANGELOG.md ] || die "CHANGELOG.md not found"
|
|
# The Unreleased section must carry content — an empty changelog ships empty notes.
|
|
UNRELEASED_BODY="$(awk '
|
|
/^## \[Unreleased\]/ {grab=1; next}
|
|
/^## / && grab {exit}
|
|
grab {print}
|
|
' CHANGELOG.md | grep -v '^[[:space:]]*$' || true)"
|
|
[ -n "$UNRELEASED_BODY" ] || die "CHANGELOG '## [Unreleased]' is empty — write the release notes there first"
|
|
|
|
# --- Plan ------------------------------------------------------------------
|
|
|
|
DATE="$(date +%Y-%m-%d)"
|
|
echo "Release plan:"
|
|
echo " version : $VERSION (tag v$VERSION)"
|
|
echo " release date : $DATE"
|
|
echo " next cycle : $NEXT_DEV"
|
|
echo
|
|
echo "Unreleased notes that will become the v$VERSION release notes:"
|
|
echo "$UNRELEASED_BODY" | sed 's/^/ | /'
|
|
echo
|
|
read -r -p "Proceed (commits, tag, and PUSH to origin)? [y/N] " ANSWER
|
|
case "$ANSWER" in
|
|
y | Y | yes | YES) ;;
|
|
*) die "aborted" ;;
|
|
esac
|
|
|
|
# --- 1. Promote CHANGELOG Unreleased -> this version -----------------------
|
|
|
|
node - "$VERSION" "$DATE" <<'NODE'
|
|
const fs = require("fs");
|
|
const [version, date] = process.argv.slice(2);
|
|
const file = "CHANGELOG.md";
|
|
let text = fs.readFileSync(file, "utf8");
|
|
|
|
if (text.includes(`## [${version}]`)) {
|
|
console.error(`release: CHANGELOG already has a [${version}] section`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Replace the Unreleased heading with a fresh empty Unreleased + the new version.
|
|
text = text.replace(
|
|
/^## \[Unreleased\][^\n]*\n/m,
|
|
`## [Unreleased]\n\n## [${version}] - ${date}\n`,
|
|
);
|
|
|
|
// Refresh the link reference block at the bottom, if present.
|
|
const repo = "https://github.com/juherr/kill-the-news";
|
|
const unreleasedLink = `[Unreleased]: ${repo}/compare/v${version}...HEAD`;
|
|
if (/^\[Unreleased\]:/m.test(text)) {
|
|
text = text.replace(
|
|
/^\[Unreleased\]:.*$/m,
|
|
`${unreleasedLink}\n[${version}]: ${repo}/compare/PREV...v${version}`,
|
|
);
|
|
// Best-effort: point the new version diff at the previous tagged version.
|
|
const prev = [...text.matchAll(/^\[(\d+\.\d+\.\d+)\]:/gm)]
|
|
.map((m) => m[1])
|
|
.find((v) => v !== version);
|
|
if (prev) {
|
|
text = text.replace("compare/PREV...", `compare/v${prev}...`);
|
|
} else {
|
|
text = text.replace(`/compare/PREV...v${version}`, `/releases/tag/v${version}`);
|
|
}
|
|
}
|
|
|
|
fs.writeFileSync(file, text);
|
|
console.log(`Updated CHANGELOG.md for ${version}`);
|
|
NODE
|
|
|
|
# --- 2. Release commit (bare version) + 3. tag -----------------------------
|
|
|
|
npm version "$VERSION" --no-git-tag-version --allow-same-version >/dev/null
|
|
git add package.json package-lock.json CHANGELOG.md
|
|
git commit -m "chore(release): $VERSION" >/dev/null
|
|
git tag "v$VERSION"
|
|
echo "Committed release v$VERSION and tagged it."
|
|
|
|
# --- 4. Open the next develop cycle ----------------------------------------
|
|
|
|
npm version "$NEXT_DEV" --no-git-tag-version >/dev/null
|
|
git add package.json package-lock.json
|
|
git commit -m "chore: open $NEXT_BASE develop cycle" >/dev/null
|
|
echo "Opened next cycle: $NEXT_DEV."
|
|
|
|
# --- 5. Push ----------------------------------------------------------------
|
|
|
|
git push origin main "v$VERSION"
|
|
echo
|
|
echo "Pushed main + v$VERSION. The Release workflow will publish the GitHub Release."
|