name: Publish Snapshot to Maven Central
permissions:
contents: read
on:
push:
branches:
- master
workflow_dispatch:
inputs:
sourceReleaseVersion:
description: "Optional release version that requested this snapshot publication."
required: false
type: string
sourceReleaseCommit:
description: "Optional release commit SHA that requested this snapshot publication."
required: false
type: string
dryRun:
description: "Run snapshot workflow in dry-run mode (build/test only; no deploy)."
required: false
default: true
type: boolean
concurrency:
group: snapshot-${{ github.repository }}
cancel-in-progress: true
jobs:
snapshot:
name: Publish Snapshot to Maven Central
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Normalize dry run input
id: dry_run
run: |
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
raw="${{ github.event.inputs.dryRun }}"
if [ -z "$raw" ]; then
raw=true
fi
normalized=$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')
if [ "$normalized" = "false" ] || [ "$normalized" = "0" ] || [ "$normalized" = "no" ]; then
dry_run=false
else
dry_run=true
fi
else
raw=""
dry_run=false
fi
echo "audit:dry_run_input_raw='${raw}'"
echo "audit:dry_run_normalized=$dry_run"
echo "dryRun=$dry_run" >> $GITHUB_OUTPUT
- name: Read snapshot version
id: version
run: |
set -euo pipefail
snapshot_version=$(python3 - <<'PY'
import sys
import xml.etree.ElementTree as ET
def find_version(path):
tree = ET.parse(path)
root = tree.getroot()
def find_first(elem):
if elem is None:
return None
for child in elem:
if child.tag.endswith("version"):
text = (child.text or "").strip()
if text:
return text
return None
return find_first(root) or find_first(root.find("./{*}parent"))
ver = find_version("pom.xml")
if not ver:
sys.exit(1)
print(ver)
PY
) || { echo "Could not read pom.xml version" >&2; exit 1; }
echo "audit:snapshot_version=${snapshot_version}"
echo "snapshotVersion=${snapshot_version}" >> "$GITHUB_OUTPUT"
- name: Cache Maven packages
uses: actions/cache@v5
with:
path: ~/.m2
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Set up JDK 25 (dry-run without publishing secrets)
if: steps.dry_run.outputs.dryRun == 'true'
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 25
cache: maven
- name: Set up JDK 25 with publishing secrets
if: steps.dry_run.outputs.dryRun != 'true'
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 25
cache: maven
server-id: central
server-username: ${{ secrets.MAVEN_CENTRAL_TOKEN_USER }}
server-password: ${{ secrets.MAVEN_CENTRAL_TOKEN_PASS }}
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }}
- name: Write Maven settings
if: steps.dry_run.outputs.dryRun != 'true'
run: |
echo "::group::Write Maven settings"
mkdir -p ~/.m2
printf '%s\n' \
'' \
' ' \
' ' \
' central' \
" ${{ secrets.MAVEN_CENTRAL_TOKEN_USER }}" \
" ${{ secrets.MAVEN_CENTRAL_TOKEN_PASS }}" \
' ' \
' ' \
'' > ~/.m2/settings.xml
echo "settings.xml created"
echo "::endgroup::"
- name: Write Maven security settings
if: steps.dry_run.outputs.dryRun != 'true'
shell: bash
env:
MAVEN_SECURITY_MASTER: ${{ secrets.MAVEN_MASTER_PASSPHRASE }}
run: |
set -euo pipefail
if [[ -z "${MAVEN_SECURITY_MASTER:-}" ]]; then
echo "MAVEN_MASTER_PASSPHRASE not set; skipping settings-security.xml generation"
exit 0
fi
mkdir -p ~/.m2
printf '%s\n' \
'' \
'' \
" ${MAVEN_SECURITY_MASTER}" \
'' > ~/.m2/settings-security.xml
chmod 600 ~/.m2/settings-security.xml
echo "settings-security.xml created"
- name: Configure GPG for signing
if: steps.dry_run.outputs.dryRun != 'true'
shell: bash
run: |
set -euo pipefail
mkdir -p ~/.gnupg
chmod 700 ~/.gnupg
echo "pinentry-mode loopback" >> ~/.gnupg/gpg.conf
chmod 600 ~/.gnupg/gpg.conf
touch ~/.gnupg/gpg-agent.conf
echo "allow-loopback-pinentry" >> ~/.gnupg/gpg-agent.conf
chmod 600 ~/.gnupg/gpg-agent.conf
gpgconf --kill gpg-agent 2>/dev/null || true
gpgconf --launch gpg-agent 2>/dev/null || true
echo "gpg.conf and gpg-agent.conf created"
- name: Dry-run mode notice
if: steps.dry_run.outputs.dryRun == 'true'
run: |
echo "DRY RUN ENABLED: running snapshot build/test only; Maven Central deployment is skipped."
- name: Build and Test
run: |
set -euo pipefail
echo "::group::Build and test snapshot candidate"
mvn -B clean test
echo "::endgroup::"
- name: Deploy Snapshot
if: steps.dry_run.outputs.dryRun != 'true'
env:
MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
set -euo pipefail
echo "::group::Deploy snapshot to Maven Central"
mvn -B deploy -Psign-snapshots 2>&1 | tee snapshot-deploy.log
echo "::endgroup::"
- name: Snapshot publication summary
if: always()
env:
DRY_RUN: ${{ steps.dry_run.outputs.dryRun }}
SNAPSHOT_VERSION: ${{ steps.version.outputs.snapshotVersion }}
SOURCE_RELEASE_VERSION: ${{ github.event.inputs.sourceReleaseVersion }}
SOURCE_RELEASE_COMMIT: ${{ github.event.inputs.sourceReleaseCommit }}
run: |
if [ "${DRY_RUN:-false}" = "true" ]; then
mutation_plan="would configure publishing secrets and deploy the snapshot to Maven Central"
rerun_guidance="rerun snapshot.yml with dryRun=false, or let the master push/publish handoff path run"
else
mutation_plan="completed snapshot deployment"
rerun_guidance="not needed for this non-dry-run snapshot run"
fi
cat > snapshot-audit.json <> "$GITHUB_STEP_SUMMARY"
- name: Upload snapshot audit artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: snapshot-audit-${{ github.run_id }}
path: |
snapshot-audit.json
snapshot-deploy.log
if-no-files-found: ignore
retention-days: 14