From f7fd34acd582a8e8d6ec9029db04c31edc71a323 Mon Sep 17 00:00:00 2001 From: Steven Sklar Date: Wed, 27 May 2026 11:13:43 -0400 Subject: [PATCH 1/3] Harden Maven Central release flow --- .github/workflows/maven_central_release.yml | 47 +++++++++++++++------ README.md | 34 +++++++++++++++ artifacts/release/README.md | 14 ++++++ 3 files changed, 82 insertions(+), 13 deletions(-) diff --git a/.github/workflows/maven_central_release.yml b/.github/workflows/maven_central_release.yml index 45c3281f..5c148b43 100644 --- a/.github/workflows/maven_central_release.yml +++ b/.github/workflows/maven_central_release.yml @@ -36,18 +36,6 @@ jobs: fi done - required_secrets=( - MAVEN_RELEASE_AWS_ROLE_ARN - MAVEN_RELEASE_AWS_SECRET_ARN - ) - - for secret_name in "${required_secrets[@]}"; do - if [[ -z "${!secret_name:-}" ]]; then - echo "::error::GitHub secret ${secret_name} is required." - exit 1 - fi - done - - name: Check out tag uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: @@ -64,15 +52,43 @@ jobs: run: | POM_VERSION=$(mvn -B -q -N -DforceStdout help:evaluate -Dexpression=project.version) if [[ "${POM_VERSION}" == *-SNAPSHOT ]]; then - echo "::error::Refusing to release SNAPSHOT version ${POM_VERSION}." + echo "::error::Refusing to release SNAPSHOT version ${POM_VERSION}. Create the release tag only after maven-release-plugin has committed the non-SNAPSHOT release POMs." exit 1 fi if [[ "${GITHUB_REF_NAME}" != "${POM_VERSION}" ]]; then echo "::error::Tag ${GITHUB_REF_NAME} does not match POM version ${POM_VERSION}." exit 1 fi + echo "MAVEN_RELEASE_VERSION=${POM_VERSION}" >> "$GITHUB_ENV" + + - name: Check whether release is already on Maven Central + run: | + artifact_url="https://repo.maven.apache.org/maven2/org/questdb/questdb-client/${MAVEN_RELEASE_VERSION}/questdb-client-${MAVEN_RELEASE_VERSION}.pom" + status_code=$(curl -sS -o /dev/null -w "%{http_code}" -I "$artifact_url" || true) + if [[ "${status_code}" == "200" ]]; then + echo "MAVEN_ARTIFACT_ALREADY_PUBLISHED=true" >> "$GITHUB_ENV" + echo "::notice::org.questdb:questdb-client:${MAVEN_RELEASE_VERSION} is already available on Maven Central; skipping publish." + else + echo "MAVEN_ARTIFACT_ALREADY_PUBLISHED=false" >> "$GITHUB_ENV" + fi + + - name: Validate release AWS configuration + if: env.MAVEN_ARTIFACT_ALREADY_PUBLISHED != 'true' + run: | + required_secrets=( + MAVEN_RELEASE_AWS_ROLE_ARN + MAVEN_RELEASE_AWS_SECRET_ARN + ) + + for secret_name in "${required_secrets[@]}"; do + if [[ -z "${!secret_name:-}" ]]; then + echo "::error::GitHub secret ${secret_name} is required." + exit 1 + fi + done - name: Configure AWS credentials + if: env.MAVEN_ARTIFACT_ALREADY_PUBLISHED != 'true' uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 with: aws-region: ${{ env.MAVEN_RELEASE_AWS_REGION }} @@ -80,6 +96,7 @@ jobs: role-session-name: java-questdb-client-release - name: Fetch release credentials + if: env.MAVEN_ARTIFACT_ALREADY_PUBLISHED != 'true' uses: aws-actions/aws-secretsmanager-get-secrets@a9a7eb4e2f2871d30dc5b892576fde60a2ecc802 # v2.0.10 with: secret-ids: | @@ -87,6 +104,7 @@ jobs: parse-json-secrets: true - name: Validate release credentials + if: env.MAVEN_ARTIFACT_ALREADY_PUBLISHED != 'true' run: | required_vars=( MAVEN_GPG_PRIVATE_KEY @@ -102,6 +120,7 @@ jobs: done - name: Configure Maven settings.xml + if: env.MAVEN_ARTIFACT_ALREADY_PUBLISHED != 'true' run: | if [[ -z "${MAVEN_GPG_PASSPHRASE+x}" ]]; then echo "MAVEN_GPG_PASSPHRASE=" >> "$GITHUB_ENV" @@ -126,6 +145,7 @@ jobs: EOF - name: Import release signing key + if: env.MAVEN_ARTIFACT_ALREADY_PUBLISHED != 'true' run: | export GNUPGHOME="$(mktemp -d)" chmod 700 "$GNUPGHOME" @@ -133,6 +153,7 @@ jobs: echo "GNUPGHOME=$GNUPGHOME" >> "$GITHUB_ENV" - name: Publish release to Maven Central + if: env.MAVEN_ARTIFACT_ALREADY_PUBLISHED != 'true' run: | mvn -B -ntp deploy -P maven-central-release -DskipTests diff --git a/README.md b/README.md index 36a14a9d..d3b77bf9 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,40 @@ cd java-questdb-client mvn clean package -DskipTests ``` +## Releasing + +Maven Central publishing is owned by the tag-triggered GitHub Actions workflow. Do not publish from a local machine in +the normal release path. + +Release tags must be created by `maven-release-plugin release:prepare` after it has committed the non-SNAPSHOT release +POMs. Do not manually create or push a version tag from `main` while the POMs still contain `-SNAPSHOT`; that tag will +trigger the release workflow and the workflow will reject it. + +Normal release flow: + +```bash +VERSION=1.2.2 +NEXT_VERSION=1.2.3 + +mvn release:clean +mvn -B release:prepare \ + -DautoVersionSubmodules=true \ + -DpushChanges=false \ + -DreleaseVersion="$VERSION" \ + -DdevelopmentVersion="$NEXT_VERSION-SNAPSHOT" \ + -Dtag="$VERSION" +git show --no-patch --oneline "$VERSION" +git show "$VERSION:pom.xml" | grep "$VERSION" +git push origin "release/$VERSION" +git push origin "$VERSION" +``` + +Do not run `mvn release:perform` or `mvn deploy` unless you are intentionally bypassing the GitHub Actions release +workflow. Running a local deploy while the tag workflow is also publishing creates competing Sonatype deployments for +the same coordinate. + +Full release procedure: [artifacts/release/README.md](artifacts/release/README.md). + ### Building Native Libraries The client includes native libraries (C/C++ and assembly) for performance-critical operations. Pre-built binaries are included in the repository, but you can rebuild them locally if needed. diff --git a/artifacts/release/README.md b/artifacts/release/README.md index 918fab65..67fa20fa 100644 --- a/artifacts/release/README.md +++ b/artifacts/release/README.md @@ -43,6 +43,9 @@ Removes any `release.properties` and `*.releaseBackup` files left over from a pr - roll the versions to the next snapshot (`1.2.3-SNAPSHOT`) - commit the next-snapshot POMs +Do not create or push the release tag before this step. A tag pushed from `main` while the POMs still contain +`-SNAPSHOT` will trigger the Maven Central workflow and be rejected. + ```bash mvn -B release:prepare \ -DautoVersionSubmodules=true \ @@ -55,6 +58,9 @@ mvn -B release:prepare \ `-B` runs non-interactively; drop it for special versions (e.g. a new major) to get the prompts. `-DpushChanges=false` keeps the commits and tag local until you have verified them. +Do not run `release:perform` or `mvn deploy` locally during the normal release path. Publishing is owned by the +GitHub Actions workflow that runs from the release tag. + If `release:prepare` fails partway through: ```bash @@ -68,6 +74,13 @@ it manually or the next attempt at the same version fails. If `release.propertie ## Push the release branch and tag +Before pushing, verify the tag points at the release commit and that the tagged POM version is not a snapshot: + +```bash +git show --no-patch --oneline 1.2.2 +git show 1.2.2:pom.xml | grep '1.2.2' +``` + ```bash git push origin release/1.2.2 git push origin 1.2.2 @@ -84,6 +97,7 @@ a tag matching `X.Y.Z` is pushed. No manual dispatch. It: - checks out the pushed tag - assumes an AWS IAM role via OIDC and reads the GPG key and Sonatype credentials from AWS Secrets Manager - verifies the tag matches the parent POM version and is not a snapshot +- skips publishing if the same version is already present on Maven Central - signs the artifacts and uploads them through the Sonatype Central Portal The workflow returns once Sonatype has validated the upload and taken ownership of the artifacts. Physical From 69b795af19262195523288f205d767de0bf78b4a Mon Sep 17 00:00:00 2001 From: Steven Sklar Date: Wed, 27 May 2026 11:18:06 -0400 Subject: [PATCH 2/3] Keep duplicate release failures explicit --- .github/workflows/maven_central_release.yml | 19 ------------------- artifacts/release/README.md | 1 - 2 files changed, 20 deletions(-) diff --git a/.github/workflows/maven_central_release.yml b/.github/workflows/maven_central_release.yml index 5c148b43..c6f27833 100644 --- a/.github/workflows/maven_central_release.yml +++ b/.github/workflows/maven_central_release.yml @@ -59,21 +59,8 @@ jobs: echo "::error::Tag ${GITHUB_REF_NAME} does not match POM version ${POM_VERSION}." exit 1 fi - echo "MAVEN_RELEASE_VERSION=${POM_VERSION}" >> "$GITHUB_ENV" - - - name: Check whether release is already on Maven Central - run: | - artifact_url="https://repo.maven.apache.org/maven2/org/questdb/questdb-client/${MAVEN_RELEASE_VERSION}/questdb-client-${MAVEN_RELEASE_VERSION}.pom" - status_code=$(curl -sS -o /dev/null -w "%{http_code}" -I "$artifact_url" || true) - if [[ "${status_code}" == "200" ]]; then - echo "MAVEN_ARTIFACT_ALREADY_PUBLISHED=true" >> "$GITHUB_ENV" - echo "::notice::org.questdb:questdb-client:${MAVEN_RELEASE_VERSION} is already available on Maven Central; skipping publish." - else - echo "MAVEN_ARTIFACT_ALREADY_PUBLISHED=false" >> "$GITHUB_ENV" - fi - name: Validate release AWS configuration - if: env.MAVEN_ARTIFACT_ALREADY_PUBLISHED != 'true' run: | required_secrets=( MAVEN_RELEASE_AWS_ROLE_ARN @@ -88,7 +75,6 @@ jobs: done - name: Configure AWS credentials - if: env.MAVEN_ARTIFACT_ALREADY_PUBLISHED != 'true' uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 with: aws-region: ${{ env.MAVEN_RELEASE_AWS_REGION }} @@ -96,7 +82,6 @@ jobs: role-session-name: java-questdb-client-release - name: Fetch release credentials - if: env.MAVEN_ARTIFACT_ALREADY_PUBLISHED != 'true' uses: aws-actions/aws-secretsmanager-get-secrets@a9a7eb4e2f2871d30dc5b892576fde60a2ecc802 # v2.0.10 with: secret-ids: | @@ -104,7 +89,6 @@ jobs: parse-json-secrets: true - name: Validate release credentials - if: env.MAVEN_ARTIFACT_ALREADY_PUBLISHED != 'true' run: | required_vars=( MAVEN_GPG_PRIVATE_KEY @@ -120,7 +104,6 @@ jobs: done - name: Configure Maven settings.xml - if: env.MAVEN_ARTIFACT_ALREADY_PUBLISHED != 'true' run: | if [[ -z "${MAVEN_GPG_PASSPHRASE+x}" ]]; then echo "MAVEN_GPG_PASSPHRASE=" >> "$GITHUB_ENV" @@ -145,7 +128,6 @@ jobs: EOF - name: Import release signing key - if: env.MAVEN_ARTIFACT_ALREADY_PUBLISHED != 'true' run: | export GNUPGHOME="$(mktemp -d)" chmod 700 "$GNUPGHOME" @@ -153,7 +135,6 @@ jobs: echo "GNUPGHOME=$GNUPGHOME" >> "$GITHUB_ENV" - name: Publish release to Maven Central - if: env.MAVEN_ARTIFACT_ALREADY_PUBLISHED != 'true' run: | mvn -B -ntp deploy -P maven-central-release -DskipTests diff --git a/artifacts/release/README.md b/artifacts/release/README.md index 67fa20fa..ace57205 100644 --- a/artifacts/release/README.md +++ b/artifacts/release/README.md @@ -97,7 +97,6 @@ a tag matching `X.Y.Z` is pushed. No manual dispatch. It: - checks out the pushed tag - assumes an AWS IAM role via OIDC and reads the GPG key and Sonatype credentials from AWS Secrets Manager - verifies the tag matches the parent POM version and is not a snapshot -- skips publishing if the same version is already present on Maven Central - signs the artifacts and uploads them through the Sonatype Central Portal The workflow returns once Sonatype has validated the upload and taken ownership of the artifacts. Physical From b483f8dce99bda16671e5916a9676701bd04e58c Mon Sep 17 00:00:00 2001 From: Steven Sklar Date: Wed, 27 May 2026 15:18:31 -0400 Subject: [PATCH 3/3] Restore release config validation ordering --- .github/workflows/maven_central_release.yml | 26 ++++++++++----------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/.github/workflows/maven_central_release.yml b/.github/workflows/maven_central_release.yml index c6f27833..09401d18 100644 --- a/.github/workflows/maven_central_release.yml +++ b/.github/workflows/maven_central_release.yml @@ -36,6 +36,18 @@ jobs: fi done + required_secrets=( + MAVEN_RELEASE_AWS_ROLE_ARN + MAVEN_RELEASE_AWS_SECRET_ARN + ) + + for secret_name in "${required_secrets[@]}"; do + if [[ -z "${!secret_name:-}" ]]; then + echo "::error::GitHub secret ${secret_name} is required." + exit 1 + fi + done + - name: Check out tag uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: @@ -60,20 +72,6 @@ jobs: exit 1 fi - - name: Validate release AWS configuration - run: | - required_secrets=( - MAVEN_RELEASE_AWS_ROLE_ARN - MAVEN_RELEASE_AWS_SECRET_ARN - ) - - for secret_name in "${required_secrets[@]}"; do - if [[ -z "${!secret_name:-}" ]]; then - echo "::error::GitHub secret ${secret_name} is required." - exit 1 - fi - done - - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 with: