Repository navigation
Expand file tree
/
Copy pathrelease.sh
More file actions
executable file
·200 lines (182 loc) · 10.6 KB
/
Copy pathrelease.sh
File metadata and controls
executable file
·200 lines (182 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
#!/usr/bin/env bash
# The release steps of CONTRIBUTING.md, one subcommand each. Releases go through a pull request like every
# other change to main; only the tag is pushed directly, and tags are not covered by the ruleset.
#
# etc/release.sh prepare 0.3.0 from an up-to-date main: create the branch release/0.3.0 and set the version
# in the POMs and README snippets, the release timestamp, and turn the
# Unreleased section of CHANGELOG.md into the release's. Nothing is committed:
# review the diff and write the lead paragraph of the section
# etc/release.sh propose [0.4.0] [--dry-run] on that branch: commit "Release 0.3.0", then set the next development version
# (0.4.0-SNAPSHOT, by default the next minor) and commit "Start 0.4.0", push
# the branch and open the pull request. Merge it with a merge commit or a
# rebase, not a squash: the release commit has to reach main as it is.
# --dry-run stops after the commits; running propose again pushes and opens
# the pull request, as it does after a push or pull request that failed
# etc/release.sh release [0.3.0] on main after the merge: create the signed tag v0.3.0 on the commit
# "Release 0.3.0" and push the tag, which starts the release workflow
#
# Needs git, the Maven wrapper, python3 (for the changelog) and gh (for the pull request and the workflow).
set -euo pipefail
cd "$(dirname "$0")/.."
fail() { echo "error: $*" >&2; exit 1; }
version_of_pom() {
./mvnw -q -N org.apache.maven.plugins:maven-help-plugin:3.5.1:evaluate -Dexpression=project.version -DforceStdout
}
semver() {
[[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "'$1' is not a version of the form X.Y.Z"
}
next_minor() {
local major minor
IFS=. read -r major minor _ <<< "$1"
echo "$major.$((minor + 1)).0"
}
require_clean() {
[ -z "$(git status --porcelain --untracked-files=no)" ] || fail "the working tree has changes, commit or stash them first"
}
require_up_to_date_main() {
[ "$(git branch --show-current)" = "main" ] || fail "run this on main (currently on $(git branch --show-current))"
git fetch -q origin
[ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ] || fail "main differs from origin/main, pull or push first"
}
prepare() {
local version=$1
semver "$version"
require_up_to_date_main
require_clean
local current; current=$(version_of_pom)
[[ "$current" == *-SNAPSHOT ]] || fail "the POMs are at $current, not at a snapshot"
grep -q '^## \[Unreleased\]' CHANGELOG.md || fail "CHANGELOG.md has no Unreleased section"
local branch="release/$version"
git rev-parse -q --verify "$branch" >/dev/null && fail "the branch $branch exists already"
git rev-parse -q --verify "refs/tags/v$version" >/dev/null && fail "the tag v$version exists already"
git checkout -q -b "$branch"
echo "setting the version $version (from $current) on $branch"
./mvnw -q versions:set -DnewVersion="$version" -DgenerateBackupPoms=false
local stamp; stamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
sed -i.bak "s|<project.build.outputTimestamp>[^<]*</project.build.outputTimestamp>|<project.build.outputTimestamp>$stamp</project.build.outputTimestamp>|" pom.xml && rm pom.xml.bak
# the dependency snippets of the easyssf artifacts; other artifacts (the Quarkus extension) keep theirs.
# The changelog carries the local date of the release, the POM timestamp UTC.
python3 - "$version" "$(date +%Y-%m-%d)" <<'PY'
import re, sys, pathlib
version, day = sys.argv[1], sys.argv[2]
for path in ['README.md', 'easyssf-receiver-spring-boot-starter/README.md']:
p = pathlib.Path(path)
s = p.read_text()
s = re.sub(r'(<artifactId>easyssf-[a-z-]+</artifactId>\n\s*<version>)[^<]+(</version>)', r'\g<1>' + version + r'\g<2>', s)
p.write_text(s)
p = pathlib.Path('CHANGELOG.md')
s = p.read_text()
start = s.index('## [Unreleased]\n')
end = s.index('\n## [', start + 1)
body = s[start + len('## [Unreleased]\n'):end].strip()
if not body:
sys.exit('error: the Unreleased section of CHANGELOG.md is empty, nothing to release')
s = s[:start] + f'## [Unreleased]\n\n## [{version}] - {day}\n\n' + body + '\n' + s[end:]
m = re.search(r'^\[Unreleased\]: (https://github\.com/[^/]+/[^/]+)/compare/v([^.]+\.[^.]+\.[^.]+)\.\.\.HEAD$', s, re.M)
if not m:
sys.exit('error: CHANGELOG.md has no [Unreleased] compare link')
repo, previous = m.group(1), m.group(2)
s = s.replace(m.group(0), f'[Unreleased]: {repo}/compare/v{version}...HEAD\n[{version}]: {repo}/compare/v{previous}...v{version}')
p.write_text(s)
print(f'CHANGELOG.md: Unreleased is now {version} ({day}), previous release v{previous}')
PY
echo
echo "prepared $version on $branch:"
git diff --stat | tail -1
grep -rnE '^\s*<version>[0-9][^<]*</version>' --include='*.md' . | grep -v target | grep -v "<version>$version</version>" \
| sed 's/^/ other dependency snippets, check by hand: /' || true
echo
echo "review the diff, write the lead paragraph of the new CHANGELOG section (it becomes the release notes),"
echo "then: etc/release.sh propose [$(next_minor "$version")]"
}
propose() {
local next="" dry_run=""
for arg in "$@"; do
case "$arg" in
--dry-run) dry_run=$arg ;;
*) next=$arg ;;
esac
done
local branch; branch=$(git branch --show-current)
[[ "$branch" == release/* ]] || fail "run this on the branch 'prepare' created (currently on $branch)"
local version=${branch#release/}
local pom_version; pom_version=$(version_of_pom)
grep -q "^## \[$version\] - " CHANGELOG.md || fail "CHANGELOG.md has no section for $version"
local notes; notes=$(release_notes "$version")
[ -n "$notes" ] || fail "the CHANGELOG section of $version is empty"
if [ -n "$(git status --porcelain --untracked-files=no)" ]; then
# the state 'prepare' left: commit the release and the next development version
[ "$pom_version" = "$version" ] || fail "the POMs are at $pom_version, the branch says $version"
next=${next:-$(next_minor "$version")}
semver "$next"
echo "$notes" | head -3 | grep -q '[a-z]' || echo "warning: the section of $version has no lead paragraph before its first heading"
git commit -q -s -am "Release $version"
./mvnw -q versions:set -DnewVersion="$next-SNAPSHOT" -DgenerateBackupPoms=false
git commit -q -s -am "Start $next"
echo "committed on $branch:"
git log --oneline -2 | sed 's/^/ /'
else
# committed already, by a dry run or a run whose push or pull request failed: pick up from there
[ "$(git log -1 --format=%s HEAD~1)" = "Release $version" ] \
|| fail "nothing to commit and the commit before HEAD is not 'Release $version'; was 'prepare' run?"
local started; started=$(git log -1 --format=%s HEAD)
[[ "$started" == "Start "* && "$pom_version" == "${started#Start }-SNAPSHOT" ]] \
|| fail "HEAD is '$started' but the POMs are at $pom_version; expected 'Start X.Y.Z' and X.Y.Z-SNAPSHOT"
[ -z "$next" ] || [ "$next" = "${started#Start }" ] || fail "the branch already starts ${started#Start }, not $next"
next=${started#Start }
echo "already committed on $branch:"
git log --oneline -2 | sed 's/^/ /'
fi
if [ "$dry_run" = "--dry-run" ]; then
echo "dry run: not pushing, not opening the pull request; run 'propose' again to do so"
return
fi
git push -u origin "$branch"
local existing; existing=$(gh pr list --head "$branch" --base main --state open --json url --jq '.[0].url // empty')
if [ -n "$existing" ]; then
echo "the pull request exists already: $existing"
else
local body
body=$(printf 'Release %s, then %s-SNAPSHOT.\n\n**Merge with a merge commit or a rebase, not a squash**: the commit "Release %s" has to reach `main` unchanged, `etc/release.sh release` tags it there.\n\nRelease notes, from CHANGELOG.md:\n\n%s' "$version" "$next" "$version" "$notes")
gh pr create --base main --head "$branch" --title "Release $version" --body "$body"
fi
echo
echo "after the merge, on main: etc/release.sh release $version"
}
release() {
local version=${1:-}
require_up_to_date_main
require_clean
if [ -z "$version" ]; then
# pipelines read to the end: an early exit would SIGPIPE git and, with pipefail, abort the script
version=$(git log --format=%s origin/main | awk '/^Release [0-9.]+$/ && !found { print substr($0, 9); found = 1 }')
[ -n "$version" ] || fail "no commit 'Release X.Y.Z' on main; name the version"
fi
semver "$version"
git rev-parse -q --verify "refs/tags/v$version" >/dev/null && fail "the tag v$version exists already"
local commit; commit=$(git log --format='%H %s' origin/main | awk -v s="Release $version" '$0 ~ " "s"$" && !found { print $1; found = 1 }')
[ -n "$commit" ] || fail "no commit 'Release $version' on main. Was the pull request squashed? Then main has no commit with the version $version to tag; restore the two commits of the release branch with a new pull request (git cherry-pick) and merge it without squashing"
local pom_version; pom_version=$(git show "$commit:pom.xml" | awk '/<version>[0-9.]+<\/version>/ && !found { sub(/.*<version>/, ""); sub(/<\/version>.*/, ""); print; found = 1 }')
[ "$pom_version" = "$version" ] || fail "the commit $commit carries the version $pom_version in pom.xml, not $version"
git show "$commit:CHANGELOG.md" | grep -q "^## \[$version\] - " || fail "CHANGELOG.md at $commit has no section for $version"
echo "tagging $(git log --oneline -1 "$commit") as v$version (signed) and pushing the tag"
git tag -s "v$version" -m "$version" "$commit"
git push origin "v$version"
echo "pushed; the release workflow builds, tests, signs and publishes $version"
if command -v gh >/dev/null; then
sleep 20
gh run watch "$(gh run list --workflow release.yml --limit 1 --json databaseId --jq '.[0].databaseId')" --exit-status \
&& echo "release $version published" || fail "the release workflow failed, see 'gh run view'"
fi
}
release_notes() {
# the section without its heading and the link definitions, trimmed of blank lines at both ends
awk -v v="$1" '/^## \[/ { in_section = ($0 ~ "^## \\[" v "\\]") ; next } in_section && !/^\[/ { print }' CHANGELOG.md \
| sed -e '/./,$!d' -e :a -e '/^\n*$/{$d;N;ba' -e '}'
}
case "${1:-}" in
prepare) [ $# -eq 2 ] || fail "usage: etc/release.sh prepare X.Y.Z"; prepare "$2" ;;
propose) shift; propose "$@" ;;
release) release "${2:-}" ;;
*) sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'; exit 1 ;;
esac