-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathrelease.sh
More file actions
executable file
·302 lines (269 loc) · 12.5 KB
/
Copy pathrelease.sh
File metadata and controls
executable file
·302 lines (269 loc) · 12.5 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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage: $0 [--dry-run] <RELEASE_VERSION> <NEXT_DEV_VERSION>"
echo
echo "Arguments:"
echo " RELEASE_VERSION The version to release (e.g., 0.5.12)"
echo " NEXT_DEV_VERSION The next development version (e.g., 0.5.13-SNAPSHOT)"
echo
echo "Options:"
echo " --dry-run Show what would be done without making any changes"
exit 1
}
DRY_RUN=false
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
DRY_RUN=true
shift
;;
-h|--help)
usage
;;
-*)
echo "Error: Unknown option $1"
usage
;;
*)
break
;;
esac
done
if [[ $# -ne 2 ]]; then
echo "Error: Expected 2 arguments, got $#"
usage
fi
RELEASE_VERSION="$1"
NEXT_DEV_VERSION="$2"
if "${DRY_RUN}"; then
echo "[DRY RUN] No changes will be made."
echo
fi
# Verify that required tools are available
for cmd in git sed python3; do
if ! command -v "$cmd" &>/dev/null; then
echo "Error: Required command '$cmd' is not installed."
exit 1
fi
done
echo "✅ Required tools (git, sed, python3) are available."
# Verify that the Maven wrapper is available and executable
if [[ ! -x "./mvnw" ]]; then
echo "Error: Maven wrapper (mvnw) not found or not executable."
exit 1
fi
echo "✅ Maven wrapper (mvnw) is available and executable."
# Verify that RELEASE_VERSION is in semver format
if ! [[ "${RELEASE_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: RELEASE_VERSION must be in semver format (e.g., 0.5.12)"
exit 1
fi
echo "✅ Release version (${RELEASE_VERSION}) is in valid semver format."
# Verify that NEXT_DEV_VERSION is in semver format with -SNAPSHOT suffix
if ! [[ "${NEXT_DEV_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-SNAPSHOT$ ]]; then
echo "Error: NEXT_DEV_VERSION must be in semver format with -SNAPSHOT suffix (e.g., 0.5.13-SNAPSHOT)"
exit 1
fi
echo "✅ Next development version (${NEXT_DEV_VERSION}) is in valid semver-SNAPSHOT format."
# Verify that NEXT_DEV_VERSION is greater than RELEASE_VERSION
NEXT_DEV_BASE="${NEXT_DEV_VERSION%-SNAPSHOT}"
if [[ "$(printf '%s\n' "${RELEASE_VERSION}" "${NEXT_DEV_BASE}" | sort -V | tail -1)" != "${NEXT_DEV_BASE}" ]] \
|| [[ "${RELEASE_VERSION}" == "${NEXT_DEV_BASE}" ]]; then
echo "Error: Next development version (${NEXT_DEV_VERSION}) must be greater than release version (${RELEASE_VERSION})"
exit 1
fi
echo "✅ Next development version (${NEXT_DEV_VERSION}) is greater than release version (${RELEASE_VERSION})."
# Verify that commit/tag signing is properly configured.
# The release commit and tag are signed (git commit -S / git tag -s), which
# delegates to git's configured signing backend. Honor gpg.format so both
# OpenPGP (default) and SSH signing (e.g. Secretive / 1Password) work.
SIGNING_KEY=$(git config --get user.signingkey 2>/dev/null || true)
if [[ -z "${SIGNING_KEY}" ]]; then
echo "Error: No signing key configured. Please set one with: git config --global user.signingkey <KEY>"
exit 1
fi
SIGNING_FORMAT=$(git config --get gpg.format 2>/dev/null || echo "openpgp")
case "${SIGNING_FORMAT}" in
ssh)
# SSH signing delegates to gpg.ssh.program (defaults to ssh-keygen).
SSH_SIGN_PROGRAM=$(git config --get gpg.ssh.program 2>/dev/null || echo "ssh-keygen")
if ! command -v "${SSH_SIGN_PROGRAM%% *}" &>/dev/null; then
echo "Error: SSH signing program '${SSH_SIGN_PROGRAM}' is not installed (needed for gpg.format=ssh)."
exit 1
fi
# When user.signingkey is a key file path, make sure the file exists.
resolved_key="${SIGNING_KEY/#\~/${HOME}}"
if [[ "${resolved_key}" == /* || "${resolved_key}" == ./* ]] && [[ ! -f "${resolved_key}" ]]; then
echo "Error: SSH signing key file '${SIGNING_KEY}' does not exist."
exit 1
fi
echo "✅ SSH signing key (${SIGNING_KEY}) is configured."
;;
openpgp|"")
if ! command -v gpg &>/dev/null; then
echo "Error: Required command 'gpg' is not installed (needed for OpenPGP signing)."
exit 1
fi
if ! gpg --list-secret-keys "${SIGNING_KEY}" &>/dev/null; then
echo "Error: GPG secret key '${SIGNING_KEY}' is not available. Please ensure the key is imported and accessible."
exit 1
fi
echo "✅ GPG signing key (${SIGNING_KEY}) is configured and available."
;;
*)
echo "Error: Unsupported gpg.format '${SIGNING_FORMAT}'. Supported values: openpgp, ssh."
exit 1
;;
esac
# Verify that the git working directory is clean (except for untracked files)
if [[ -n "$(git status --porcelain --untracked-files=no)" ]]; then
echo "Error: Git working directory is not clean. Please commit or stash your changes before running this script."
exit 1
fi
echo "✅ Git working directory is clean."
# Verify that the current branch is main
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [[ "${CURRENT_BRANCH}" != "main" ]]; then
echo "Error: Current branch is ${CURRENT_BRANCH}. Please switch to main branch before running this script."
exit 1
fi
echo "✅ Current branch is main."
# Verify that the local main branch is up to date (or ahead without diverging) with origin/main
git fetch -q origin main
LOCAL_COMMIT=$(git rev-parse main)
REMOTE_COMMIT=$(git rev-parse origin/main)
if [[ "${LOCAL_COMMIT}" != "${REMOTE_COMMIT}" ]]; then
if git merge-base --is-ancestor "${LOCAL_COMMIT}" "${REMOTE_COMMIT}"; then
echo "Error: Local main branch is behind origin/main. Please pull the latest changes before running this script."
exit 1
elif git merge-base --is-ancestor "${REMOTE_COMMIT}" "${LOCAL_COMMIT}"; then
echo "Local main branch is ahead of origin/main. Changes will be pushed along with this script."
else
echo "Error: Local main branch has diverged from origin/main. Please resolve the divergence before running this script."
exit 1
fi
fi
echo "✅ Local main branch is up to date with origin/main."
# Verify that the current POM version is the SNAPSHOT of the release version
CURRENT_POM_VERSION=$(./mvnw -q -DforceStdout help:evaluate -Dexpression=project.version)
if [[ "${CURRENT_POM_VERSION}" != "${RELEASE_VERSION}-SNAPSHOT" ]]; then
echo "Error: Current POM version is ${CURRENT_POM_VERSION}, expected ${RELEASE_VERSION}-SNAPSHOT"
exit 1
fi
echo "✅ Current POM version (${CURRENT_POM_VERSION}) matches expected SNAPSHOT version."
# Verify that the tag for the release version does not already exist
if git rev-parse "v${RELEASE_VERSION}" >/dev/null 2>&1; then
echo "Error: Tag v${RELEASE_VERSION} already exists. Please choose a different release version or delete the existing tag before running this script."
exit 1
fi
echo "✅ Tag v${RELEASE_VERSION} does not already exist."
# Verify that the release version is not already in use in the pom.xml files
for pom in pom.xml api/pom.xml cli/pom.xml maven-plugin/pom.xml; do
pom_version=$(./mvnw -q -DforceStdout help:evaluate -Dexpression=project.version -f "${pom}")
if [[ "${pom_version}" == "${RELEASE_VERSION}" ]]; then
echo "Error: Release version ${RELEASE_VERSION} is already set in ${pom}. Please choose a different release version or update the pom.xml files before running this script."
exit 1
fi
if [[ "${pom_version}" == "${NEXT_DEV_VERSION}" ]]; then
echo "Error: New development version ${NEXT_DEV_VERSION} is already set in ${pom}. Please choose a different new development version or update the pom.xml files before running this script."
exit 1
fi
done
echo "✅ Release and development versions are not already in use in pom.xml files."
# Verify that the release version is not set in the CHANGELOG.md file
if grep -q "## \[${RELEASE_VERSION}\]" CHANGELOG.md || grep -q "## \[v${RELEASE_VERSION}\]" CHANGELOG.md; then
echo "Error: Release version ${RELEASE_VERSION} is already in use in the CHANGELOG.md file. Please choose a different release version or update the CHANGELOG.md file before running this script."
exit 1
fi
echo "✅ Release version ${RELEASE_VERSION} is not already in CHANGELOG.md."
# Verify that there is an unreleased section in the CHANGELOG.md file
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
echo "Error: No unreleased section found in CHANGELOG.md file. Please add an unreleased section before running this script."
exit 1
fi
echo "✅ CHANGELOG.md has an [Unreleased] section."
# Verify that the unreleased section has content
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/{/## \[/!p;}' CHANGELOG.md | grep -v '^[[:space:]]*$' || true)
if [[ -z "${UNRELEASED_CONTENT}" ]]; then
echo "Error: The [Unreleased] section in CHANGELOG.md has no content. Please add changelog entries before releasing."
exit 1
fi
echo "✅ [Unreleased] section in CHANGELOG.md has content."
# Verify that the [Unreleased] section renders as release notes. The GitHub release body is
# produced from it by .github/scripts/render-release-notes.py, so a problem here would
# otherwise only surface in CI, after the tag has already been pushed. GPG_KEY_ID must match
# the repository secret of the same name: it is the artifact signing key advertised to users
# in the notes, which is not necessarily the key git signs commits with.
if [[ -z "${GPG_KEY_ID:-}" ]]; then
echo "Error: GPG_KEY_ID is not set. Export the OpenPGP key that signs the release artifacts (same value as the GPG_KEY_ID repository secret) before running this script."
exit 1
fi
if ! python3 .github/scripts/render-release-notes.py --unreleased --check; then
echo "Error: Could not render release notes from the [Unreleased] section of CHANGELOG.md."
exit 1
fi
echo "✅ Release notes render from the [Unreleased] section."
# Verify that the SBOM scanning tools are available (matches release.yml)
for cmd in trivy cdxgen cyclonedx; do
if ! command -v "$cmd" &>/dev/null; then
echo "Error: Required command '$cmd' is not installed (needed for the SBOM vulnerability scan)."
exit 1
fi
done
echo "✅ SBOM scanning tools (trivy, cdxgen, cyclonedx) are available."
# Generate the aggregate SBOM and scan it for known HIGH/CRITICAL vulnerabilities,
# mirroring the "Scan SBOM for vulnerabilities" step in .github/workflows/release.yml
# so issues surface here instead of failing the release pipeline.
echo "Generating aggregate SBOM (this may take a few minutes)..."
./mvnw -ntp -P generate-sbom -DskipTests package
if [[ ! -f target/bom_all.json ]]; then
echo "Error: Aggregate SBOM (target/bom_all.json) was not generated."
exit 1
fi
echo "Scanning aggregate SBOM for HIGH/CRITICAL vulnerabilities with Trivy..."
trivy sbom --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 --format table target/bom_all.json
echo "✅ No HIGH/CRITICAL vulnerabilities found in aggregate SBOM."
# Confirm before proceeding (unless dry-run)
if ! "${DRY_RUN}"; then
echo "About to release ${RELEASE_VERSION} and start development of ${NEXT_DEV_VERSION}."
read -rp "Continue? [y/N] " confirm
if [[ ! "${confirm}" =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 0
fi
fi
run() {
if "${DRY_RUN}"; then
echo "[DRY RUN] $*"
else
"$@"
fi
}
sed_inplace() {
local file="${!#}"
local tmp
tmp=$(mktemp)
sed "$@" > "$tmp" && mv "$tmp" "$file"
}
# Rename the unreleased section in the CHANGELOG.md file to the release version with today's date
RELEASE_DATE=$(date +%Y-%m-%d)
run sed_inplace "s/## \[Unreleased\]/## [v${RELEASE_VERSION}] - ${RELEASE_DATE}/g" CHANGELOG.md
run ./mvnw versions:set -DnewVersion="${RELEASE_VERSION}" -DgenerateBackupPoms=false -f pom.xml
run git add CHANGELOG.md api/pom.xml cli/pom.xml maven-plugin/pom.xml pom.xml
run git commit -s -S -m "Release version ${RELEASE_VERSION}"
run git tag -s "v${RELEASE_VERSION}" -m "Release version ${RELEASE_VERSION}"
# Add unreleased section in the CHANGELOG.md file as the first h2 in the file.
# There is always a h1 at the top of the file, so it needs to be added after the first h1 and its content.
run sed_inplace "0,/## /s/## /## [Unreleased]\n\n## /" CHANGELOG.md
# Set the new development version in the pom.xml files and commit the changes
run ./mvnw versions:set -DnewVersion="${NEXT_DEV_VERSION}" -DgenerateBackupPoms=false -f pom.xml
run git add CHANGELOG.md api/pom.xml cli/pom.xml maven-plugin/pom.xml pom.xml
run git commit -s -S -m "Start development of version ${NEXT_DEV_VERSION}"
# Push everything at the end: main once, carrying both commits, so CI sees a single
# push instead of two (the release commit alone is a non-SNAPSHOT no-op for the site).
# The tag goes last so the Release workflow only starts once main already describes the
# next development version.
run git push origin main
run git push origin "v${RELEASE_VERSION}"
echo "Release ${RELEASE_VERSION} created and pushed to origin."