Skip to content

Commit c62eee2

Browse files
authored
Merge pull request #226 from oschwald/greg/release-script
Add release script
2 parents 1f7d53b + fb72706 commit c62eee2

1 file changed

Lines changed: 180 additions & 0 deletions

File tree

dev-bin/release.sh

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
cd "$(git rev-parse --show-toplevel)"
6+
7+
die() {
8+
echo "Error: $*" >&2
9+
exit 1
10+
}
11+
12+
require_command() {
13+
if ! command -v "$1" >/dev/null 2>&1; then
14+
die "$1 is not installed or not in PATH"
15+
fi
16+
}
17+
18+
for command in gh git go golangci-lint; do
19+
require_command "$command"
20+
done
21+
22+
current_branch=$(git branch --show-current)
23+
if [ -z "$current_branch" ]; then
24+
die "releases cannot be created from a detached HEAD"
25+
fi
26+
27+
echo "Fetching from origin..."
28+
git fetch origin
29+
git submodule update --init --recursive
30+
31+
if ! git merge-base --is-ancestor origin/main HEAD; then
32+
die "the current branch does not contain the latest origin/main"
33+
fi
34+
35+
if [ -n "$(git status --porcelain)" ]; then
36+
die "the working directory is not clean"
37+
fi
38+
39+
release_commit=$(git rev-parse --verify HEAD)
40+
41+
release_heading=$(awk '/^## / { print; exit }' CHANGELOG.md)
42+
if [ "$release_heading" = "## Unreleased" ]; then
43+
die "replace the Unreleased heading with a version and release date"
44+
fi
45+
46+
semver_number='(0|[1-9][0-9]*)'
47+
semver_identifier='(0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)'
48+
heading_pattern="^## $semver_number\.$semver_number\.$semver_number(-$semver_identifier(\.$semver_identifier)*)? - [0-9]{4}-[0-9]{2}-[0-9]{2}$"
49+
if [ -z "$release_heading" ] || [[ ! $release_heading =~ $heading_pattern ]]; then
50+
die "CHANGELOG.md does not start with a release heading such as ## 2.6.0 - 2026-08-08"
51+
fi
52+
53+
heading_regex='^##[[:space:]]([^[:space:]]+)[[:space:]]-[[:space:]]([0-9]{4}-[0-9]{2}-[0-9]{2})$'
54+
if [[ ! $release_heading =~ $heading_regex ]]; then
55+
die "could not parse release heading: $release_heading"
56+
fi
57+
58+
version=${BASH_REMATCH[1]}
59+
release_date=${BASH_REMATCH[2]}
60+
today=$(date +%Y-%m-%d)
61+
tag="v$version"
62+
63+
if [ "$release_date" != "$today" ]; then
64+
die "release date $release_date is not today ($today)"
65+
fi
66+
67+
notes=$(
68+
awk -v heading="$release_heading" '
69+
$0 == heading { found = 1; next }
70+
found && /^## / { exit }
71+
found { print }
72+
' CHANGELOG.md | sed '/./,$!d'
73+
)
74+
if [ -z "$notes" ]; then
75+
die "the $version changelog section has no release notes"
76+
fi
77+
78+
module_path=$(go list -m -f '{{.Path}}')
79+
major=${version%%.*}
80+
case "$major" in
81+
0 | 1)
82+
if [[ $module_path =~ /v[0-9]+$ ]]; then
83+
die "tag $tag does not match module path $module_path"
84+
fi
85+
;;
86+
*)
87+
if [[ $module_path != */v"$major" ]]; then
88+
die "tag $tag does not match module path $module_path"
89+
fi
90+
;;
91+
esac
92+
93+
if ! gh auth status >/dev/null 2>&1; then
94+
die "gh is not authenticated"
95+
fi
96+
97+
repository=$(
98+
gh repo view "$(git remote get-url origin)" --json nameWithOwner --jq .nameWithOwner
99+
) || die "could not resolve the origin GitHub repository"
100+
101+
if git show-ref --verify --quiet "refs/tags/$tag"; then
102+
die "tag $tag already exists locally"
103+
fi
104+
105+
remote_tag=$(git ls-remote origin "refs/tags/$tag") ||
106+
die "could not check whether tag $tag exists on origin"
107+
if [ -n "$remote_tag" ]; then
108+
die "tag $tag already exists on origin"
109+
fi
110+
111+
release_response=$(
112+
gh api --include --silent "repos/$repository/releases/tags/$tag" 2>/dev/null || true
113+
)
114+
release_http_status=$(printf '%s\n' "$release_response" | awk 'NR == 1 { print $2 }')
115+
case "$release_http_status" in
116+
200) die "release $tag already exists" ;;
117+
404) ;;
118+
*) die "could not verify whether release $tag exists" ;;
119+
esac
120+
121+
echo "Running release checks..."
122+
if [ "$(go env GOHOSTOS)" != "linux" ] || [ "$(go env GOHOSTARCH)" != "amd64" ]; then
123+
die "release checks require a Linux/amd64 host"
124+
fi
125+
if [ "$(go env GOOS)" != "linux" ] || [ "$(go env GOARCH)" != "amd64" ]; then
126+
die "release checks require GOOS=linux and GOARCH=amd64"
127+
fi
128+
if [ "$(go env CGO_ENABLED)" != "1" ]; then
129+
die "release checks require CGO_ENABLED=1"
130+
fi
131+
golangci-lint fmt
132+
go mod tidy
133+
go mod verify
134+
go test ./...
135+
go test -race ./...
136+
CGO_ENABLED=0 GOOS=linux GOARCH=386 go test ./...
137+
golangci-lint run
138+
139+
if [ -n "$(git status --porcelain)" ]; then
140+
echo "Release checks modified the working directory:" >&2
141+
git status --short >&2
142+
git diff >&2
143+
exit 1
144+
fi
145+
146+
if [ "$(git rev-parse --verify HEAD)" != "$release_commit" ]; then
147+
die "HEAD changed during release checks"
148+
fi
149+
150+
echo
151+
echo "Version: $version"
152+
echo
153+
echo "Release notes:"
154+
printf '%s\n' "$notes"
155+
echo
156+
157+
read -r -p "Push $current_branch and create $tag in $repository? [y/N] " should_release
158+
if [[ ! $should_release =~ ^[Yy]$ ]]; then
159+
echo "Aborting"
160+
exit 1
161+
fi
162+
163+
notes_file=$(mktemp "${TMPDIR:-/tmp}/maxminddb-release-notes.XXXXXX")
164+
trap 'rm -f -- "$notes_file"' EXIT
165+
printf '%s\n' "$notes" >"$notes_file"
166+
167+
git push origin "$release_commit:refs/heads/$current_branch"
168+
169+
remote_tag=$(git ls-remote origin "refs/tags/$tag") ||
170+
die "could not recheck whether tag $tag exists on origin"
171+
if [ -n "$remote_tag" ]; then
172+
die "tag $tag appeared on origin during release checks"
173+
fi
174+
175+
gh release create \
176+
--repo "$repository" \
177+
--target "$release_commit" \
178+
--title "$version" \
179+
--notes-file "$notes_file" \
180+
"$tag"

0 commit comments

Comments
 (0)