Skip to content

Commit f00150c

Browse files
author
Fabiana Severin
committed
refactor(release): reusable CI actions and review hardening
- Extract resolve-release-version and configure-release-aws-credentials composite actions shared by both release workflows - Fetch signing key + Sonatype token inside the publish step so secrets never cross $GITHUB_ENV; scrub settings.xml and keyring on exit - Guard releases to the main branch; cut OIDC role session to 300s - Set RIC to 2.12.0-SNAPSHOT so the release pipeline can publish it
1 parent e3223b5 commit f00150c

5 files changed

Lines changed: 166 additions & 93 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: "Configure AWS credentials for release (OIDC)"
2+
description: >
3+
Assumes the release OIDC role via aws-actions/configure-aws-credentials so the
4+
job can read the signing key and Sonatype token from Secrets Manager. Pinning
5+
of the underlying action lives here so it is updated in one place.
6+
7+
inputs:
8+
aws-region:
9+
description: "AWS region to operate in."
10+
required: true
11+
role-to-assume:
12+
description: "ARN of the OIDC role to assume."
13+
required: true
14+
role-session-name:
15+
description: "Session name for the assumed role (helps distinguish callers in CloudTrail)."
16+
required: true
17+
18+
runs:
19+
using: composite
20+
steps:
21+
- uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4
22+
with:
23+
aws-region: ${{ inputs.aws-region }}
24+
role-to-assume: ${{ inputs.role-to-assume }}
25+
role-session-name: ${{ inputs.role-session-name }}
26+
# Short-lived: the job only needs the role briefly to read two secrets.
27+
role-duration-seconds: 300
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: "Resolve and validate release version"
2+
description: >
3+
Reads the module POM version (the source of truth), verifies it is a
4+
-SNAPSHOT, and derives the effective release version (the optional override,
5+
or the POM version with -SNAPSHOT stripped). Exports CURRENT_VERSION and
6+
EFFECTIVE_RELEASE_VERSION to the job environment for subsequent steps.
7+
8+
inputs:
9+
module:
10+
description: "Module directory containing the pom.xml to release."
11+
required: true
12+
release-version-override:
13+
description: "Optional release version; defaults to the POM version without -SNAPSHOT."
14+
required: false
15+
default: ""
16+
validate-module-dir:
17+
description: "Fail if the module directory or its pom.xml is missing (use for the choice-driven workflow)."
18+
required: false
19+
default: "false"
20+
21+
runs:
22+
using: composite
23+
steps:
24+
- name: Resolve and validate release version
25+
shell: bash
26+
env:
27+
MODULE: ${{ inputs.module }}
28+
RELEASE_VERSION_OVERRIDE: ${{ inputs.release-version-override }}
29+
VALIDATE_MODULE_DIR: ${{ inputs.validate-module-dir }}
30+
run: |
31+
if [[ "$VALIDATE_MODULE_DIR" == "true" ]]; then
32+
if [[ ! -d "$MODULE" ]]; then
33+
echo "::error::Module directory '$MODULE' does not exist"
34+
exit 1
35+
fi
36+
if [[ ! -f "$MODULE/pom.xml" ]]; then
37+
echo "::error::No pom.xml found in '$MODULE'"
38+
exit 1
39+
fi
40+
fi
41+
42+
# The POM version is the source of truth and must be a SNAPSHOT.
43+
CURRENT_VERSION=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version --file "$MODULE/pom.xml")
44+
CURRENT_VERSION="${CURRENT_VERSION//[$'\r\n']/}"
45+
if [[ "$CURRENT_VERSION" != *-SNAPSHOT ]]; then
46+
echo "::error::POM version '$CURRENT_VERSION' is not a SNAPSHOT"
47+
exit 1
48+
fi
49+
50+
# Optional override; default strips -SNAPSHOT.
51+
EFFECTIVE_RELEASE_VERSION="${RELEASE_VERSION_OVERRIDE:-${CURRENT_VERSION%-SNAPSHOT}}"
52+
53+
echo "CURRENT_VERSION=$CURRENT_VERSION" >> "$GITHUB_ENV"
54+
echo "EFFECTIVE_RELEASE_VERSION=$EFFECTIVE_RELEASE_VERSION" >> "$GITHUB_ENV"

.github/workflows/release-runtime-interface-client.yml

