#!/usr/bin/env bash # # Cut a release: bump the version, generate a CHANGELOG entry from the commits # since the last tag, commit, tag, and push. # # scripts/release.sh patch|minor|major # set -euo pipefail part="${1:-}" case "$part" in patch | minor | major) ;; *) echo "usage: $0 patch|minor|major" >&2 exit 1 ;; esac cd "$(git rev-parse --show-toplevel)" if ! git diff --quiet || ! git diff --cached --quiet; then echo "working tree is not clean; commit or stash first" >&2 exit 1 fi branch="$(git symbolic-ref --short HEAD)" if [ "$branch" != "main" ]; then echo "not on main (on $branch)" >&2 exit 1 fi current="$(git tag --list 'v*' --sort=-v:refname | head -n1)" current="${current:-v0.0.0}" IFS=. read -r major minor patch <<<"${current#v}" case "$part" in major) major=$((major + 1)); minor=0; patch=0 ;; minor) minor=$((minor + 1)); patch=0 ;; patch) patch=$((patch + 1)) ;; esac next="v$major.$minor.$patch" date="$(date +%Y-%m-%d)" if grep -qiE '^## +unreleased *$' CHANGELOG.md; then # Promote the hand-written "## Unreleased" section to this version. awk -v repl="## $next - $date" ' !done && tolower($0) ~ /^## +unreleased *$/ { print repl; done = 1; next } { print } ' CHANGELOG.md >CHANGELOG.tmp && mv CHANGELOG.tmp CHANGELOG.md else # No curated section: fall back to a list of commit subjects since the tag. range="" [ "$current" != "v0.0.0" ] && range="$current..HEAD" notes="$(git log --no-merges --pretty='- %s' $range)" notes="${notes:-- (no changes)}" head_part="$(sed '/^## /,$d' CHANGELOG.md)" rest_part="$(sed -n '/^## /,$p' CHANGELOG.md)" { printf '%s\n' "$head_part" printf '\n## %s - %s\n\n%s\n\n' "$next" "$date" "$notes" printf '%s\n' "$rest_part" } >CHANGELOG.md fi git commit -aqm "Release $next" git tag -a "$next" -m "$next" git push -q origin "$branch" "$next" echo "released $next"