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