Lines changed: 41 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ jobs:
5757
runs-on: ${{ matrix.runner }}
5858
timeout-minutes: 45
5959
steps:
60+
# Manual (workflow_dispatch) releases must only run from main, never from
61+
# an arbitrary branch that could carry unreviewed release logic. Guarding
62+
# the first job blocks the whole pipeline (release needs build-natives).
63+
- name: Verify release branch
64+
run: |
65+
if [[ "$GITHUB_REF_NAME" != "main" ]]; then
66+
echo "::error::Releases must run from the main branch, got '$GITHUB_REF_NAME'"
67+
exit 1
68+
fi
69+
6070
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
6171

6272
- name: Set up JDK 8
@@ -66,15 +76,11 @@ jobs:
6676
distribution: corretto
6777
cache: maven
6878

69-
- name: Resolve release version
70-
run: |
71-
CURRENT_VERSION=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version --file "$MODULE/pom.xml")
72-
CURRENT_VERSION="${CURRENT_VERSION//[$'\r\n']/}"
73-
if [[ "$CURRENT_VERSION" != *-SNAPSHOT ]]; then
74-
echo "::error::POM version '$CURRENT_VERSION' is not a SNAPSHOT"
75-
exit 1
76-
fi
77-
echo "EFFECTIVE_RELEASE_VERSION=${RELEASE_VERSION_INPUT:-${CURRENT_VERSION%-SNAPSHOT}}" >> "$GITHUB_ENV"
79+
- name: Resolve and validate release version
80+
uses: ./.github/actions/resolve-release-version
81+
with:
82+
module: ${{ env.MODULE }}
83+
release-version-override: ${{ env.RELEASE_VERSION_INPUT }}
7884

7985
# -DskipTests: only installed so the module compiles, not released here.
8086
- name: Install intra-repo dependencies
@@ -124,17 +130,14 @@ jobs:
124130
distribution: corretto
125131
cache: maven
126132

127-
- name: Validate inputs and resolve versions
128-
run: |
129-
CURRENT_VERSION=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version --file "$MODULE/pom.xml")
130-
CURRENT_VERSION="${CURRENT_VERSION//[$'\r\n']/}"
131-
if [[ "$CURRENT_VERSION" != *-SNAPSHOT ]]; then
132-
echo "::error::POM version '$CURRENT_VERSION' is not a SNAPSHOT"
133-
exit 1
134-
fi
135-
136-
EFFECTIVE_RELEASE_VERSION="${RELEASE_VERSION_INPUT:-${CURRENT_VERSION%-SNAPSHOT}}"
133+
- name: Resolve and validate release version
134+
uses: ./.github/actions/resolve-release-version
135+
with:
136+
module: ${{ env.MODULE }}
137+
release-version-override: ${{ env.RELEASE_VERSION_INPUT }}
137138

139+
- name: Resolve next development version and tag
140+
run: |
138141
# Next development version: use the override, or bump the patch.
139142
if [[ -n "$DEVELOPMENT_VERSION_INPUT" ]]; then
140143
if [[ "$DEVELOPMENT_VERSION_INPUT" != *-SNAPSHOT ]]; then
@@ -147,7 +150,6 @@ jobs:
147150
NEXT_DEV_VERSION="${MA}.${MI}.$((PA + 1))-SNAPSHOT"
148151
fi
149152
150-
echo "EFFECTIVE_RELEASE_VERSION=$EFFECTIVE_RELEASE_VERSION" >> "$GITHUB_ENV"
151153
echo "NEXT_DEV_VERSION=$NEXT_DEV_VERSION" >> "$GITHUB_ENV"
152154
echo "TAG_NAME=${MODULE}-${EFFECTIVE_RELEASE_VERSION}" >> "$GITHUB_ENV"
153155
echo "::notice::Releasing $MODULE $EFFECTIVE_RELEASE_VERSION (next dev $NEXT_DEV_VERSION)"
@@ -190,20 +192,32 @@ jobs:
190192
191193
- name: Configure AWS credentials (OIDC)
192194
if: ${{ github.event.inputs.skip_publish != 'true' }}
193-
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4
195+
uses: ./.github/actions/configure-release-aws-credentials
194196
with:
195197
aws-region: ${{ env.AWS_REGION }}
196198
role-to-assume: ${{ env.OIDC_ROLE_ARN }}
197199
role-session-name: GitHubActionsRicMavenCentralRelease
198-
role-duration-seconds: 3600
199200

