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