Skip to content

Commit 307555a

Browse files
committed
Add release-bump.sh script
1 parent 95884f9 commit 307555a

1 file changed

Lines changed: 138 additions & 0 deletions

File tree

scripts/release-bump.sh

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/usr/bin/env bash
2+
# Bump the most recent v<major>.<minor>.<patch> tag in this repository,
3+
# create a GitHub release, and force-update the floating v<major> tag.
4+
#
5+
# Usage:
6+
# scripts/release-bump.sh [--bump major|minor|patch]
7+
#
8+
# Default --bump is "patch". If no semver tag exists yet the script treats
9+
# the current version as v1.0.0, so the first patch release becomes v1.0.1,
10+
# the first minor v1.1.0, and the first major v2.0.0.
11+
12+
set -euo pipefail
13+
14+
bump=patch
15+
while [[ $# -gt 0 ]]; do
16+
case "$1" in
17+
--bump) bump="${2:-}"; shift 2 ;;
18+
--bump=*) bump="${1#--bump=}"; shift ;;
19+
-h|--help)
20+
sed -n '2,11p' "$0" | sed 's/^# \{0,1\}//'
21+
exit 0
22+
;;
23+
*)
24+
echo "release-bump: unknown argument: $1" >&2
25+
echo "usage: scripts/release-bump.sh [--bump major|minor|patch]" >&2
26+
exit 2
27+
;;
28+
esac
29+
done
30+
31+
case "$bump" in
32+
major|minor|patch) ;;
33+
*)
34+
echo "release-bump: --bump must be one of: major, minor, patch (got '$bump')" >&2
35+
exit 2
36+
;;
37+
esac
38+
39+
# ─── Pre-flight ─────────────────────────────────────────────────────────────
40+
41+
# Run from the repo root so relative behaviour is consistent.
42+
cd "$(git rev-parse --show-toplevel)"
43+
44+
if ! command -v gh >/dev/null 2>&1; then
45+
echo "release-bump: 'gh' (the GitHub CLI) is required but not installed." >&2
46+
exit 1
47+
fi
48+
49+
if [[ -n "$(git status --porcelain)" ]]; then
50+
echo "release-bump: working tree is not clean — commit or stash changes first." >&2
51+
git status --short >&2
52+
exit 1
53+
fi
54+
55+
# Make sure we have an up-to-date view of remote tags.
56+
git fetch --tags --quiet origin
57+
58+
# ─── Compute next version ───────────────────────────────────────────────────
59+
60+
# Pick the highest v<major>.<minor>.<patch> tag (annotated or lightweight).
61+
latest=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n 1 || true)
62+
if [[ -z "${latest}" ]]; then
63+
latest="v1.0.0"
64+
no_existing_tag=1
65+
else
66+
no_existing_tag=0
67+
fi
68+
69+
ver="${latest#v}"
70+
IFS=. read -r cur_major cur_minor cur_patch <<<"$ver"
71+
72+
if ! [[ "$cur_major" =~ ^[0-9]+$ && "$cur_minor" =~ ^[0-9]+$ && "$cur_patch" =~ ^[0-9]+$ ]]; then
73+
echo "release-bump: could not parse '$latest' as v<major>.<minor>.<patch>" >&2
74+
exit 1
75+
fi
76+
77+
case "$bump" in
78+
major) new_major=$((cur_major + 1)); new_minor=0; new_patch=0 ;;
79+
minor) new_major=$cur_major; new_minor=$((cur_minor + 1)); new_patch=0 ;;
80+
patch) new_major=$cur_major; new_minor=$cur_minor; new_patch=$((cur_patch + 1)) ;;
81+
esac
82+
83+
new_tag="v${new_major}.${new_minor}.${new_patch}"
84+
major_tag="v${new_major}"
85+
86+
if git rev-parse --verify --quiet "refs/tags/${new_tag}" >/dev/null; then
87+
echo "release-bump: tag ${new_tag} already exists locally; aborting." >&2
88+
exit 1
89+
fi
90+
91+
# ─── Confirm ────────────────────────────────────────────────────────────────
92+
93+
branch=$(git rev-parse --abbrev-ref HEAD)
94+
head_sha=$(git rev-parse --short HEAD)
95+
head_subject=$(git log -1 --pretty=%s)
96+
97+
echo "Repository: $(git config --get remote.origin.url 2>/dev/null || echo '<no remote>')"
98+
echo "Branch: ${branch}"
99+
echo "HEAD: ${head_sha} ${head_subject}"
100+
if [[ ${no_existing_tag} -eq 1 ]]; then
101+
echo "Latest tag: (none — defaulting to v1.0.0)"
102+
else
103+
echo "Latest tag: ${latest}"
104+
fi
105+
echo
106+
echo "About to:"
107+
echo " • create annotated tag ${new_tag} at HEAD"
108+
echo " • push ${new_tag} to origin"
109+
echo " • create GitHub release ${new_tag} with auto-generated notes"
110+
echo " • force-update tag ${major_tag} to point at ${new_tag}"
111+
echo " • force-push ${major_tag} to origin"
112+
echo
113+
114+
read -r -p "Do you want to tag and release ${new_tag} and update the ${major_tag} tag? [y/N] " answer
115+
case "$answer" in
116+
y|Y|yes|YES|Yes) ;;
117+
*) echo "Aborted."; exit 0 ;;
118+
esac
119+
120+
# ─── Execute ────────────────────────────────────────────────────────────────
121+
122+
echo "› git tag -a ${new_tag}"
123+
git tag -a "${new_tag}" -m "Release ${new_tag}"
124+
125+
echo "› git push origin ${new_tag}"
126+
git push origin "${new_tag}"
127+
128+
echo "› gh release create ${new_tag} --generate-notes"
129+
gh release create "${new_tag}" --generate-notes --title "${new_tag}"
130+
131+
echo "› git tag -f ${major_tag} ${new_tag}"
132+
git tag -f "${major_tag}" "${new_tag}"
133+
134+
echo "› git push --force origin ${major_tag}"
135+
git push --force origin "${major_tag}"
136+
137+
echo
138+
echo "Released ${new_tag} and moved ${major_tag}${new_tag}."

0 commit comments

Comments
 (0)