200-
- name: Fetch signing key and Sonatype credentials
201+
# Fetch signing material and publish in a single step so the GPG passphrase
202+
# and Sonatype token stay in this shell and never cross a $GITHUB_ENV
203+
# boundary, where a later (possibly compromised) step could read them.
204+
# -DmultiArch=false builds only the host .so; the aarch_64 .so is already
205+
# staged, so the main JAR still bundles all four. build-helper attaches
206+
# the staged classifier JARs. Gate already ran, so -DskipTests.
207+
- name: Publish to Maven Central
201208
if: ${{ github.event.inputs.skip_publish != 'true' }}
209+
env:
210+
IS_JAVA_8: true
202211
run: |
203-
# Shared secrets from LambdaMavenDeploy; nothing stored in GitHub.
212+
# Scrub the settings.xml (contains the Sonatype token) and the keyring
213+
# on exit, so no sensitive file is left on the runner even on failure.
214+
MAVEN_SETTINGS="$RUNNER_TEMP/settings.xml"
215+
export GNUPGHOME=$(mktemp -d)
216+
trap 'rm -rf "$MAVEN_SETTINGS" "$GNUPGHOME"' EXIT
217+
218+
# --- Signing key + Sonatype token (shared secrets from LambdaMavenDeploy) ---
204219
GPG_JSON=$(aws secretsmanager get-secret-value --secret-id maven.gpg.keys --query SecretString --output text)
205220
CREDS_JSON=$(aws secretsmanager get-secret-value --secret-id maven.sonatype.creds --query SecretString --output text)
206-
207221
GPG_PRIVATE_KEY=$(jq -r '.private' <<< "$GPG_JSON")
208222
GPG_PASSPHRASE=$(jq -r '.passphrase' <<< "$GPG_JSON")
209223
SONATYPE_USERNAME=$(jq -r '."maven-central-login"' <<< "$CREDS_JSON")
@@ -213,38 +227,23 @@ jobs:
213227
echo "::add-mask::$SONATYPE_PASSWORD"
214228
215229
# Import the key with loopback pinentry so Maven can sign non-interactively.
216-
GNUPGHOME=$(mktemp -d)
217230
chmod 700 "$GNUPGHOME"
218231
echo "allow-loopback-pinentry" > "$GNUPGHOME/gpg-agent.conf"
219232
echo "pinentry-mode loopback" > "$GNUPGHOME/gpg.conf"
220-
export GNUPGHOME
221233
gpgconf --kill gpg-agent || true
222234
gpg --batch --import <<< "$GPG_PRIVATE_KEY"
223235
GPG_KEYNAME=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/ {print $5; exit}')
224236
225237
# settings.xml with the Sonatype token (server id "central").
226-
SETTINGS="$RUNNER_TEMP/settings.xml"
227238
{
228239
echo '<settings><servers><server>'
229240
echo "<id>central</id>"
230241
echo "<username>${SONATYPE_USERNAME}</username>"
231242
echo "<password>${SONATYPE_PASSWORD}</password>"
232243
echo '</server></servers></settings>'
233-
} > "$SETTINGS"
244+
} > "$MAVEN_SETTINGS"
234245
235-
echo "GNUPGHOME=$GNUPGHOME" >> "$GITHUB_ENV"
236-
echo "GPG_KEYNAME=$GPG_KEYNAME" >> "$GITHUB_ENV"
237-
echo "GPG_PASSPHRASE=$GPG_PASSPHRASE" >> "$GITHUB_ENV"
238-
echo "MAVEN_SETTINGS=$SETTINGS" >> "$GITHUB_ENV"
239-
240-
# -DmultiArch=false builds only the host .so; the aarch_64 .so is already
241-
# staged, so the main JAR still bundles all four. build-helper attaches
242-
# the staged classifier JARs. Gate already ran, so -DskipTests.
243-
- name: Publish to Maven Central
244-
if: ${{ github.event.inputs.skip_publish != 'true' }}
245-
env:
246-
IS_JAVA_8: true
247-
run: |
246+
# --- Publish ---
248247
mvn deploy -Prelease -DskipTests -DmultiArch=false \
249248
-s "$MAVEN_SETTINGS" \
250249
-Dgpg.keyname="$GPG_KEYNAME" -Dgpg.passphrase="$GPG_PASSPHRASE" \

.github/workflows/release.yml

Lines changed: 43 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ jobs:
5959
timeout-minutes: 30
6060

6161
steps:
62+
# Manual (workflow_dispatch) releases must only run from main, never from
63+
# an arbitrary branch that could carry unreviewed release logic.
64+
- name: Verify release branch
65+
run: |
66+
if [[ "$GITHUB_REF_NAME" != "main" ]]; then
67+
echo "::error::Releases must run from the main branch, got '$GITHUB_REF_NAME'"
68+
exit 1
69+
fi
70+
6271
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
6372
with:
6473
fetch-depth: 0 # full history for tagging/pushing
@@ -71,41 +80,19 @@ jobs:
7180
distribution: corretto
7281
cache: maven
7382

