-
Notifications
You must be signed in to change notification settings - Fork 22
290 lines (273 loc) Β· 12.8 KB
/
Copy pathrelease.yml
File metadata and controls
290 lines (273 loc) Β· 12.8 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
name: Release
# Triggers on a v*.*.* tag push and performs the complete release: builds
# and signs the agent, smoke tests the exact jar that will ship, publishes
# it to Maven Central, and creates the GitHub Release from those same
# signed artifacts.
#
# Split into two jobs on purpose. Publishing to Central is irreversible
# and non-idempotent β the Portal rejects a re-upload of a version that
# already exists β so everything that can fail cheaply (build, smoke test,
# asset staging) runs *before* it, and everything after it lives in a
# separate job that can be re-run on its own without touching Central.
#
# This matters because the one release incident this repo has actually
# had, on v2.0.6, was exactly that shape: the Woodpecker deploy succeeded
# and the pipeline then failed in the repo1 wait step (a ${VERSION}
# templating bug, fixed in c1b96c1). v2.0.6 is on Central and is fine. Had
# the wait step been able to strand the release, that cosmetic failure
# would have been unrecoverable.
#
# workflow_dispatch is available as a manual recovery path; run it from
# the tag ref, not a branch.
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
permissions:
contents: write
# Never let two releases of the same ref overlap. cancel-in-progress is
# deliberately false: cancelling mid-deploy is the one thing that could
# strand a half-published version.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
publish:
name: Build, sign and publish to Maven Central
runs-on: ubuntu-latest
# The pom's central-publishing-maven-plugin blocks until Central
# reports the deployment PUBLISHED (waitMaxTime=7200, i.e. 120
# minutes), plus the build and smoke test ahead of it.
timeout-minutes: 150
outputs:
version: ${{ steps.ver.outputs.version }}
env:
# Public key id of the Keploy release signing key. Selects the right
# secret key when more than one is present in the keyring.
GPG_KEY_ID: 8541784E4EC36FB8
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve and validate release version
id: ver
# Runs before the signing key is imported, and rejects anything
# that is not a plain vMAJOR.MINOR.PATCH[-suffix] tag. The tag name
# is attacker-influenced (git accepts backticks and semicolons in
# ref names, and `v*.*.*` happily matches them), and it is
# interpolated into Maven arguments below.
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
# Hyphens are allowed inside the pre-release identifier (v2.1.0-alpha-1
# is legal and `v*.*.*` matches it). Leading zeros are not: 02.1.0 and
# 2.1.0 are distinct, permanent coordinates on Central.
if [[ ! "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$ ]]; then
echo "Refusing to release from ref '${GITHUB_REF_NAME}'." >&2
echo "Expected a tag of the form vMAJOR.MINOR.PATCH[-prerelease]," >&2
echo "with no leading zeros in the numeric components." >&2
exit 1
fi
# Central only accepts release versions, and a SNAPSHOT tag would
# otherwise burn a full build and smoke test before being rejected.
if [[ "$version" == *[Ss][Nn][Aa][Pp][Ss][Hh][Oo][Tt]* ]]; then
echo "Refusing to release a SNAPSHOT version: ${version}" >&2
exit 1
fi
echo "version=${version}" >> "$GITHUB_OUTPUT"
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
cache: maven
# Writes ~/.m2/settings.xml with a `central` server entry that
# resolves from the env vars named below, and imports the release
# signing key (removed again when the job ends). The key must be
# stored as a plain ASCII-armored block: unlike the Woodpecker
# script this replaces, setup-java has no base64 / escaped-newline
# fallback.
server-id: central
server-username: CENTRAL_USERNAME
server-password: CENTRAL_PASSWORD
gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
gpg-passphrase: MAVEN_GPG_PASSPHRASE
- name: Set release version in poms
# Pinned: an unqualified `versions:set` resolves the latest plugin
# from Central at run time, which would execute unreviewed code in
# the job that holds the signing key.
env:
VERSION: ${{ steps.ver.outputs.version }}
run: >-
mvn -B -ntp org.codehaus.mojo:versions-maven-plugin:2.21.0:set
-DnewVersion="$VERSION"
-DprocessAllModules=true
-DgenerateBackupPoms=false
- name: Build and sign
# The `release` profile attaches sources + javadoc and signs every
# artifact with maven-gpg-plugin.
env:
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
run: >-
mvn -B -ntp -P release -DskipTests clean verify
-Dgpg.keyname="$GPG_KEY_ID"
- name: Smoke test the release jar
# Runs against the artifact built above, which β because the deploy
# step below reuses this same target/ without cleaning β is the exact
# jar that gets published: on the second lifecycle pass `jar:jar` and
# `source:jar-no-fork` skip as up-to-date, so the agent jar and the
# sources jar are byte-identical to the ones tested here. (The javadoc
# jar is regenerated and differs by its embedded generation timestamp;
# it is content-equivalent and carries its own signature.)
run: ./scripts/smoke-javaagent.sh
- name: Stage release assets
# Deliberately ahead of the Central deploy: this is fiddly,
# failure-prone file shuffling, and it must not be able to fail
# after an irreversible publish.
env:
VERSION: ${{ steps.ver.outputs.version }}
run: |
set -euo pipefail
mkdir -p release-assets
cp "keploy-sdk/target/keploy-sdk-${VERSION}.jar" release-assets/
cp "keploy-sdk/target/keploy-sdk-${VERSION}-sources.jar" release-assets/
cp "keploy-sdk/target/keploy-sdk-${VERSION}-javadoc.jar" release-assets/
# Unversioned copy so docs and scripts can link a stable filename,
# with a matching signature so it is verifiable under that name too.
cp "keploy-sdk/target/keploy-sdk-${VERSION}.jar" "release-assets/keploy-sdk.jar"
cp "keploy-sdk/target/keploy-sdk-${VERSION}.jar.asc" "release-assets/keploy-sdk.jar.asc"
# The deployed poms are the source poms verbatim
# (createDependencyReducedPom=false), and the module pom declares
# the parent, so both are needed to consume these assets offline.
cp "keploy-sdk/pom.xml" "release-assets/keploy-sdk-${VERSION}.pom"
cp "pom.xml" "release-assets/java-sdk-${VERSION}.pom"
# Signatures. No nullglob: an unmatched glob must fail the job
# rather than quietly publish an unverifiable release.
cp keploy-sdk/target/*.asc release-assets/
cp target/*.asc release-assets/
# Belt and braces β assert every artifact shipped is signed.
missing=0
for f in release-assets/*.jar release-assets/*.pom; do
if [[ ! -f "${f}.asc" ]]; then
echo "Unsigned release asset: ${f}" >&2
missing=1
fi
done
[[ "$missing" -eq 0 ]] || exit 1
ls -la release-assets/
- name: Upload release assets
# Persisted so the github-release job below β and any later re-run
# of it β works from the exact artifacts that were validated here.
uses: actions/upload-artifact@v4
with:
name: release-assets
path: release-assets/
retention-days: 7
if-no-files-found: error
# v4 artifact names are immutable within a run and a partial re-run
# does not clear them, so without this a re-run of a failed publish
# job would 409 here β before it ever reached the deploy.
overwrite: true
- name: Check whether this version is already published
id: central
# Makes the deploy idempotent, which is what makes recovery work at
# all. Central rejects a re-upload of an existing version, so without
# this any whole-run retrigger β a re-pushed tag, "Re-run all jobs",
# or the workflow_dispatch below β would dead-end here and skip the
# github-release job forever, permanently stranding the release it
# was supposed to rescue.
env:
VERSION: ${{ steps.ver.outputs.version }}
run: |
set -euo pipefail
url="https://repo.maven.apache.org/maven2/io/keploy/keploy-sdk/${VERSION}/keploy-sdk-${VERSION}.jar"
if curl -fsI --max-time 30 "$url" >/dev/null; then
echo "already=true" >> "$GITHUB_OUTPUT"
echo "${VERSION} is already on Maven Central β skipping the deploy."
else
echo "already=false" >> "$GITHUB_OUTPUT"
fi
- name: Publish to Maven Central
# Last step in the job, and the point of no return. No `clean`: this
# reuses and republishes the artifacts smoke tested above.
if: steps.central.outputs.already != 'true'
env:
CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }}
CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
run: >-
mvn -B -ntp -P release -DskipTests deploy
-Dgpg.keyname="$GPG_KEY_ID"
github-release:
name: Publish GitHub Release
needs: publish
runs-on: ubuntu-latest
# Bounded by the repo1 poll below. Kept in its own job so that a slow
# Central sync fails here, re-runnably, instead of stranding a release.
timeout-minutes: 45
steps:
- name: Download release assets
uses: actions/download-artifact@v4
with:
name: release-assets
path: release-assets
- name: Wait for artifact on Maven Central
env:
VERSION: ${{ needs.publish.outputs.version }}
run: |
set -euo pipefail
artifact_url="https://repo.maven.apache.org/maven2/io/keploy/keploy-sdk/${VERSION}/keploy-sdk-${VERSION}.jar"
echo "Waiting for $artifact_url"
# Bounded by elapsed time, not by an iteration count: with
# `--max-time 30` on the request, 60 iterations of request+sleep
# could run for a full hour and be killed by the job timeout
# before the actionable message below ever printed.
deadline=$(( SECONDS + 1800 ))
attempt=0
while (( SECONDS < deadline )); do
attempt=$(( attempt + 1 ))
if curl -fsI --max-time 30 "$artifact_url" >/dev/null; then
echo "Released artifact is available on Maven Central"
exit 0
fi
echo "Artifact not available yet, retrying in 30s (attempt ${attempt})"
sleep 30
done
echo "Released artifact is not available on Maven Central after 30 minutes"
echo "The Central publish itself succeeded β re-run this job once it syncs."
exit 1
- name: Publish GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
SHA: ${{ github.sha }}
run: |
set -euo pipefail
# Pre-release flag: anything with a hyphen suffix (e.g. v2.0.6-rc1)
prerelease_flag=()
if [[ "$TAG" == *-* ]]; then
prerelease_flag=(--prerelease)
fi
if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then
# Idempotent re-run: --clobber overwrites partial assets so the
# job ends in the same state either way. A release left as a
# draft by an interrupted run is promoted, otherwise it would
# stay invisible forever.
echo "Release $TAG already exists β uploading assets with --clobber"
gh release upload "$TAG" --repo "$REPO" --clobber release-assets/*
if [[ "$(gh release view "$TAG" --repo "$REPO" --json isDraft -q .isDraft)" == "true" ]]; then
echo "Promoting draft release $TAG"
gh release edit "$TAG" --repo "$REPO" --draft=false
fi
else
gh release create "$TAG" \
--repo "$REPO" \
--title "Java SDK $TAG" \
--target "$SHA" \
--generate-notes \
"${prerelease_flag[@]}" \
release-assets/*
fi