Skip to content

Commit 83e54ee

Browse files
authored
1 parent d339397 commit 83e54ee

File tree

2 files changed

+317
-1
lines changed

2 files changed

+317
-1
lines changed

Diff for: pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494

9595
<properties>
9696
<!-- Dependencies -->
97-
<assertj.version>3.27.3</assertj.version>
97+
<assertj.version>4.0.0-M1</assertj.version>
9898
<awaitility.version>4.3.0</awaitility.version>
9999
<fuzzywuzzy.version>1.4.0</fuzzywuzzy.version>
100100
<jspecify.version>1.0.0</jspecify.version>

Diff for: scripts/close-nexus-repos.sh

+316
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
#!/usr/bin/env bash
2+
###
3+
### Script that helps automate Nexus auto-staging promotion without using the
4+
### Nexus Maven Plugin which will break when we exclude the acceptance tests from
5+
### being deployed.
6+
###
7+
### This script will:
8+
### 1. Find all open repositories under the current user account on Nexus.
9+
### 2. Filter out any non-"open" repositories.
10+
### 3. Find the repository that corresponds to the given group ID, artifact ID, and version.
11+
### 4. Invoke the close operation on the repository
12+
### 5. Wait for the close operation to end (or time out)
13+
### 6. Check that the close operation succeeded (i.e. all Nexus rules for POM content, signing,
14+
### artifact inclusion, documentation, etc are all green)
15+
### 7. Trigger a promotion (release to Maven Central) or drop (discard the release entirely).
16+
###
17+
### I have written this in such a way that I can hopefully reuse it elsewhere in the future.
18+
###
19+
### Note: this targets Sonatype Nexus Manager v2.x, not v3.x.
20+
###
21+
### Author: ascopes
22+
###
23+
set -o errexit
24+
set -o nounset
25+
[[ -n ${DEBUG+defined} ]] && set -o xtrace
26+
27+
function usage() {
28+
echo "USAGE: ${BASH_SOURCE[0]} [-h] -a <artifactId> -g <groupId> -v <version> -u <userName> -p <password> -s <server>"
29+
echo " -a <artifactId> The base artifact ID to use. This can be any artifact ID in the project"
30+
echo " and is only used to determine the correct staging repository on Nexus"
31+
echo " to deploy."
32+
echo " -d Drop rather than promote. Default is to promote."
33+
echo " -g <groupId> The group ID for the artifact ID to look for."
34+
echo " -v <version> The expected deployed version to look for."
35+
echo " -u <userName> The Nexus username to use."
36+
echo " -p <password> The Nexus password to use."
37+
echo " -s <server> The Nexus server to use."
38+
echo " -h Show this message and exit."
39+
echo
40+
}
41+
42+
artifact_id=""
43+
operation="promote"
44+
group_id=""
45+
version=""
46+
username=""
47+
password=""
48+
server=""
49+
50+
while getopts "a:dg:hp:s:u:v:" opt; do
51+
case "${opt}" in
52+
a)
53+
artifact_id="${OPTARG}"
54+
;;
55+
d)
56+
operation="drop"
57+
;;
58+
g)
59+
group_id="${OPTARG}"
60+
;;
61+
h)
62+
usage
63+
exit 0
64+
;;
65+
p)
66+
password="${OPTARG}"
67+
;;
68+
s)
69+
# Remove https:// or http:// at the start, remove trailing forward-slash
70+
# shellcheck disable=SC2001
71+
server="$(sed 's#^http://##g; s#https://##g; s#/$##g' <<<"${OPTARG}")"
72+
;;
73+
u)
74+
username="${OPTARG}"
75+
;;
76+
v)
77+
version="${OPTARG}"
78+
;;
79+
? | *)
80+
echo "ERROR: Unrecognised argument"
81+
usage
82+
exit 1
83+
;;
84+
esac
85+
done
86+
87+
for required_arg in artifact_id group_id password server username version; do
88+
if [[ -z "${!required_arg}" ]]; then
89+
echo "ERROR: Missing required argument: ${required_arg}" >&2
90+
usage
91+
exit 1
92+
fi
93+
done
94+
95+
for command in base64 curl jq; do
96+
if ! command -v "${command}" >/dev/null 2>&1; then
97+
echo "ERROR: ${command} is not on the \$PATH" >&2
98+
exit 2
99+
fi
100+
done
101+
102+
function print() {
103+
printf "%s" "${*}"
104+
}
105+
106+
function try-jq() {
107+
local file
108+
file="$(mktemp)"
109+
trap 'rm -f "${file}"' EXIT INT TERM
110+
111+
# pipe into file
112+
cat > "${file}"
113+
114+
if ! jq 2>&1 > /dev/null < "${file}"; then
115+
echo -e "\e[1;31mJQ failed to parse the HTTP response. Content was:\e[0m" >&2
116+
cat "${file}" >&2
117+
return 99
118+
fi
119+
120+
jq "${@}" < "${file}"
121+
}
122+
123+
function accept-json-header() {
124+
print "Accept: application/json"
125+
}
126+
127+
function authorization-header() {
128+
print "Authorization: Basic $(print "${username}:${password}" | base64)"
129+
}
130+
131+
function content-type-json-header() {
132+
print "Content-Type: application/json"
133+
}
134+
135+
function get-staging-repositories() {
136+
local url="https://${server}/service/local/staging/profile_repositories"
137+
echo -e "\e[1;33m[GET ${url}]\e[0m Retrieving repository IDs... (this may be slow) " >&2
138+
139+
if curl \
140+
-X GET \
141+
--fail \
142+
--silent \
143+
--header "$(accept-json-header)" \
144+
--header "$(authorization-header)" \
145+
"${url}" |
146+
try-jq -e -r '.data[] | select(.type == "open" or .type == "closed") | .repositoryId'; then
147+
148+
echo -e "\e[1;32mRetrieved all repository IDs successfully\e[0m" >&2
149+
return 0
150+
else
151+
echo -e "\e[1;31mFailed to retrieve the repository IDs\e[0m" >&2
152+
return 100
153+
fi
154+
}
155+
156+
function is-artifact-in-repository() {
157+
# Group ID has . replaced with /
158+
local path="${group_id//./\/}/${artifact_id}/${version}"
159+
local repository_id="${1?Pass the repository ID}"
160+
local url="https://${server}/service/local/repositories/${repository_id}/content/${path}/"
161+
162+
echo -e "\e[1;33m[GET ${url}]\e[0m" >&2
163+
if curl \
164+
-X GET \
165+
--fail \
166+
--silent \
167+
--header "$(accept-json-header)" \
168+
--header "$(authorization-header)" \
169+
"${url}" |
170+
try-jq '.' > /dev/null; then
171+
172+
echo -e "\e[1;32mFound artifact in repository ${repository_id}, will close this repository\e[0m" >&2
173+
return 0
174+
else
175+
echo -e "\e[1;31mArtifact is not present in repository ${repository_id}, skipping\e[0m" >&2
176+
return 101
177+
fi
178+
}
179+
180+
function find-correct-repository-id() {
181+
local repository_id
182+
for repository_id in $(get-staging-repositories); do
183+
if is-artifact-in-repository "${repository_id}"; then
184+
echo "${repository_id}"
185+
return 0
186+
fi
187+
done
188+
189+
echo -e "\e[1;31mERROR: Could not find the artifact in any open repositories\e[0m" >&2
190+
return 102
191+
}
192+
193+
function close-staging-repository() {
194+
local repository_id="${1?Pass the repository ID}"
195+
local url="https://${server}/service/local/staging/bulk/close"
196+
local payload
197+
198+
payload="$(
199+
jq -cn '{ data: { description: $description, stagedRepositoryIds: [ $repository_id ] } }' \
200+
--arg description "" \
201+
--arg repository_id "${repository_id}"
202+
)"
203+
204+
echo "Waiting a few seconds to mitigate eventual consistency on Nexus" >&2
205+
sleep 10
206+
207+
echo -e "\e[1;33m[POST ${url} ${payload}]\e[0m Triggering the closure process" >&2
208+
209+
if curl \
210+
-X POST \
211+
--fail \
212+
--silent \
213+
--header "$(accept-json-header)" \
214+
--header "$(content-type-json-header)" \
215+
--header "$(authorization-header)" \
216+
--data "${payload}" \
217+
"${url}"; then
218+
219+
echo -e "\e[1;32mStarted closure successfully\e[0m" >&2
220+
return 0
221+
else
222+
echo -e "\e[1;31mFailed to start closure\e[0m" >&2
223+
return 103
224+
fi
225+
}
226+
227+
function wait-for-closure-to-end() {
228+
local repository_id="${1?Pass the repository ID}"
229+
local url="https://${server}/service/local/staging/repository/${repository_id}/activity"
230+
231+
echo -e "\e[1;33m[GET ${url}]\e[0m Waiting for the repository to complete the closure process" >&2
232+
local attempt=1
233+
while true; do
234+
# In our case, the "close" activity will gain the attribute named "stopped" once the process
235+
# is over (we then need to check if it passed or failed separately).
236+
if curl \
237+
-X GET \
238+
--fail \
239+
--silent \
240+
--header "$(accept-json-header)" \
241+
--header "$(authorization-header)" \
242+
"${url}" |
243+
try-jq -e '.[] | select(.name == "close") | .stopped != null' >/dev/null; then
244+
245+
echo -e "\e[1;32mClosure process completed after ${attempt} attempts (@ $(date))}\e[0m" >&2
246+
return 0
247+
else
248+
echo -e "\e[1;32mStill waiting for closure to complete... - attempt $attempt (@ $(date))\e[0m" >&2
249+
((attempt++))
250+
fi
251+
sleep 5
252+
done
253+
}
254+
255+
function ensure-closure-succeeded() {
256+
local repository_id="${1?Pass the repository ID}"
257+
local url="https://${server}/service/local/staging/repository/${repository_id}/activity"
258+
259+
echo -e "\e[1;33m[GET ${url}]\e[0m Checking the closure process succeeded" >&2
260+
# Closure has succeeded if the "close" activity has an event named "repositoryClosed" somewhere.
261+
262+
if curl \
263+
-X GET \
264+
--fail \
265+
--silent \
266+
--header "$(accept-json-header)" \
267+
--header "$(authorization-header)" \
268+
"${url}" |
269+
try-jq -ce '.[] | select(.name == "close") | .events[] | select(.name == "repositoryClosed")'; then
270+
271+
echo -e "\e[1;32mRepository closed successfully\e[0m" >&2
272+
return 0
273+
else
274+
echo -e "\e[1;31mERROR: Repository failed to close, you should check this on the Nexus dashboard\e[0m" >&2
275+
return 105
276+
fi
277+
}
278+
279+
function trigger-drop-or-promote() {
280+
local repository_id="${1?Pass the repository ID}"
281+
local url="https://${server}/service/local/staging/bulk/${operation}"
282+
local payload
283+
payload="$(
284+
jq -cn '{ data: { description: $description, stagedRepositoryIds: [ $repository_id ] } }' \
285+
--arg description "" \
286+
--arg repository_id "${repository_id}"
287+
)"
288+
289+
echo -e "\e[1;33m[POST ${url} ${payload}]\e[0m ${operation^} the staging release" >&2
290+
291+
if curl \
292+
-X POST \
293+
--fail \
294+
--silent \
295+
--header "$(accept-json-header)" \
296+
--header "$(content-type-json-header)" \
297+
--header "$(authorization-header)" \
298+
--data "${payload}" \
299+
"${url}" |
300+
try-jq -ce '.'; then
301+
302+
echo -e "\e[1;32m${operation^} succeeded\e[0m" >&2
303+
return 0
304+
else
305+
echo -e "\e[1;31mERROR: ${operation^} failed\e[0m" >&2
306+
return 106
307+
fi
308+
}
309+
310+
repository_id="$(find-correct-repository-id)"
311+
close-staging-repository "${repository_id}"
312+
wait-for-closure-to-end "${repository_id}"
313+
ensure-closure-succeeded "${repository_id}"
314+
trigger-drop-or-promote "${repository_id}" || :
315+
316+
echo -e "\e[1;32mRelease ${operation} for repository ${repository_id} completed. Have a nice day :-)\e[0m" >&2

0 commit comments

Comments
 (0)