74-
- name: Validate inputs and resolve versions
75-
run: |
76-
if [[ ! -d "$MODULE" ]]; then
77-
echo "::error::Module directory '$MODULE' does not exist"
78-
exit 1
79-
fi
80-
if [[ ! -f "$MODULE/pom.xml" ]]; then
81-
echo "::error::No pom.xml found in '$MODULE'"
82-
exit 1
83-
fi
84-
85-
# The POM version is the source of truth and must be a SNAPSHOT.
86-
CURRENT_VERSION=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version --file "$MODULE/pom.xml")
87-
CURRENT_VERSION="${CURRENT_VERSION//[$'\r\n']/}"
88-
if [[ "$CURRENT_VERSION" != *-SNAPSHOT ]]; then
89-
echo "::error::POM version '$CURRENT_VERSION' is not a SNAPSHOT"
90-
exit 1
91-
fi
92-
93-
# releaseVersion input is an optional override; default strips -SNAPSHOT.
94-
EFFECTIVE_RELEASE_VERSION="${RELEASE_VERSION_INPUT:-${CURRENT_VERSION%-SNAPSHOT}}"
83+
- name: Resolve and validate release version
84+
uses: ./.github/actions/resolve-release-version
85+
with:
86+
module: ${{ env.MODULE }}
87+
release-version-override: ${{ env.RELEASE_VERSION_INPUT }}
88+
validate-module-dir: "true"
9589

90+
- name: Validate development version override
91+
run: |
9692
if [[ -n "$DEVELOPMENT_VERSION_INPUT" && "$DEVELOPMENT_VERSION_INPUT" != *-SNAPSHOT ]]; then
9793
echo "::error::developmentVersion '$DEVELOPMENT_VERSION_INPUT' must end with -SNAPSHOT"
9894
exit 1
9995
fi
100-
101-
# Build the release plugin version args once; reused by both paths.
102-
RELEASE_ARGS="-DreleaseVersion=$EFFECTIVE_RELEASE_VERSION"
103-
if [[ -n "$DEVELOPMENT_VERSION_INPUT" ]]; then
104-
RELEASE_ARGS="$RELEASE_ARGS -DdevelopmentVersion=$DEVELOPMENT_VERSION_INPUT"
105-
fi
106-
107-
echo "EFFECTIVE_RELEASE_VERSION=$EFFECTIVE_RELEASE_VERSION" >> "$GITHUB_ENV"
108-
echo "RELEASE_ARGS=$RELEASE_ARGS" >> "$GITHUB_ENV"
10996
echo "::notice::Releasing $MODULE $EFFECTIVE_RELEASE_VERSION (POM currently $CURRENT_VERSION)"
11097
11198
- name: Configure git user
@@ -141,20 +128,28 @@ jobs:
141128

142129
- name: Configure AWS credentials (OIDC)
143130
if: ${{ github.event.inputs.skip_publish != 'true' }}
144-
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4
131+
uses: ./.github/actions/configure-release-aws-credentials
145132
with:
146133
aws-region: ${{ env.AWS_REGION }}
147134
role-to-assume: ${{ env.OIDC_ROLE_ARN }}
148135
role-session-name: GitHubActionsMavenCentralRelease
149-
role-duration-seconds: 3600
150136

151-
- name: Fetch signing key and Sonatype credentials
137+
# Fetch signing material and publish in a single step so the GPG passphrase
138+
# and Sonatype token stay in this shell and never cross a $GITHUB_ENV
139+
# boundary, where a later (possibly compromised) step could read them.
140+
# prepare/perform aren't atomic: prepare locally, publish, push only after.
141+
- name: Release (prepare locally, publish, then push)
152142
if: ${{ github.event.inputs.skip_publish != 'true' }}
153143
run: |
154-
# Shared secrets from LambdaMavenDeploy; nothing stored in GitHub.
144+
# Scrub the settings.xml (contains the Sonatype token) and the keyring
145+
# on exit, so no sensitive file is left on the runner even on failure.
146+
MAVEN_SETTINGS="$RUNNER_TEMP/settings.xml"
147+
export GNUPGHOME=$(mktemp -d)
148+
trap 'rm -rf "$MAVEN_SETTINGS" "$GNUPGHOME"' EXIT
149+
150+
# --- Signing key + Sonatype token (shared secrets from LambdaMavenDeploy) ---
155151
GPG_JSON=$(aws secretsmanager get-secret-value --secret-id maven.gpg.keys --query SecretString --output text)
156152
CREDS_JSON=$(aws secretsmanager get-secret-value --secret-id maven.sonatype.creds --query SecretString --output text)
157-
158153
GPG_PRIVATE_KEY=$(jq -r '.private' <<< "$GPG_JSON")
159154
GPG_PASSPHRASE=$(jq -r '.passphrase' <<< "$GPG_JSON")
160155
SONATYPE_USERNAME=$(jq -r '."maven-central-login"' <<< "$CREDS_JSON")
@@ -164,37 +159,31 @@ jobs:
164159
echo "::add-mask::$SONATYPE_PASSWORD"
165160
166161
# Import the key with loopback pinentry so Maven can sign non-interactively.
167-
GNUPGHOME=$(mktemp -d)
168162
chmod 700 "$GNUPGHOME"
169163
echo "allow-loopback-pinentry" > "$GNUPGHOME/gpg-agent.conf"
170164
echo "pinentry-mode loopback" > "$GNUPGHOME/gpg.conf"
171-
export GNUPGHOME
172165
gpgconf --kill gpg-agent || true
173166
gpg --batch --import <<< "$GPG_PRIVATE_KEY"
174167
GPG_KEYNAME=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/ {print $5; exit}')
175168
176169
# settings.xml with the Sonatype token (server id "central").
177-
SETTINGS="$RUNNER_TEMP/settings.xml"
178170
{
179171
echo '<settings><servers><server>'
180172
echo "<id>central</id>"
181173
echo "<username>${SONATYPE_USERNAME}</username>"
182174
echo "<password>${SONATYPE_PASSWORD}</password>"
183175
echo '</server></servers></settings>'
184-
} > "$SETTINGS"
176+
} > "$MAVEN_SETTINGS"
185177
186-
# Pass to later steps (values already masked).
187-
echo "GNUPGHOME=$GNUPGHOME" >> "$GITHUB_ENV"
188-
echo "GPG_KEYNAME=$GPG_KEYNAME" >> "$GITHUB_ENV"
189-
echo "GPG_PASSPHRASE=$GPG_PASSPHRASE" >> "$GITHUB_ENV"
190-
echo "MAVEN_SETTINGS=$SETTINGS" >> "$GITHUB_ENV"
178+
# --- Release: build args as an array so each value is a single,
179+
# properly quoted argument (no word-splitting of untrusted input). ---
180+
RELEASE_ARGS=(-DreleaseVersion="$EFFECTIVE_RELEASE_VERSION")
181+
if [[ -n "$DEVELOPMENT_VERSION_INPUT" ]]; then
182+
RELEASE_ARGS+=(-DdevelopmentVersion="$DEVELOPMENT_VERSION_INPUT")
183+
fi
191184
192-
# prepare/perform aren't atomic: prepare locally, publish, push only after.
193-
- name: Release (prepare locally, publish, then push)
194-
if: ${{ github.event.inputs.skip_publish != 'true' }}
195-
run: |
196185
# Prepare locally (no push): release commits + tag.
197-
mvn release:prepare -DpushChanges=false $RELEASE_ARGS --file "$MODULE/pom.xml"
186+
mvn release:prepare -DpushChanges=false "${RELEASE_ARGS[@]}" --file "$MODULE/pom.xml"
198187
199188
# perform forks a fresh build, so pass settings/gpg via -Darguments.
200189
mvn release:perform -DlocalCheckout=true \
@@ -209,7 +198,11 @@ jobs:
209198
- name: Dry-run release (prepare only, no publish)
210199
if: ${{ github.event.inputs.skip_publish == 'true' }}
211200
run: |
212-
mvn release:prepare -DdryRun=true $RELEASE_ARGS --file "$MODULE/pom.xml"
201+
RELEASE_ARGS=(-DreleaseVersion="$EFFECTIVE_RELEASE_VERSION")
202+
if [[ -n "$DEVELOPMENT_VERSION_INPUT" ]]; then
203+
RELEASE_ARGS+=(-DdevelopmentVersion="$DEVELOPMENT_VERSION_INPUT")
204+
fi
205+
mvn release:prepare -DdryRun=true "${RELEASE_ARGS[@]}" --file "$MODULE/pom.xml"
213206
mvn release:clean --file "$MODULE/pom.xml" || true
214207
215208
# Nothing was pushed, so this only cleans the runner for a retry.

0 commit comments

Comments
 (0)