From f7fd34acd582a8e8d6ec9029db04c31edc71a323 Mon Sep 17 00:00:00 2001 From: Steven Sklar Date: Wed, 27 May 2026 11:13:43 -0400 Subject: [PATCH 01/15] 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 02/15] 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 03/15] 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: From b49af91e607907484596aa5d7836e4092adabc93 Mon Sep 17 00:00:00 2001 From: Steven Sklar Date: Wed, 27 May 2026 20:27:52 -0400 Subject: [PATCH 04/15] builds binaries in CI --- .github/workflows/maven_central_release.yml | 561 +++++++++++++++++++- .github/workflows/rebuild_native_libs.yml | 296 ----------- README.md | 34 +- artifacts/release/README.md | 154 +++--- core/pom.xml | 58 +- pom.xml | 1 - 6 files changed, 671 insertions(+), 433 deletions(-) delete mode 100644 .github/workflows/rebuild_native_libs.yml diff --git a/.github/workflows/maven_central_release.yml b/.github/workflows/maven_central_release.yml index 09401d18..a1acebb1 100644 --- a/.github/workflows/maven_central_release.yml +++ b/.github/workflows/maven_central_release.yml @@ -1,23 +1,525 @@ name: Release to Maven Central on: - push: - tags: - - '[0-9]+.[0-9]+.[0-9]+' + workflow_dispatch: + inputs: + source_ref: + description: "Branch/ref to release from" + required: true + default: "main" + type: string + release_version_override: + description: "Optional release version override; normally inferred from the current -SNAPSHOT POM" + required: false + type: string + next_development_version_override: + description: "Optional next development version override; normally inferred by maven-release-plugin" + required: false + type: string + dry_run: + description: "Exercise prepare, native builds, and local deploy without pushing or publishing" + required: true + default: true + type: boolean permissions: contents: read - id-token: write concurrency: group: maven-central-release cancel-in-progress: false jobs: - release: + prepare: + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + outputs: + release_tag: ${{ steps.release-tag.outputs.release_tag }} + env: + SOURCE_REF: ${{ inputs.source_ref }} + DRY_RUN: ${{ inputs.dry_run }} + RELEASE_VERSION_OVERRIDE: ${{ inputs.release_version_override }} + NEXT_DEVELOPMENT_VERSION_OVERRIDE: ${{ inputs.next_development_version_override }} + steps: + - name: Check out source ref + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + ref: ${{ inputs.source_ref }} + fetch-depth: 0 + submodules: true + + - name: Set up Java 11 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: temurin + java-version: "11" + cache: maven + + - name: Prepare release + run: | + git config user.name "GitHub Actions - Maven Release" + git config user.email "actions@github.com" + + mvn -B -ntp release:clean + + release_args=( + -DautoVersionSubmodules=true + -DpushChanges=false + ) + + if [[ -n "${RELEASE_VERSION_OVERRIDE}" ]]; then + release_args+=("-DreleaseVersion=${RELEASE_VERSION_OVERRIDE}") + fi + + if [[ -n "${NEXT_DEVELOPMENT_VERSION_OVERRIDE}" ]]; then + release_args+=("-DdevelopmentVersion=${NEXT_DEVELOPMENT_VERSION_OVERRIDE}") + fi + + mvn -B -ntp release:prepare "${release_args[@]}" + + - name: Capture and verify release tag + id: release-tag + run: | + release_tag="$(sed -n 's/^scm.tag=//p' release.properties | tail -n 1)" + if [[ -z "${release_tag}" ]]; then + echo "::error::release.properties did not contain scm.tag." + exit 1 + fi + + pom_version="$(git show "${release_tag}:pom.xml" | sed -n 's:.*\(.*\).*:\1:p' | head -n 1)" + if [[ -z "${pom_version}" ]]; then + echo "::error::Could not read project version from tag ${release_tag}." + exit 1 + fi + if [[ "${pom_version}" == *-SNAPSHOT ]]; then + echo "::error::Refusing to release SNAPSHOT version ${pom_version}." + exit 1 + fi + if [[ "${release_tag}" != "${pom_version}" ]]; then + echo "::error::Generated tag ${release_tag} does not match POM version ${pom_version}." + exit 1 + fi + + echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT" + + - name: Push release commits and tag + if: ${{ !inputs.dry_run }} + run: | + git push origin "HEAD:${SOURCE_REF}" + git push origin "refs/tags/${{ steps.release-tag.outputs.release_tag }}" + + - name: Archive dry-run release source + if: ${{ inputs.dry_run }} + run: | + git checkout "${{ steps.release-tag.outputs.release_tag }}" + git submodule update --init --recursive + mkdir -p /tmp/release-source + tar --exclude-vcs --exclude=target -cf /tmp/release-source/release-source.tar . + + - name: Upload dry-run release source + if: ${{ inputs.dry_run }} + uses: actions/upload-artifact@v4 + with: + name: release-source + path: /tmp/release-source/release-source.tar + if-no-files-found: error + + build-macos: + needs: prepare + strategy: + matrix: + include: + - os: macos-14 + platform: darwin-aarch64 + - os: macos-15-intel + platform: darwin-x86-64 + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + steps: + - name: Check out release tag + if: ${{ !inputs.dry_run }} + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + ref: ${{ needs.prepare.outputs.release_tag }} + submodules: true + + - name: Download dry-run release source + if: ${{ inputs.dry_run }} + uses: actions/download-artifact@v4 + with: + name: release-source + path: target/release-source + + - name: Extract dry-run release source + if: ${{ inputs.dry_run }} + run: | + tar -xf target/release-source/release-source.tar + + - name: Install toolchains + run: | + brew uninstall cmake || true + brew install make cmake gcc nasm + + - name: Build native library + run: | + cd core + export MACOSX_DEPLOYMENT_TARGET=13.0 + cmake -B build/release -DCMAKE_BUILD_TYPE=Release + cmake --build build/release --config Release + + - name: Stage native library + run: | + mkdir -p native-artifacts/${{ matrix.platform }} + cp core/target/classes/io/questdb/client/bin-local/libquestdb.dylib native-artifacts/${{ matrix.platform }}/libquestdb.dylib + + - name: Upload native library + uses: actions/upload-artifact@v4 + with: + name: native-${{ matrix.platform }} + path: native-artifacts/${{ matrix.platform }}/libquestdb.dylib + if-no-files-found: error + + build-linux-x86-64: + needs: prepare + runs-on: ubuntu-latest + timeout-minutes: 60 + container: + image: quay.io/pypa/manylinux2014_x86_64 + volumes: + - /node20217:/node20217 + - /node20217:/__e/node20 + steps: + - name: Install tools + run: | + ldd --version + yum update -y + yum install 'perl(Env)' perl-Font-TTF perl-Sort-Versions gcc wget perf asciidoc xmlto ghostscript adobe-source-sans-pro-fonts adobe-source-code-pro-fonts rpm-build zstd curl -y + + - name: Build nasm + run: | + wget https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/linux/nasm-2.16.03-0.fc39.src.rpm + rpmbuild --rebuild ./nasm-2.16.03-0.fc39.src.rpm + rpm -i ~/rpmbuild/RPMS/x86_64/nasm-2.16.03-0.el7.x86_64.rpm + + - name: Install Node.js 20 glibc2.17 + run: | + curl -LO https://unofficial-builds.nodejs.org/download/release/v20.9.0/node-v20.9.0-linux-x64-glibc-217.tar.xz + tar -xf node-v20.9.0-linux-x64-glibc-217.tar.xz --strip-components 1 -C /node20217 + ldd /__e/node20/bin/node + + - name: Check out release tag + if: ${{ !inputs.dry_run }} + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + ref: ${{ needs.prepare.outputs.release_tag }} + submodules: true + + - name: Download dry-run release source + if: ${{ inputs.dry_run }} + uses: actions/download-artifact@v4 + with: + name: release-source + path: target/release-source + + - name: Extract dry-run release source + if: ${{ inputs.dry_run }} + run: | + tar -xf target/release-source/release-source.tar + + - name: Install up-to-date CMake + run: | + wget -nv https://github.com/Kitware/CMake/releases/download/v3.29.2/cmake-3.29.2-linux-x86_64.tar.gz + tar -zxf cmake-3.29.2-linux-x86_64.tar.gz + echo "PATH=`pwd`/cmake-3.29.2-linux-x86_64/bin/:$PATH" >> "$GITHUB_ENV" + + - name: Install GraalVM JDK 25 + run: | + wget -nv -O graalvm.tar.gz https://download.oracle.com/graalvm/25/latest/graalvm-jdk-25_linux-x64_bin.tar.gz + mkdir graalvm + tar xfz graalvm.tar.gz -C graalvm --strip-components=1 + echo "JAVA_HOME=`pwd`/graalvm" >> "$GITHUB_ENV" + + - name: Build native library + run: | + cd core + cmake -DCMAKE_BUILD_TYPE=Release -B cmake-build-release -S. + cmake --build cmake-build-release --config Release + + - name: Stage native library + run: | + mkdir -p native-artifacts/linux-x86-64 + cp core/target/classes/io/questdb/client/bin-local/libquestdb.so native-artifacts/linux-x86-64/libquestdb.so + + - name: Upload native library + uses: actions/upload-artifact@v4 + with: + name: native-linux-x86-64 + path: native-artifacts/linux-x86-64/libquestdb.so + if-no-files-found: error + + build-linux-aarch64: + needs: prepare + runs-on: ubuntu-22.04-arm + timeout-minutes: 60 + container: + image: quay.io/pypa/manylinux_2_28_aarch64 + steps: + - name: Check out release tag + if: ${{ !inputs.dry_run }} + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + ref: ${{ needs.prepare.outputs.release_tag }} + submodules: true + + - name: Download dry-run release source + if: ${{ inputs.dry_run }} + uses: actions/download-artifact@v4 + with: + name: release-source + path: target/release-source + + - name: Extract dry-run release source + if: ${{ inputs.dry_run }} + run: | + tar -xf target/release-source/release-source.tar + + - name: Install tooling + run: | + yum update -y + yum install wget nasm zstd -y + + - name: Install GraalVM JDK 25 + run: | + wget -v --timeout=180 -O graalvm.tar.gz https://download.oracle.com/graalvm/25/latest/graalvm-jdk-25_linux-aarch64_bin.tar.gz + mkdir graalvm + tar xfz graalvm.tar.gz -C graalvm --strip-components=1 + echo "JAVA_HOME=`pwd`/graalvm" >> "$GITHUB_ENV" + + - name: Build native library + run: | + cd core + cmake -DCMAKE_TOOLCHAIN_FILE=./src/main/c/toolchains/linux-arm64.cmake -DCMAKE_BUILD_TYPE=Release -B cmake-build-release-arm64 -S. + cmake --build cmake-build-release-arm64 --config Release + + - name: Stage native library + run: | + mkdir -p native-artifacts/linux-aarch64 + cp core/target/classes/io/questdb/client/bin-local/libquestdb.so native-artifacts/linux-aarch64/libquestdb.so + + - name: Upload native library + uses: actions/upload-artifact@v4 + with: + name: native-linux-aarch64 + path: native-artifacts/linux-aarch64/libquestdb.so + if-no-files-found: error + + build-windows-x86-64: + needs: prepare + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - name: Check out release tag + if: ${{ !inputs.dry_run }} + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + ref: ${{ needs.prepare.outputs.release_tag }} + submodules: true + + - name: Download dry-run release source + if: ${{ inputs.dry_run }} + uses: actions/download-artifact@v4 + with: + name: release-source + path: target/release-source + + - name: Extract dry-run release source + if: ${{ inputs.dry_run }} + run: | + tar -xf target/release-source/release-source.tar + + - name: Install tooling + run: | + sudo sysctl -w fs.file-max=500000 + sudo apt-get update -y + sudo apt-get install -y nasm gcc-mingw-w64 g++-mingw-w64 + + - name: Install GraalVM JDK 25 + run: | + wget -nv -O graalvm.tar.gz https://download.oracle.com/graalvm/25/latest/graalvm-jdk-25_linux-x64_bin.tar.gz + mkdir graalvm + tar xfz graalvm.tar.gz -C graalvm --strip-components=1 + echo "JAVA_HOME=`pwd`/graalvm" >> "$GITHUB_ENV" + + - name: Download Windows jni_md.h from JDK 25 + run: | + cd core + curl https://raw.githubusercontent.com/openjdk/jdk25u/master/src/java.base/windows/native/include/jni_md.h > "$JAVA_HOME/include/jni_md.h" + + - name: Build native library + run: | + cd core + cmake -DCMAKE_TOOLCHAIN_FILE=./src/main/c/toolchains/windows-x86_64.cmake -DCMAKE_CROSSCOMPILING=True -DCMAKE_BUILD_TYPE=Release -B cmake-build-release-win64 + cmake --build cmake-build-release-win64 --config Release + + - name: Check CXX runtime dependency + run: | + if x86_64-w64-mingw32-objdump -p ./core/target/classes/io/questdb/client/bin-local/libquestdb.dll | grep -q libstdc++; then + echo "Failure: CXX runtime dependency detected" + exit 1 + fi + + - name: Stage native library + run: | + mkdir -p native-artifacts/windows-x86-64 + cp core/target/classes/io/questdb/client/bin-local/libquestdb.dll native-artifacts/windows-x86-64/libquestdb.dll + + - name: Upload native library + uses: actions/upload-artifact@v4 + with: + name: native-windows-x86-64 + path: native-artifacts/windows-x86-64/libquestdb.dll + if-no-files-found: error + + dry-run-package: + if: ${{ inputs.dry_run }} + needs: + - prepare + - build-macos + - build-linux-x86-64 + - build-linux-aarch64 + - build-windows-x86-64 + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Download dry-run release source + uses: actions/download-artifact@v4 + with: + name: release-source + path: target/release-source + + - name: Extract dry-run release source + run: | + tar -xf target/release-source/release-source.tar + + - name: Set up Java 11 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: temurin + java-version: "11" + cache: maven + + - name: Download native artifacts + uses: actions/download-artifact@v4 + with: + pattern: native-* + path: core/target/downloaded-native-artifacts + merge-multiple: false + + - name: Stage native artifacts for Maven + run: | + mkdir -p core/target/native-libs/io/questdb/client/bin/darwin-aarch64 + mkdir -p core/target/native-libs/io/questdb/client/bin/darwin-x86-64 + mkdir -p core/target/native-libs/io/questdb/client/bin/linux-aarch64 + mkdir -p core/target/native-libs/io/questdb/client/bin/linux-x86-64 + mkdir -p core/target/native-libs/io/questdb/client/bin/windows-x86-64 + cp core/target/downloaded-native-artifacts/native-darwin-aarch64/libquestdb.dylib core/target/native-libs/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib + cp core/target/downloaded-native-artifacts/native-darwin-x86-64/libquestdb.dylib core/target/native-libs/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib + cp core/target/downloaded-native-artifacts/native-linux-aarch64/libquestdb.so core/target/native-libs/io/questdb/client/bin/linux-aarch64/libquestdb.so + cp core/target/downloaded-native-artifacts/native-linux-x86-64/libquestdb.so core/target/native-libs/io/questdb/client/bin/linux-x86-64/libquestdb.so + cp core/target/downloaded-native-artifacts/native-windows-x86-64/libquestdb.dll core/target/native-libs/io/questdb/client/bin/windows-x86-64/libquestdb.dll + + expected=( + core/target/native-libs/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib + core/target/native-libs/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib + core/target/native-libs/io/questdb/client/bin/linux-aarch64/libquestdb.so + core/target/native-libs/io/questdb/client/bin/linux-x86-64/libquestdb.so + core/target/native-libs/io/questdb/client/bin/windows-x86-64/libquestdb.dll + ) + + for file_name in "${expected[@]}"; do + if [[ ! -s "${file_name}" ]]; then + echo "::error::Missing native artifact ${file_name}." + exit 1 + fi + done + + - name: Generate dry-run signing key + run: | + export GNUPGHOME="$(mktemp -d)" + chmod 700 "$GNUPGHOME" + cat > keyparams <<'EOF' + Key-Type: RSA + Key-Length: 3072 + Name-Real: QuestDB Maven Release Dry Run + Name-Email: dry-run@questdb.io + Expire-Date: 0 + %no-protection + %commit + EOF + gpg --batch --generate-key keyparams + echo "GNUPGHOME=$GNUPGHOME" >> "$GITHUB_ENV" + echo "MAVEN_GPG_PASSPHRASE=" >> "$GITHUB_ENV" + + - name: Configure Maven settings.xml + run: | + mkdir -p "$HOME/.m2" + cat > "$HOME/.m2/settings.xml" <<'EOF' + + + + gpg.passphrase + ${env.MAVEN_GPG_PASSPHRASE} + + + + EOF + + - name: Dry-run release packaging and deploy + run: | + mvn -B -ntp deploy -P release-artifacts,release-dry-run-deploy,include-native-artifacts -DskipTests + + - name: Upload dry-run Maven repository + uses: actions/upload-artifact@v4 + with: + name: dry-run-maven-repository + path: core/target/dry-run-repo + if-no-files-found: error + + - name: Upload dry-run built artifacts + uses: actions/upload-artifact@v4 + with: + name: dry-run-core-target-artifacts + path: | + core/target/*.jar + core/target/*.pom + core/target/*.asc + if-no-files-found: warn + + - name: Remove imported signing key + if: always() + run: | + if [[ -n "${GNUPGHOME:-}" && -d "${GNUPGHOME}" ]]; then + rm -rf "$GNUPGHOME" + fi + + publish-central: + if: ${{ !inputs.dry_run }} + needs: + - prepare + - build-macos + - build-linux-x86-64 + - build-linux-aarch64 + - build-windows-x86-64 runs-on: ubuntu-latest environment: maven-release timeout-minutes: 30 + permissions: + contents: read + id-token: write env: MAVEN_RELEASE_AWS_REGION: ${{ vars.MAVEN_RELEASE_AWS_REGION }} MAVEN_RELEASE_AWS_ROLE_ARN: ${{ secrets.MAVEN_RELEASE_AWS_ROLE_ARN }} @@ -48,10 +550,10 @@ jobs: fi done - - name: Check out tag + - name: Check out release tag uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: - ref: ${{ github.ref }} + ref: ${{ needs.prepare.outputs.release_tag }} - name: Set up Java 11 uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 @@ -60,17 +562,40 @@ jobs: java-version: "11" cache: maven - - name: Verify tag matches POM version + - name: Download native artifacts + uses: actions/download-artifact@v4 + with: + pattern: native-* + path: core/target/downloaded-native-artifacts + merge-multiple: false + + - name: Stage native artifacts for Maven 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}. 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 + mkdir -p core/target/native-libs/io/questdb/client/bin/darwin-aarch64 + mkdir -p core/target/native-libs/io/questdb/client/bin/darwin-x86-64 + mkdir -p core/target/native-libs/io/questdb/client/bin/linux-aarch64 + mkdir -p core/target/native-libs/io/questdb/client/bin/linux-x86-64 + mkdir -p core/target/native-libs/io/questdb/client/bin/windows-x86-64 + cp core/target/downloaded-native-artifacts/native-darwin-aarch64/libquestdb.dylib core/target/native-libs/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib + cp core/target/downloaded-native-artifacts/native-darwin-x86-64/libquestdb.dylib core/target/native-libs/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib + cp core/target/downloaded-native-artifacts/native-linux-aarch64/libquestdb.so core/target/native-libs/io/questdb/client/bin/linux-aarch64/libquestdb.so + cp core/target/downloaded-native-artifacts/native-linux-x86-64/libquestdb.so core/target/native-libs/io/questdb/client/bin/linux-x86-64/libquestdb.so + cp core/target/downloaded-native-artifacts/native-windows-x86-64/libquestdb.dll core/target/native-libs/io/questdb/client/bin/windows-x86-64/libquestdb.dll + + expected=( + core/target/native-libs/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib + core/target/native-libs/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib + core/target/native-libs/io/questdb/client/bin/linux-aarch64/libquestdb.so + core/target/native-libs/io/questdb/client/bin/linux-x86-64/libquestdb.so + core/target/native-libs/io/questdb/client/bin/windows-x86-64/libquestdb.dll + ) + + for file_name in "${expected[@]}"; do + if [[ ! -s "${file_name}" ]]; then + echo "::error::Missing native artifact ${file_name}." + exit 1 + fi + done - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 @@ -134,7 +659,7 @@ jobs: - name: Publish release to Maven Central run: | - mvn -B -ntp deploy -P maven-central-release -DskipTests + mvn -B -ntp deploy -P release-artifacts,maven-central-publish,include-native-artifacts -DskipTests - name: Remove imported signing key if: always() diff --git a/.github/workflows/rebuild_native_libs.yml b/.github/workflows/rebuild_native_libs.yml deleted file mode 100644 index 026d3c3e..00000000 --- a/.github/workflows/rebuild_native_libs.yml +++ /dev/null @@ -1,296 +0,0 @@ -name: Build and Push Release CXX Libraries -on: - workflow_dispatch: -# This workflow is triggered manually from the Actions tab. -# It's meant to be run on a PR that changes the native C++ code. - -# It builds native libraries for all supported platforms and pushes them to the current branch. -# It splits the building process into 3 build jobs: -# 1. build-macos - Builds native libraries for MacOS: both ARM and x64. -# It uses native runners for each platform, because cross compilation on MacOS is complicated. -# 2. build-others - Builds native libraries for x64 Linux, ARM Linux and Windows. -# It uses cross-compilation for ARM Linux and Windows. -# -# Each build job saves the resulting binaries to the cache under a unique key -# When all build jobs are finished, the collect-commit-and-push job restores the binaries from the cache -# and pushes them to the current branch. - -jobs: - build-all-macos: - strategy: - matrix: - # macos-14 = ARM M1 - # macos-15-intel = x64 - # if you change OS definitions then you need to change conditions in cache-save steps below - os: [ macos-14, macos-15-intel ] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - with: - submodules: true - - name: Install toolchains (CXX/NASM) - run: | - # https://github.com/actions/runner-images/issues/12912 - # Temporary fix (mtopolnik 2025-09-02): GH Action pre-installs a custom cmake version, install then fails - brew uninstall cmake - brew install make cmake gcc nasm - - name: Build CXX Library - run: | - cd core - export MACOSX_DEPLOYMENT_TARGET=13.0 - cmake -B build/release -DCMAKE_BUILD_TYPE=Release - cmake --build build/release --config Release - - name: Copy darwin-aarch64 CXX Library to the final directory - if: ${{ matrix.os == 'macos-14' }} - run: | - mkdir -p core/src/main/resources/io/questdb/client/bin/darwin-aarch64/ - mkdir -p core/src/main/bin/darwin-aarch64/ - cp core/target/classes/io/questdb/client/bin-local/libquestdb.dylib core/src/main/resources/io/questdb/client/bin/darwin-aarch64/ - - name: Copy darwin-x86-64 CXX Library to the final directory - if: ${{ matrix.os == 'macos-15-intel' }} - run: | - mkdir -p core/src/main/resources/io/questdb/client/bin/darwin-x86-64/ - mkdir -p core/src/main/bin/darwin-x86-64/ - cp core/target/classes/io/questdb/client/bin-local/libquestdb.dylib core/src/main/resources/io/questdb/client/bin/darwin-x86-64/ - - name: Save darwin-aarch64 Libraries to Cache - if: ${{ matrix.os == 'macos-14' }} - uses: actions/cache/save@v3 - with: - path: | - core/src/main/resources/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib - key: nativelibs-armosx-${{ github.sha }} - - name: Save darwin-x86-64 Libraries to Cache - if: ${{ matrix.os == 'macos-15-intel' }} - uses: actions/cache/save@v3 - with: - path: | - core/src/main/resources/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib - key: nativelibs-osx-${{ github.sha }} - build-all-linux-x86-64: - runs-on: ubuntu-latest - # manylinux2014 is a container with new-ish compilers and tools, but old glibc - 2.17 - # 2.17 is old enough to be compatible with most Linux distributions out there - container: - image: quay.io/pypa/manylinux2014_x86_64 - volumes: - - /node20217:/node20217 - - /node20217:/__e/node20 - steps: - - name: Install tools, most are needed to build nasm - run: | - ldd --version - yum update -y - yum install 'perl(Env)' perl-Font-TTF perl-Sort-Versions gcc wget perf asciidoc xmlto ghostscript adobe-source-sans-pro-fonts adobe-source-code-pro-fonts rpm-build zstd curl -y - - name: Build nasm - # we need nasm 2.14+ due to this bug https://bugzilla.nasm.us/show_bug.cgi?id=3392205 - # manylinux2014 distribution includes nasm 2.10 - # the nasm project itself provides RPMs, but they built against a newer glibc and other dependencies too - # thus we take src.rpm from nasm project and rebuild it in the manylinux2014 container - # this way we get a nasm binary that is compatible with the manylinux2014 environment - run: | - wget https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/linux/nasm-2.16.03-0.fc39.src.rpm - rpmbuild --rebuild ./nasm-2.16.03-0.fc39.src.rpm - rpm -i ~/rpmbuild/RPMS/x86_64/nasm-2.16.03-0.el7.x86_64.rpm - - name: Install Node.js 20 glibc2.17 - # A hack to override default nodejs 20 to a build compatible with older glibc. - # Inspired by https://github.com/pytorch/test-infra/pull/5959 If it's good for pytorch, it's good for us too! :) - # Q: Why do we need this hack at all? A: Because many github actions, include action/checkout@v4, depend on nodejs 20. - # GitHub Actions runner provides a build of nodejs 20 that requires a newer glibc than manylinux2014 has. - # Thus we download a build of nodejs 20 that is compatible with manylinux2014 and override the default one. - run: | - curl -LO https://unofficial-builds.nodejs.org/download/release/v20.9.0/node-v20.9.0-linux-x64-glibc-217.tar.xz - tar -xf node-v20.9.0-linux-x64-glibc-217.tar.xz --strip-components 1 -C /node20217 - ldd /__e/node20/bin/node - - uses: actions/checkout@v4 - with: - submodules: true - - name: Install up-to-date CMake - run: | - wget -nv https://github.com/Kitware/CMake/releases/download/v3.29.2/cmake-3.29.2-linux-x86_64.tar.gz - tar -zxf cmake-3.29.2-linux-x86_64.tar.gz - echo "PATH=`pwd`/cmake-3.29.2-linux-x86_64/bin/:$PATH" >> "$GITHUB_ENV" - - name: Install GraalVM JDK 25 (for jni.h) - run: | - wget -nv -O graalvm.tar.gz https://download.oracle.com/graalvm/25/latest/graalvm-jdk-25_linux-x64_bin.tar.gz - mkdir graalvm - tar xfz graalvm.tar.gz -C graalvm --strip-components=1 - echo "JAVA_HOME=`pwd`/graalvm" >> "$GITHUB_ENV" - - name: Generate Makefiles - run: | - cd ./core - # git submodule update --init - cmake -DCMAKE_BUILD_TYPE=Release -B cmake-build-release -S. - - name: Build linux-x86-64 CXX Library - run: | - cd core - cmake --build cmake-build-release --config Release - mkdir -p src/main/resources/io/questdb/client/bin/linux-x86-64/ - mkdir -p src/main/bin/linux-x86-64/ - cp target/classes/io/questdb/client/bin-local/libquestdb.so src/main/resources/io/questdb/client/bin/linux-x86-64/ - - name: Save linux-x86-64 Libraries to Cache - uses: actions/cache/save@v3 - with: - path: | - core/src/main/resources/io/questdb/client/bin/linux-x86-64/libquestdb.so - key: nativelibs-linux-${{ github.sha }} - build-all-linux-aarch64: - runs-on: ubuntu-22.04-arm - container: - image: quay.io/pypa/manylinux_2_28_aarch64 - steps: - - uses: actions/checkout@v4 - with: - submodules: true - - name: Install tooling - run: | - yum update -y - yum install wget nasm zstd -y - - name: Install GraalVM JDK 25 (for jni.h) - run: | - wget -v --timeout=180 -O graalvm.tar.gz https://download.oracle.com/graalvm/25/latest/graalvm-jdk-25_linux-aarch64_bin.tar.gz - mkdir graalvm - tar xfz graalvm.tar.gz -C graalvm --strip-components=1 - echo "JAVA_HOME=`pwd`/graalvm" >> "$GITHUB_ENV" - - name: CMAKE linux-aarch64 - run: | - cd ./core - cmake -DCMAKE_TOOLCHAIN_FILE=./src/main/c/toolchains/linux-arm64.cmake -DCMAKE_BUILD_TYPE=Release -B cmake-build-release-arm64 -S. - - name: Build linux-aarch64 CXX Library - run: | - cd core - cmake --build cmake-build-release-arm64 --config Release - mkdir -p src/main/resources/io/questdb/client/bin/linux-aarch64/ - mkdir -p src/main/bin/linux-aarch64/ - cp target/classes/io/questdb/client/bin-local/libquestdb.so src/main/resources/io/questdb/client/bin/linux-aarch64/ - - name: Save linux-aarch64 Libraries to Cache - uses: actions/cache/save@v3 - with: - path: | - core/src/main/resources/io/questdb/client/bin/linux-aarch64/libquestdb.so - key: nativelibs-armlinux-${{ github.sha }} - build-cxx-windows: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - with: - submodules: true - - name: Increase file count and install tooling - run: | - sudo sysctl -w fs.file-max=500000 - sudo apt-get update -y - sudo apt-get install -y nasm gcc-mingw-w64 g++-mingw-w64 - - name: Install GraalVM JDK 25 (for jni.h) - run: | - wget -nv -O graalvm.tar.gz https://download.oracle.com/graalvm/25/latest/graalvm-jdk-25_linux-x64_bin.tar.gz - mkdir graalvm - tar xfz graalvm.tar.gz -C graalvm --strip-components=1 - echo "JAVA_HOME=`pwd`/graalvm" >> "$GITHUB_ENV" - - name: Download windows jni_md.h from JDK 25 - run: | - cd core - curl https://raw.githubusercontent.com/openjdk/jdk25u/master/src/java.base/windows/native/include/jni_md.h > $JAVA_HOME/include/jni_md.h - - name: CMake Windows - run: | - cd core - cmake -DCMAKE_TOOLCHAIN_FILE=./src/main/c/toolchains/windows-x86_64.cmake -DCMAKE_CROSSCOMPILING=True -DCMAKE_BUILD_TYPE=Release -B cmake-build-release-win64 - - name: Build Windows CXX Library - run: | - cd core - cmake --build cmake-build-release-win64 --config Release - mkdir -p src/main/resources/io/questdb/client/bin/windows-x86-64/ - cp target/classes/io/questdb/client/bin-local/libquestdb.dll src/main/resources/io/questdb/client/bin/windows-x86-64/ - - name: Check CXX runtime dependency - run: | - cd ./core - if x86_64-w64-mingw32-objdump -p ./src/main/resources/io/questdb/client/bin/windows-x86-64/libquestdb.dll | grep -q libstdc++; then - echo "Failure: CXX runtime dependency detected" - exit 1 - fi - - name: Check git status - run: | - git status - - name: Save Windows CXX Library to Cache - uses: actions/cache/save@v3 - with: - path: | - core/src/main/resources/io/questdb/client/bin/windows-x86-64/libquestdb.dll - key: nativelibs-windows-${{ github.sha }} - collect-commit-and-push: - needs: - [ - build-all-macos, - build-cxx-windows, - build-all-linux-x86-64, - build-all-linux-aarch64, - ] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Print file sizes before - run: | - mkdir -p ./core/src/main/resources/io/questdb/client/bin/ - find ./core/src/main/resources/io/questdb/client/bin/ -type f -exec ls -l {} \; || true - - name: Restore darwin-aarch64 Libraries from Cache - uses: actions/cache/restore@v3 - with: - path: | - core/src/main/resources/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib - key: nativelibs-armosx-${{ github.sha }} - - name: Restore darwin-x86-64 Libraries from Cache - uses: actions/cache/restore@v3 - with: - path: | - core/src/main/resources/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib - key: nativelibs-osx-${{ github.sha }} - - name: Restore linux-x86-64 Libraries from Cache - uses: actions/cache/restore@v3 - with: - path: | - core/src/main/resources/io/questdb/client/bin/linux-x86-64/libquestdb.so - key: nativelibs-linux-${{ github.sha }} - - name: Restore linux-aarch64 Libraries from Cache - uses: actions/cache/restore@v3 - with: - path: | - core/src/main/resources/io/questdb/client/bin/linux-aarch64/libquestdb.so - key: nativelibs-armlinux-${{ github.sha }} - - name: Restore Windows CXX Library from Cache - uses: actions/cache/restore@v3 - with: - path: | - core/src/main/resources/io/questdb/client/bin/windows-x86-64/libquestdb.dll - key: nativelibs-windows-${{ github.sha }} - - name: Check git status before - run: | - git status - - name: Commit the files - run: | - git config --global user.name 'GitHub Actions - Rebuild Native Libraries' - git config --global user.email 'jaromir@questdb.io' - git add core/src/main/resources/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib - git add core/src/main/resources/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib - git add core/src/main/resources/io/questdb/client/bin/linux-x86-64/libquestdb.so - git add core/src/main/resources/io/questdb/client/bin/linux-aarch64/libquestdb.so - git add core/src/main/resources/io/questdb/client/bin/windows-x86-64/libquestdb.dll - - echo "Removing exec permissions in Git index..." - git update-index --chmod=-x core/src/main/resources/io/questdb/client/bin/linux-x86-64/libquestdb.so - git update-index --chmod=-x core/src/main/resources/io/questdb/client/bin/linux-aarch64/libquestdb.so - git update-index --chmod=-x core/src/main/resources/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib - git update-index --chmod=-x core/src/main/resources/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib - - git commit -m "Rebuild CXX libraries" - - name: Check git status after - run: | - git status - - name: Print file sizes after - run: | - find ./core/src/main/resources/io/questdb/client/bin/ -type f -exec ls -l {} \; - - name: Push the files to the current branch - uses: ad-m/github-push-action@d91a481090679876dfc4178fef17f286781251df - # Why do we use a commit hash instead of a tag for the github-push-action? - # ad-m/github-push-action is not as well-known repo as e.g. actions/checkout, and therefore we trust it less. - # d91a48109 is the same as the tag v0.8.0, but it's guaranteed to be immutable. - # So even if a bad actor takes over the repo, and rewrites tags to point to malicious commits, we will still be safe. - with: - branch: ${{ github.head_ref || github.ref_name }} diff --git a/README.md b/README.md index d3b77bf9..bc90fad3 100644 --- a/README.md +++ b/README.md @@ -240,34 +240,22 @@ 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. +Maven Central publishing is owned by the manually triggered `Release to Maven Central` 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. +The workflow runs `mvn release:prepare`, lets Maven infer the release version from the current `-SNAPSHOT` POM, builds +all native libraries from the generated release tag, stores them as GitHub Actions artifacts, downloads them into the +Maven build, and publishes the final JAR to Maven Central. -Normal release flow: +Run the workflow from the Actions tab with `dry_run=true` first. Dry run creates the local Maven release state, builds +native artifacts, and deploys to a local file repository inside the workflow; it does not push commits, push tags, or +contact Maven Central. For the real release, rerun with `dry_run=false`. -```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" -``` +The real Maven Central publish job uses the `maven-release` GitHub environment, so configure that environment with +required reviewers to pause before immutable Central publishing. 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 +workflow. Running a local deploy while the workflow is also publishing creates competing Sonatype deployments for the same coordinate. Full release procedure: [artifacts/release/README.md](artifacts/release/README.md). diff --git a/artifacts/release/README.md b/artifacts/release/README.md index ace57205..4bff00d8 100644 --- a/artifacts/release/README.md +++ b/artifacts/release/README.md @@ -1,122 +1,88 @@ # Release steps -Steps to release `org.questdb:questdb-client` to Maven Central. Examples below use `1.2.2` (release) and -`1.2.3-SNAPSHOT` (next snapshot); substitute the actual versions when running. +Steps to release `org.questdb:questdb-client` to Maven Central. -**Prerequisite:** tag creation is restricted by an org-wide ruleset, so you must be a member of the `questdb/release` -team to push the release tag. Confirm membership before starting. +The release is owned by the manually triggered +[`Release to Maven Central`](../../.github/workflows/maven_central_release.yml) workflow. Do not create release tags +by hand, do not push version tags to trigger publishing, and do not run `mvn deploy` locally during the normal release +path. -## Edit release notes +## Prepare release notes -Create a draft release with the intended version and notes. Do not create the git tag up front -- pick the tag name -in the draft and let GitHub create it when the release is published. Match the style of previous release notes. +Create a draft GitHub release with the intended version and notes. Do not create the git tag up front. The workflow +creates the tag through `mvn release:prepare`; finalize the GitHub release after Maven Central propagation. -## Create a release branch +## Run a dry run -Direct pushes to `main` are blocked by the org ruleset (one-approval squash-merged PR is the only path), so release -commits live on a dedicated branch. +Start the `Release to Maven Central` workflow from the Actions tab. -```bash -git fetch -git checkout main -git pull -git checkout -b release/1.2.2 -``` +Use these inputs: -Make sure your working tree is clean. +- `source_ref`: branch/ref to release from, usually `main` +- `dry_run`: `true` +- `release_version_override`: blank unless doing a non-standard version +- `next_development_version_override`: blank unless doing a non-standard next snapshot -## Clear previous release "memory" +The workflow will: -```bash -mvn release:clean -``` +- check out `source_ref` +- run `mvn release:clean` +- run `mvn release:prepare` without pushing changes +- infer the release version from the current `-SNAPSHOT` POM +- capture the generated Maven release tag from `release.properties` +- archive the generated release source +- build all native libraries from that release source +- store native libraries as GitHub Actions artifacts +- download the native artifacts into `core/target/native-libs` +- package them into the JAR through the `include-native-artifacts` Maven profile +- sign the Maven artifacts with a temporary dry-run GPG key +- deploy to a local file repository in the workflow instead of Maven Central +- upload the local file repository as the `dry-run-maven-repository` GitHub Actions artifact +- upload the direct `core/target` build outputs as the `dry-run-core-target-artifacts` GitHub Actions artifact -Removes any `release.properties` and `*.releaseBackup` files left over from a previous attempt. +A dry run must not push commits, push tags, request AWS credentials, request Sonatype credentials, or contact +Sonatype/Maven Central. -## Roll versions and create the tag +## Publish -`release:prepare` will: +After the dry run has been reviewed, rerun the same workflow with: -- roll parent and module versions from snapshot to release (`1.2.2-SNAPSHOT` -> `1.2.2`) -- commit the release POMs -- create the release tag locally -- roll the versions to the next snapshot (`1.2.3-SNAPSHOT`) -- commit the next-snapshot POMs +- the same `source_ref` +- `dry_run`: `false` +- override inputs still blank unless intentionally needed -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. +The real release path uses the generated Maven tag as the source of truth. It pushes the `mvn release:prepare` commits +back to `source_ref`, pushes the generated release tag, builds native libraries from that tag, downloads the native +artifacts into the Maven build, signs the release artifacts, and uploads through the Sonatype Central Portal. -```bash -mvn -B release:prepare \ - -DautoVersionSubmodules=true \ - -DpushChanges=false \ - -DreleaseVersion=1.2.2 \ - -DdevelopmentVersion=1.2.3-SNAPSHOT \ - -Dtag=1.2.2 -``` +The final Central upload runs in the `publish-central` job, which is attached to the `maven-release` GitHub +environment. Configure that environment with required reviewers so the workflow pauses before the immutable Maven +Central publish step. -`-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. +The workflow returns once Sonatype has validated the upload and taken ownership of the artifacts. Physical propagation +to Maven Central happens asynchronously after the workflow finishes, so a green run does not guarantee the artifacts +are immediately visible on `central.sonatype.com`. -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. +## Versioning -If `release:prepare` fails partway through: +In the normal path, leave both version override inputs blank. `mvn release:prepare` derives: -```bash -mvn release:rollback -git tag -d 1.2.2 -``` +- release version from the current POM, for example `1.3.2-SNAPSHOT` -> `1.3.2` +- release tag from the release version, via `tagNameFormat=@{project.version}` +- next development version from Maven Release Plugin defaults, for example `1.3.3-SNAPSHOT` -`release:rollback` reverts the prepare commits and removes the backup files but does **not** delete the tag -- drop -it manually or the next attempt at the same version fails. If `release.properties` is already gone, use -`git reset --hard ` instead (and still drop the tag). +Use `release_version_override` and `next_development_version_override` only for non-standard releases. -## Push the release branch and tag +## Failure handling -Before pushing, verify the tag points at the release commit and that the tagged POM version is not a snapshot: +If a dry run fails, fix the workflow or source branch and rerun it. Dry runs do not push release commits or tags. -```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 -``` - -The tag push triggers the Maven Central workflow (see below). The branch is merged to `main` afterwards -- see -[Merge the release branch to `main`](#merge-the-release-branch-to-main). - -## Publish to Maven Central - -The [`Release to Maven Central`](../../.github/workflows/maven_central_release.yml) workflow fires automatically when -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 -- 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 -propagation to Maven Central happens asynchronously after the workflow finishes, so a green run does **not** mean the -artifacts are visible on `central.sonatype.com` yet -- that step is covered under [Post-release](#post-release). - -## Merge the release branch to `main` - -Once the workflow finishes, open a PR from `release/1.2.2` to `main` and squash-merge it after approval. Delete the -release branch afterwards. You do not need to wait for Maven Central propagation before merging -- once the workflow -is green, Sonatype owns the artifacts and the next snapshot version on `main` is the source of truth for ongoing -development. - -Squash-merge is the only merge method allowed by the org ruleset on `main`, so the original `[maven-release-plugin]` -commits will not appear in `main`'s history. The tag remains the canonical pointer to the released code; `main` -carries a single squashed commit that bumps the snapshot version. +If a real release fails before the final Maven Central upload, inspect whether the prepare commits or tag were pushed. +If they were pushed, either rerun from the same source state after fixing the workflow or clean up the failed release +state deliberately. Do not reuse a version that may already have reached Maven Central; Maven Central coordinates are +immutable. ## Post-release -After the workflow completes, Sonatype still has to propagate the artifacts to Maven Central. This typically takes a -few minutes but can occasionally run longer. Check -[Maven Central](https://central.sonatype.com/artifact/org.questdb/questdb-client) until the new version is listed, -then finalize the GitHub release draft against the new tag and add the release notes. +Check [Maven Central](https://central.sonatype.com/artifact/org.questdb/questdb-client) until the new version is +listed, then finalize the GitHub release draft against the generated tag and add the release notes. diff --git a/core/pom.xml b/core/pom.xml index eb3dfdf5..618ef9a0 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -287,7 +287,40 @@ - maven-central-release + include-native-artifacts + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + copy-release-native-artifacts + process-resources + + copy-resources + + + ${project.build.outputDirectory} + + + ${project.build.directory}/native-libs + false + + io/questdb/client/bin/** + + + + + + + + + + + + release-artifacts @@ -350,6 +383,13 @@ + + + + + maven-central-publish + + org.sonatype.central central-publishing-maven-plugin @@ -364,6 +404,22 @@ + + release-dry-run-deploy + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.4 + + false + dry-run::default::file://${project.build.directory}/dry-run-repo + + + + + jacoco diff --git a/pom.xml b/pom.xml index af869033..d63b1061 100644 --- a/pom.xml +++ b/pom.xml @@ -75,7 +75,6 @@ @{project.version} -Dmaven.test.skipTests=true -Dmaven.test.skip=true - maven-central-release [maven-release-plugin] [maven-release-plugin] prepare release @{releaseLabel} From be9dfd9e757f3e2496e2eb36af6130ca6473c023 Mon Sep 17 00:00:00 2001 From: Steven Sklar Date: Wed, 27 May 2026 20:46:43 -0400 Subject: [PATCH 05/15] review comments --- .github/actions/stage-native-libs/action.yml | 44 ++++++++ .github/workflows/maven_central_release.yml | 106 +++++++------------ README.md | 7 +- artifacts/release/README.md | 5 +- 4 files changed, 88 insertions(+), 74 deletions(-) create mode 100644 .github/actions/stage-native-libs/action.yml diff --git a/.github/actions/stage-native-libs/action.yml b/.github/actions/stage-native-libs/action.yml new file mode 100644 index 00000000..91b479e7 --- /dev/null +++ b/.github/actions/stage-native-libs/action.yml @@ -0,0 +1,44 @@ +name: Stage native libraries for Maven +description: > + Download the per-platform native library artifacts uploaded by the build-* jobs + and stage them under core/target/native-libs/io/questdb/client/bin// + in the layout the include-native-artifacts Maven profile expects. + +runs: + using: composite + steps: + - name: Download native artifacts + uses: actions/download-artifact@v4 + with: + pattern: native-* + path: core/target/downloaded-native-artifacts + merge-multiple: false + + - name: Stage native artifacts for Maven + shell: bash + run: | + mkdir -p core/target/native-libs/io/questdb/client/bin/darwin-aarch64 + mkdir -p core/target/native-libs/io/questdb/client/bin/darwin-x86-64 + mkdir -p core/target/native-libs/io/questdb/client/bin/linux-aarch64 + mkdir -p core/target/native-libs/io/questdb/client/bin/linux-x86-64 + mkdir -p core/target/native-libs/io/questdb/client/bin/windows-x86-64 + cp core/target/downloaded-native-artifacts/native-darwin-aarch64/libquestdb.dylib core/target/native-libs/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib + cp core/target/downloaded-native-artifacts/native-darwin-x86-64/libquestdb.dylib core/target/native-libs/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib + cp core/target/downloaded-native-artifacts/native-linux-aarch64/libquestdb.so core/target/native-libs/io/questdb/client/bin/linux-aarch64/libquestdb.so + cp core/target/downloaded-native-artifacts/native-linux-x86-64/libquestdb.so core/target/native-libs/io/questdb/client/bin/linux-x86-64/libquestdb.so + cp core/target/downloaded-native-artifacts/native-windows-x86-64/libquestdb.dll core/target/native-libs/io/questdb/client/bin/windows-x86-64/libquestdb.dll + + expected=( + core/target/native-libs/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib + core/target/native-libs/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib + core/target/native-libs/io/questdb/client/bin/linux-aarch64/libquestdb.so + core/target/native-libs/io/questdb/client/bin/linux-x86-64/libquestdb.so + core/target/native-libs/io/questdb/client/bin/windows-x86-64/libquestdb.dll + ) + + for file_name in "${expected[@]}"; do + if [[ ! -s "${file_name}" ]]; then + echo "::error::Missing native artifact ${file_name}." + exit 1 + fi + done diff --git a/.github/workflows/maven_central_release.yml b/.github/workflows/maven_central_release.yml index a1acebb1..7f83e8e9 100644 --- a/.github/workflows/maven_central_release.yml +++ b/.github/workflows/maven_central_release.yml @@ -21,6 +21,11 @@ on: required: true default: true type: boolean + confirm_publish: + description: "Type 'PUBLISH' to confirm an immutable Maven Central upload. Ignored when dry_run=true." + required: false + default: "" + type: string permissions: contents: read @@ -410,40 +415,8 @@ jobs: java-version: "11" cache: maven - - name: Download native artifacts - uses: actions/download-artifact@v4 - with: - pattern: native-* - path: core/target/downloaded-native-artifacts - merge-multiple: false - - - name: Stage native artifacts for Maven - run: | - mkdir -p core/target/native-libs/io/questdb/client/bin/darwin-aarch64 - mkdir -p core/target/native-libs/io/questdb/client/bin/darwin-x86-64 - mkdir -p core/target/native-libs/io/questdb/client/bin/linux-aarch64 - mkdir -p core/target/native-libs/io/questdb/client/bin/linux-x86-64 - mkdir -p core/target/native-libs/io/questdb/client/bin/windows-x86-64 - cp core/target/downloaded-native-artifacts/native-darwin-aarch64/libquestdb.dylib core/target/native-libs/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib - cp core/target/downloaded-native-artifacts/native-darwin-x86-64/libquestdb.dylib core/target/native-libs/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib - cp core/target/downloaded-native-artifacts/native-linux-aarch64/libquestdb.so core/target/native-libs/io/questdb/client/bin/linux-aarch64/libquestdb.so - cp core/target/downloaded-native-artifacts/native-linux-x86-64/libquestdb.so core/target/native-libs/io/questdb/client/bin/linux-x86-64/libquestdb.so - cp core/target/downloaded-native-artifacts/native-windows-x86-64/libquestdb.dll core/target/native-libs/io/questdb/client/bin/windows-x86-64/libquestdb.dll - - expected=( - core/target/native-libs/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib - core/target/native-libs/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib - core/target/native-libs/io/questdb/client/bin/linux-aarch64/libquestdb.so - core/target/native-libs/io/questdb/client/bin/linux-x86-64/libquestdb.so - core/target/native-libs/io/questdb/client/bin/windows-x86-64/libquestdb.dll - ) - - for file_name in "${expected[@]}"; do - if [[ ! -s "${file_name}" ]]; then - echo "::error::Missing native artifact ${file_name}." - exit 1 - fi - done + - name: Stage native libraries for Maven + uses: ./.github/actions/stage-native-libs - name: Generate dry-run signing key run: | @@ -451,7 +424,7 @@ jobs: chmod 700 "$GNUPGHOME" cat > keyparams <<'EOF' Key-Type: RSA - Key-Length: 3072 + Key-Length: 2048 Name-Real: QuestDB Maven Release Dry Run Name-Email: dry-run@questdb.io Expire-Date: 0 @@ -525,6 +498,15 @@ jobs: MAVEN_RELEASE_AWS_ROLE_ARN: ${{ secrets.MAVEN_RELEASE_AWS_ROLE_ARN }} MAVEN_RELEASE_AWS_SECRET_ARN: ${{ secrets.MAVEN_RELEASE_AWS_SECRET_ARN }} steps: + - name: Verify confirm_publish input + env: + CONFIRM_PUBLISH: ${{ inputs.confirm_publish }} + run: | + if [[ "${CONFIRM_PUBLISH}" != "PUBLISH" ]]; then + echo "::error::Maven Central publish requires confirm_publish=PUBLISH. Got: '${CONFIRM_PUBLISH}'." + exit 1 + fi + - name: Validate workflow configuration run: | required_vars=( @@ -562,40 +544,26 @@ jobs: java-version: "11" cache: maven - - name: Download native artifacts - uses: actions/download-artifact@v4 - with: - pattern: native-* - path: core/target/downloaded-native-artifacts - merge-multiple: false - - - name: Stage native artifacts for Maven - run: | - mkdir -p core/target/native-libs/io/questdb/client/bin/darwin-aarch64 - mkdir -p core/target/native-libs/io/questdb/client/bin/darwin-x86-64 - mkdir -p core/target/native-libs/io/questdb/client/bin/linux-aarch64 - mkdir -p core/target/native-libs/io/questdb/client/bin/linux-x86-64 - mkdir -p core/target/native-libs/io/questdb/client/bin/windows-x86-64 - cp core/target/downloaded-native-artifacts/native-darwin-aarch64/libquestdb.dylib core/target/native-libs/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib - cp core/target/downloaded-native-artifacts/native-darwin-x86-64/libquestdb.dylib core/target/native-libs/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib - cp core/target/downloaded-native-artifacts/native-linux-aarch64/libquestdb.so core/target/native-libs/io/questdb/client/bin/linux-aarch64/libquestdb.so - cp core/target/downloaded-native-artifacts/native-linux-x86-64/libquestdb.so core/target/native-libs/io/questdb/client/bin/linux-x86-64/libquestdb.so - cp core/target/downloaded-native-artifacts/native-windows-x86-64/libquestdb.dll core/target/native-libs/io/questdb/client/bin/windows-x86-64/libquestdb.dll - - expected=( - core/target/native-libs/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib - core/target/native-libs/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib - core/target/native-libs/io/questdb/client/bin/linux-aarch64/libquestdb.so - core/target/native-libs/io/questdb/client/bin/linux-x86-64/libquestdb.so - core/target/native-libs/io/questdb/client/bin/windows-x86-64/libquestdb.dll - ) + - name: Verify checked-out tag matches POM version + env: + RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }} + run: | + pom_version="$(mvn -B -q -N -DforceStdout help:evaluate -Dexpression=project.version)" + if [[ -z "${pom_version}" ]]; then + echo "::error::Could not read project version from checked-out tag." + exit 1 + fi + if [[ "${pom_version}" == *-SNAPSHOT ]]; then + echo "::error::Refusing to release SNAPSHOT version ${pom_version}." + exit 1 + fi + if [[ "${RELEASE_TAG}" != "${pom_version}" ]]; then + echo "::error::Release tag ${RELEASE_TAG} does not match POM version ${pom_version}." + exit 1 + fi - for file_name in "${expected[@]}"; do - if [[ ! -s "${file_name}" ]]; then - echo "::error::Missing native artifact ${file_name}." - exit 1 - fi - done + - name: Stage native libraries for Maven + uses: ./.github/actions/stage-native-libs - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 @@ -628,7 +596,7 @@ jobs: - name: Configure Maven settings.xml run: | - if [[ -z "${MAVEN_GPG_PASSPHRASE+x}" ]]; then + if [[ -z "${MAVEN_GPG_PASSPHRASE:-}" ]]; then echo "MAVEN_GPG_PASSPHRASE=" >> "$GITHUB_ENV" fi mkdir -p "$HOME/.m2" diff --git a/README.md b/README.md index bc90fad3..5e55b7ca 100644 --- a/README.md +++ b/README.md @@ -249,10 +249,11 @@ Maven build, and publishes the final JAR to Maven Central. Run the workflow from the Actions tab with `dry_run=true` first. Dry run creates the local Maven release state, builds native artifacts, and deploys to a local file repository inside the workflow; it does not push commits, push tags, or -contact Maven Central. For the real release, rerun with `dry_run=false`. +contact Maven Central. For the real release, rerun with `dry_run=false` and `confirm_publish=PUBLISH` (the publish job +refuses to run without the literal string). -The real Maven Central publish job uses the `maven-release` GitHub environment, so configure that environment with -required reviewers to pause before immutable Central publishing. +The real Maven Central publish job uses the `maven-release` GitHub environment as an additional gate; configure that +environment with required reviewers so the workflow also pauses for human approval before immutable Central publishing. 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 workflow is also publishing creates competing Sonatype deployments for diff --git a/artifacts/release/README.md b/artifacts/release/README.md index 4bff00d8..f539ec50 100644 --- a/artifacts/release/README.md +++ b/artifacts/release/README.md @@ -49,6 +49,7 @@ After the dry run has been reviewed, rerun the same workflow with: - the same `source_ref` - `dry_run`: `false` +- `confirm_publish`: `PUBLISH` (literal string; the publish job fails fast on any other value) - override inputs still blank unless intentionally needed The real release path uses the generated Maven tag as the source of truth. It pushes the `mvn release:prepare` commits @@ -56,8 +57,8 @@ back to `source_ref`, pushes the generated release tag, builds native libraries artifacts into the Maven build, signs the release artifacts, and uploads through the Sonatype Central Portal. The final Central upload runs in the `publish-central` job, which is attached to the `maven-release` GitHub -environment. Configure that environment with required reviewers so the workflow pauses before the immutable Maven -Central publish step. +environment. As a second layer of protection, configure that environment with required reviewers so the workflow also +pauses for human approval before the immutable Maven Central publish step. The workflow returns once Sonatype has validated the upload and taken ownership of the artifacts. Physical propagation to Maven Central happens asynchronously after the workflow finishes, so a green run does not guarantee the artifacts From 514b10592d0e3f40697277ceac6197e084d99c78 Mon Sep 17 00:00:00 2001 From: Steven Sklar Date: Wed, 27 May 2026 21:12:13 -0400 Subject: [PATCH 06/15] ugh --- .github/workflows/maven_central_release.yml | 182 ------------ .github/workflows/rebuild_native_libs.yml | 296 ++++++++++++++++++++ README.md | 7 +- artifacts/release/README.md | 38 +-- core/pom.xml | 16 -- 5 files changed, 301 insertions(+), 238 deletions(-) create mode 100644 .github/workflows/rebuild_native_libs.yml diff --git a/.github/workflows/maven_central_release.yml b/.github/workflows/maven_central_release.yml index 7f83e8e9..b355c15b 100644 --- a/.github/workflows/maven_central_release.yml +++ b/.github/workflows/maven_central_release.yml @@ -16,16 +16,6 @@ on: description: "Optional next development version override; normally inferred by maven-release-plugin" required: false type: string - dry_run: - description: "Exercise prepare, native builds, and local deploy without pushing or publishing" - required: true - default: true - type: boolean - confirm_publish: - description: "Type 'PUBLISH' to confirm an immutable Maven Central upload. Ignored when dry_run=true." - required: false - default: "" - type: string permissions: contents: read @@ -44,7 +34,6 @@ jobs: release_tag: ${{ steps.release-tag.outputs.release_tag }} env: SOURCE_REF: ${{ inputs.source_ref }} - DRY_RUN: ${{ inputs.dry_run }} RELEASE_VERSION_OVERRIDE: ${{ inputs.release_version_override }} NEXT_DEVELOPMENT_VERSION_OVERRIDE: ${{ inputs.next_development_version_override }} steps: @@ -110,27 +99,10 @@ jobs: echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT" - name: Push release commits and tag - if: ${{ !inputs.dry_run }} run: | git push origin "HEAD:${SOURCE_REF}" git push origin "refs/tags/${{ steps.release-tag.outputs.release_tag }}" - - name: Archive dry-run release source - if: ${{ inputs.dry_run }} - run: | - git checkout "${{ steps.release-tag.outputs.release_tag }}" - git submodule update --init --recursive - mkdir -p /tmp/release-source - tar --exclude-vcs --exclude=target -cf /tmp/release-source/release-source.tar . - - - name: Upload dry-run release source - if: ${{ inputs.dry_run }} - uses: actions/upload-artifact@v4 - with: - name: release-source - path: /tmp/release-source/release-source.tar - if-no-files-found: error - build-macos: needs: prepare strategy: @@ -144,24 +116,11 @@ jobs: timeout-minutes: 60 steps: - name: Check out release tag - if: ${{ !inputs.dry_run }} uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{ needs.prepare.outputs.release_tag }} submodules: true - - name: Download dry-run release source - if: ${{ inputs.dry_run }} - uses: actions/download-artifact@v4 - with: - name: release-source - path: target/release-source - - - name: Extract dry-run release source - if: ${{ inputs.dry_run }} - run: | - tar -xf target/release-source/release-source.tar - - name: Install toolchains run: | brew uninstall cmake || true @@ -215,24 +174,11 @@ jobs: ldd /__e/node20/bin/node - name: Check out release tag - if: ${{ !inputs.dry_run }} uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{ needs.prepare.outputs.release_tag }} submodules: true - - name: Download dry-run release source - if: ${{ inputs.dry_run }} - uses: actions/download-artifact@v4 - with: - name: release-source - path: target/release-source - - - name: Extract dry-run release source - if: ${{ inputs.dry_run }} - run: | - tar -xf target/release-source/release-source.tar - - name: Install up-to-date CMake run: | wget -nv https://github.com/Kitware/CMake/releases/download/v3.29.2/cmake-3.29.2-linux-x86_64.tar.gz @@ -272,24 +218,11 @@ jobs: image: quay.io/pypa/manylinux_2_28_aarch64 steps: - name: Check out release tag - if: ${{ !inputs.dry_run }} uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{ needs.prepare.outputs.release_tag }} submodules: true - - name: Download dry-run release source - if: ${{ inputs.dry_run }} - uses: actions/download-artifact@v4 - with: - name: release-source - path: target/release-source - - - name: Extract dry-run release source - if: ${{ inputs.dry_run }} - run: | - tar -xf target/release-source/release-source.tar - - name: Install tooling run: | yum update -y @@ -326,24 +259,11 @@ jobs: timeout-minutes: 60 steps: - name: Check out release tag - if: ${{ !inputs.dry_run }} uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{ needs.prepare.outputs.release_tag }} submodules: true - - name: Download dry-run release source - if: ${{ inputs.dry_run }} - uses: actions/download-artifact@v4 - with: - name: release-source - path: target/release-source - - - name: Extract dry-run release source - if: ${{ inputs.dry_run }} - run: | - tar -xf target/release-source/release-source.tar - - name: Install tooling run: | sudo sysctl -w fs.file-max=500000 @@ -387,100 +307,7 @@ jobs: path: native-artifacts/windows-x86-64/libquestdb.dll if-no-files-found: error - dry-run-package: - if: ${{ inputs.dry_run }} - needs: - - prepare - - build-macos - - build-linux-x86-64 - - build-linux-aarch64 - - build-windows-x86-64 - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Download dry-run release source - uses: actions/download-artifact@v4 - with: - name: release-source - path: target/release-source - - - name: Extract dry-run release source - run: | - tar -xf target/release-source/release-source.tar - - - name: Set up Java 11 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 - with: - distribution: temurin - java-version: "11" - cache: maven - - - name: Stage native libraries for Maven - uses: ./.github/actions/stage-native-libs - - - name: Generate dry-run signing key - run: | - export GNUPGHOME="$(mktemp -d)" - chmod 700 "$GNUPGHOME" - cat > keyparams <<'EOF' - Key-Type: RSA - Key-Length: 2048 - Name-Real: QuestDB Maven Release Dry Run - Name-Email: dry-run@questdb.io - Expire-Date: 0 - %no-protection - %commit - EOF - gpg --batch --generate-key keyparams - echo "GNUPGHOME=$GNUPGHOME" >> "$GITHUB_ENV" - echo "MAVEN_GPG_PASSPHRASE=" >> "$GITHUB_ENV" - - - name: Configure Maven settings.xml - run: | - mkdir -p "$HOME/.m2" - cat > "$HOME/.m2/settings.xml" <<'EOF' - - - - gpg.passphrase - ${env.MAVEN_GPG_PASSPHRASE} - - - - EOF - - - name: Dry-run release packaging and deploy - run: | - mvn -B -ntp deploy -P release-artifacts,release-dry-run-deploy,include-native-artifacts -DskipTests - - - name: Upload dry-run Maven repository - uses: actions/upload-artifact@v4 - with: - name: dry-run-maven-repository - path: core/target/dry-run-repo - if-no-files-found: error - - - name: Upload dry-run built artifacts - uses: actions/upload-artifact@v4 - with: - name: dry-run-core-target-artifacts - path: | - core/target/*.jar - core/target/*.pom - core/target/*.asc - if-no-files-found: warn - - - name: Remove imported signing key - if: always() - run: | - if [[ -n "${GNUPGHOME:-}" && -d "${GNUPGHOME}" ]]; then - rm -rf "$GNUPGHOME" - fi - publish-central: - if: ${{ !inputs.dry_run }} needs: - prepare - build-macos @@ -498,15 +325,6 @@ jobs: MAVEN_RELEASE_AWS_ROLE_ARN: ${{ secrets.MAVEN_RELEASE_AWS_ROLE_ARN }} MAVEN_RELEASE_AWS_SECRET_ARN: ${{ secrets.MAVEN_RELEASE_AWS_SECRET_ARN }} steps: - - name: Verify confirm_publish input - env: - CONFIRM_PUBLISH: ${{ inputs.confirm_publish }} - run: | - if [[ "${CONFIRM_PUBLISH}" != "PUBLISH" ]]; then - echo "::error::Maven Central publish requires confirm_publish=PUBLISH. Got: '${CONFIRM_PUBLISH}'." - exit 1 - fi - - name: Validate workflow configuration run: | required_vars=( diff --git a/.github/workflows/rebuild_native_libs.yml b/.github/workflows/rebuild_native_libs.yml new file mode 100644 index 00000000..026d3c3e --- /dev/null +++ b/.github/workflows/rebuild_native_libs.yml @@ -0,0 +1,296 @@ +name: Build and Push Release CXX Libraries +on: + workflow_dispatch: +# This workflow is triggered manually from the Actions tab. +# It's meant to be run on a PR that changes the native C++ code. + +# It builds native libraries for all supported platforms and pushes them to the current branch. +# It splits the building process into 3 build jobs: +# 1. build-macos - Builds native libraries for MacOS: both ARM and x64. +# It uses native runners for each platform, because cross compilation on MacOS is complicated. +# 2. build-others - Builds native libraries for x64 Linux, ARM Linux and Windows. +# It uses cross-compilation for ARM Linux and Windows. +# +# Each build job saves the resulting binaries to the cache under a unique key +# When all build jobs are finished, the collect-commit-and-push job restores the binaries from the cache +# and pushes them to the current branch. + +jobs: + build-all-macos: + strategy: + matrix: + # macos-14 = ARM M1 + # macos-15-intel = x64 + # if you change OS definitions then you need to change conditions in cache-save steps below + os: [ macos-14, macos-15-intel ] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install toolchains (CXX/NASM) + run: | + # https://github.com/actions/runner-images/issues/12912 + # Temporary fix (mtopolnik 2025-09-02): GH Action pre-installs a custom cmake version, install then fails + brew uninstall cmake + brew install make cmake gcc nasm + - name: Build CXX Library + run: | + cd core + export MACOSX_DEPLOYMENT_TARGET=13.0 + cmake -B build/release -DCMAKE_BUILD_TYPE=Release + cmake --build build/release --config Release + - name: Copy darwin-aarch64 CXX Library to the final directory + if: ${{ matrix.os == 'macos-14' }} + run: | + mkdir -p core/src/main/resources/io/questdb/client/bin/darwin-aarch64/ + mkdir -p core/src/main/bin/darwin-aarch64/ + cp core/target/classes/io/questdb/client/bin-local/libquestdb.dylib core/src/main/resources/io/questdb/client/bin/darwin-aarch64/ + - name: Copy darwin-x86-64 CXX Library to the final directory + if: ${{ matrix.os == 'macos-15-intel' }} + run: | + mkdir -p core/src/main/resources/io/questdb/client/bin/darwin-x86-64/ + mkdir -p core/src/main/bin/darwin-x86-64/ + cp core/target/classes/io/questdb/client/bin-local/libquestdb.dylib core/src/main/resources/io/questdb/client/bin/darwin-x86-64/ + - name: Save darwin-aarch64 Libraries to Cache + if: ${{ matrix.os == 'macos-14' }} + uses: actions/cache/save@v3 + with: + path: | + core/src/main/resources/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib + key: nativelibs-armosx-${{ github.sha }} + - name: Save darwin-x86-64 Libraries to Cache + if: ${{ matrix.os == 'macos-15-intel' }} + uses: actions/cache/save@v3 + with: + path: | + core/src/main/resources/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib + key: nativelibs-osx-${{ github.sha }} + build-all-linux-x86-64: + runs-on: ubuntu-latest + # manylinux2014 is a container with new-ish compilers and tools, but old glibc - 2.17 + # 2.17 is old enough to be compatible with most Linux distributions out there + container: + image: quay.io/pypa/manylinux2014_x86_64 + volumes: + - /node20217:/node20217 + - /node20217:/__e/node20 + steps: + - name: Install tools, most are needed to build nasm + run: | + ldd --version + yum update -y + yum install 'perl(Env)' perl-Font-TTF perl-Sort-Versions gcc wget perf asciidoc xmlto ghostscript adobe-source-sans-pro-fonts adobe-source-code-pro-fonts rpm-build zstd curl -y + - name: Build nasm + # we need nasm 2.14+ due to this bug https://bugzilla.nasm.us/show_bug.cgi?id=3392205 + # manylinux2014 distribution includes nasm 2.10 + # the nasm project itself provides RPMs, but they built against a newer glibc and other dependencies too + # thus we take src.rpm from nasm project and rebuild it in the manylinux2014 container + # this way we get a nasm binary that is compatible with the manylinux2014 environment + run: | + wget https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/linux/nasm-2.16.03-0.fc39.src.rpm + rpmbuild --rebuild ./nasm-2.16.03-0.fc39.src.rpm + rpm -i ~/rpmbuild/RPMS/x86_64/nasm-2.16.03-0.el7.x86_64.rpm + - name: Install Node.js 20 glibc2.17 + # A hack to override default nodejs 20 to a build compatible with older glibc. + # Inspired by https://github.com/pytorch/test-infra/pull/5959 If it's good for pytorch, it's good for us too! :) + # Q: Why do we need this hack at all? A: Because many github actions, include action/checkout@v4, depend on nodejs 20. + # GitHub Actions runner provides a build of nodejs 20 that requires a newer glibc than manylinux2014 has. + # Thus we download a build of nodejs 20 that is compatible with manylinux2014 and override the default one. + run: | + curl -LO https://unofficial-builds.nodejs.org/download/release/v20.9.0/node-v20.9.0-linux-x64-glibc-217.tar.xz + tar -xf node-v20.9.0-linux-x64-glibc-217.tar.xz --strip-components 1 -C /node20217 + ldd /__e/node20/bin/node + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install up-to-date CMake + run: | + wget -nv https://github.com/Kitware/CMake/releases/download/v3.29.2/cmake-3.29.2-linux-x86_64.tar.gz + tar -zxf cmake-3.29.2-linux-x86_64.tar.gz + echo "PATH=`pwd`/cmake-3.29.2-linux-x86_64/bin/:$PATH" >> "$GITHUB_ENV" + - name: Install GraalVM JDK 25 (for jni.h) + run: | + wget -nv -O graalvm.tar.gz https://download.oracle.com/graalvm/25/latest/graalvm-jdk-25_linux-x64_bin.tar.gz + mkdir graalvm + tar xfz graalvm.tar.gz -C graalvm --strip-components=1 + echo "JAVA_HOME=`pwd`/graalvm" >> "$GITHUB_ENV" + - name: Generate Makefiles + run: | + cd ./core + # git submodule update --init + cmake -DCMAKE_BUILD_TYPE=Release -B cmake-build-release -S. + - name: Build linux-x86-64 CXX Library + run: | + cd core + cmake --build cmake-build-release --config Release + mkdir -p src/main/resources/io/questdb/client/bin/linux-x86-64/ + mkdir -p src/main/bin/linux-x86-64/ + cp target/classes/io/questdb/client/bin-local/libquestdb.so src/main/resources/io/questdb/client/bin/linux-x86-64/ + - name: Save linux-x86-64 Libraries to Cache + uses: actions/cache/save@v3 + with: + path: | + core/src/main/resources/io/questdb/client/bin/linux-x86-64/libquestdb.so + key: nativelibs-linux-${{ github.sha }} + build-all-linux-aarch64: + runs-on: ubuntu-22.04-arm + container: + image: quay.io/pypa/manylinux_2_28_aarch64 + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install tooling + run: | + yum update -y + yum install wget nasm zstd -y + - name: Install GraalVM JDK 25 (for jni.h) + run: | + wget -v --timeout=180 -O graalvm.tar.gz https://download.oracle.com/graalvm/25/latest/graalvm-jdk-25_linux-aarch64_bin.tar.gz + mkdir graalvm + tar xfz graalvm.tar.gz -C graalvm --strip-components=1 + echo "JAVA_HOME=`pwd`/graalvm" >> "$GITHUB_ENV" + - name: CMAKE linux-aarch64 + run: | + cd ./core + cmake -DCMAKE_TOOLCHAIN_FILE=./src/main/c/toolchains/linux-arm64.cmake -DCMAKE_BUILD_TYPE=Release -B cmake-build-release-arm64 -S. + - name: Build linux-aarch64 CXX Library + run: | + cd core + cmake --build cmake-build-release-arm64 --config Release + mkdir -p src/main/resources/io/questdb/client/bin/linux-aarch64/ + mkdir -p src/main/bin/linux-aarch64/ + cp target/classes/io/questdb/client/bin-local/libquestdb.so src/main/resources/io/questdb/client/bin/linux-aarch64/ + - name: Save linux-aarch64 Libraries to Cache + uses: actions/cache/save@v3 + with: + path: | + core/src/main/resources/io/questdb/client/bin/linux-aarch64/libquestdb.so + key: nativelibs-armlinux-${{ github.sha }} + build-cxx-windows: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Increase file count and install tooling + run: | + sudo sysctl -w fs.file-max=500000 + sudo apt-get update -y + sudo apt-get install -y nasm gcc-mingw-w64 g++-mingw-w64 + - name: Install GraalVM JDK 25 (for jni.h) + run: | + wget -nv -O graalvm.tar.gz https://download.oracle.com/graalvm/25/latest/graalvm-jdk-25_linux-x64_bin.tar.gz + mkdir graalvm + tar xfz graalvm.tar.gz -C graalvm --strip-components=1 + echo "JAVA_HOME=`pwd`/graalvm" >> "$GITHUB_ENV" + - name: Download windows jni_md.h from JDK 25 + run: | + cd core + curl https://raw.githubusercontent.com/openjdk/jdk25u/master/src/java.base/windows/native/include/jni_md.h > $JAVA_HOME/include/jni_md.h + - name: CMake Windows + run: | + cd core + cmake -DCMAKE_TOOLCHAIN_FILE=./src/main/c/toolchains/windows-x86_64.cmake -DCMAKE_CROSSCOMPILING=True -DCMAKE_BUILD_TYPE=Release -B cmake-build-release-win64 + - name: Build Windows CXX Library + run: | + cd core + cmake --build cmake-build-release-win64 --config Release + mkdir -p src/main/resources/io/questdb/client/bin/windows-x86-64/ + cp target/classes/io/questdb/client/bin-local/libquestdb.dll src/main/resources/io/questdb/client/bin/windows-x86-64/ + - name: Check CXX runtime dependency + run: | + cd ./core + if x86_64-w64-mingw32-objdump -p ./src/main/resources/io/questdb/client/bin/windows-x86-64/libquestdb.dll | grep -q libstdc++; then + echo "Failure: CXX runtime dependency detected" + exit 1 + fi + - name: Check git status + run: | + git status + - name: Save Windows CXX Library to Cache + uses: actions/cache/save@v3 + with: + path: | + core/src/main/resources/io/questdb/client/bin/windows-x86-64/libquestdb.dll + key: nativelibs-windows-${{ github.sha }} + collect-commit-and-push: + needs: + [ + build-all-macos, + build-cxx-windows, + build-all-linux-x86-64, + build-all-linux-aarch64, + ] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Print file sizes before + run: | + mkdir -p ./core/src/main/resources/io/questdb/client/bin/ + find ./core/src/main/resources/io/questdb/client/bin/ -type f -exec ls -l {} \; || true + - name: Restore darwin-aarch64 Libraries from Cache + uses: actions/cache/restore@v3 + with: + path: | + core/src/main/resources/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib + key: nativelibs-armosx-${{ github.sha }} + - name: Restore darwin-x86-64 Libraries from Cache + uses: actions/cache/restore@v3 + with: + path: | + core/src/main/resources/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib + key: nativelibs-osx-${{ github.sha }} + - name: Restore linux-x86-64 Libraries from Cache + uses: actions/cache/restore@v3 + with: + path: | + core/src/main/resources/io/questdb/client/bin/linux-x86-64/libquestdb.so + key: nativelibs-linux-${{ github.sha }} + - name: Restore linux-aarch64 Libraries from Cache + uses: actions/cache/restore@v3 + with: + path: | + core/src/main/resources/io/questdb/client/bin/linux-aarch64/libquestdb.so + key: nativelibs-armlinux-${{ github.sha }} + - name: Restore Windows CXX Library from Cache + uses: actions/cache/restore@v3 + with: + path: | + core/src/main/resources/io/questdb/client/bin/windows-x86-64/libquestdb.dll + key: nativelibs-windows-${{ github.sha }} + - name: Check git status before + run: | + git status + - name: Commit the files + run: | + git config --global user.name 'GitHub Actions - Rebuild Native Libraries' + git config --global user.email 'jaromir@questdb.io' + git add core/src/main/resources/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib + git add core/src/main/resources/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib + git add core/src/main/resources/io/questdb/client/bin/linux-x86-64/libquestdb.so + git add core/src/main/resources/io/questdb/client/bin/linux-aarch64/libquestdb.so + git add core/src/main/resources/io/questdb/client/bin/windows-x86-64/libquestdb.dll + + echo "Removing exec permissions in Git index..." + git update-index --chmod=-x core/src/main/resources/io/questdb/client/bin/linux-x86-64/libquestdb.so + git update-index --chmod=-x core/src/main/resources/io/questdb/client/bin/linux-aarch64/libquestdb.so + git update-index --chmod=-x core/src/main/resources/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib + git update-index --chmod=-x core/src/main/resources/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib + + git commit -m "Rebuild CXX libraries" + - name: Check git status after + run: | + git status + - name: Print file sizes after + run: | + find ./core/src/main/resources/io/questdb/client/bin/ -type f -exec ls -l {} \; + - name: Push the files to the current branch + uses: ad-m/github-push-action@d91a481090679876dfc4178fef17f286781251df + # Why do we use a commit hash instead of a tag for the github-push-action? + # ad-m/github-push-action is not as well-known repo as e.g. actions/checkout, and therefore we trust it less. + # d91a48109 is the same as the tag v0.8.0, but it's guaranteed to be immutable. + # So even if a bad actor takes over the repo, and rewrites tags to point to malicious commits, we will still be safe. + with: + branch: ${{ github.head_ref || github.ref_name }} diff --git a/README.md b/README.md index 5e55b7ca..f46d2abe 100644 --- a/README.md +++ b/README.md @@ -247,12 +247,9 @@ The workflow runs `mvn release:prepare`, lets Maven infer the release version fr all native libraries from the generated release tag, stores them as GitHub Actions artifacts, downloads them into the Maven build, and publishes the final JAR to Maven Central. -Run the workflow from the Actions tab with `dry_run=true` first. Dry run creates the local Maven release state, builds -native artifacts, and deploys to a local file repository inside the workflow; it does not push commits, push tags, or -contact Maven Central. For the real release, rerun with `dry_run=false` and `confirm_publish=PUBLISH` (the publish job -refuses to run without the literal string). +Run the workflow from the Actions tab. -The real Maven Central publish job uses the `maven-release` GitHub environment as an additional gate; configure that +The Maven Central publish job uses the `maven-release` GitHub environment as an additional gate; configure that environment with required reviewers so the workflow also pauses for human approval before immutable Central publishing. Do not run `mvn release:perform` or `mvn deploy` unless you are intentionally bypassing the GitHub Actions release diff --git a/artifacts/release/README.md b/artifacts/release/README.md index f539ec50..2d45c22d 100644 --- a/artifacts/release/README.md +++ b/artifacts/release/README.md @@ -12,47 +12,17 @@ path. Create a draft GitHub release with the intended version and notes. Do not create the git tag up front. The workflow creates the tag through `mvn release:prepare`; finalize the GitHub release after Maven Central propagation. -## Run a dry run +## Publish Start the `Release to Maven Central` workflow from the Actions tab. Use these inputs: - `source_ref`: branch/ref to release from, usually `main` -- `dry_run`: `true` - `release_version_override`: blank unless doing a non-standard version - `next_development_version_override`: blank unless doing a non-standard next snapshot -The workflow will: - -- check out `source_ref` -- run `mvn release:clean` -- run `mvn release:prepare` without pushing changes -- infer the release version from the current `-SNAPSHOT` POM -- capture the generated Maven release tag from `release.properties` -- archive the generated release source -- build all native libraries from that release source -- store native libraries as GitHub Actions artifacts -- download the native artifacts into `core/target/native-libs` -- package them into the JAR through the `include-native-artifacts` Maven profile -- sign the Maven artifacts with a temporary dry-run GPG key -- deploy to a local file repository in the workflow instead of Maven Central -- upload the local file repository as the `dry-run-maven-repository` GitHub Actions artifact -- upload the direct `core/target` build outputs as the `dry-run-core-target-artifacts` GitHub Actions artifact - -A dry run must not push commits, push tags, request AWS credentials, request Sonatype credentials, or contact -Sonatype/Maven Central. - -## Publish - -After the dry run has been reviewed, rerun the same workflow with: - -- the same `source_ref` -- `dry_run`: `false` -- `confirm_publish`: `PUBLISH` (literal string; the publish job fails fast on any other value) -- override inputs still blank unless intentionally needed - -The real release path uses the generated Maven tag as the source of truth. It pushes the `mvn release:prepare` commits +The release path uses the generated Maven tag as the source of truth. It pushes the `mvn release:prepare` commits back to `source_ref`, pushes the generated release tag, builds native libraries from that tag, downloads the native artifacts into the Maven build, signs the release artifacts, and uploads through the Sonatype Central Portal. @@ -76,9 +46,7 @@ Use `release_version_override` and `next_development_version_override` only for ## Failure handling -If a dry run fails, fix the workflow or source branch and rerun it. Dry runs do not push release commits or tags. - -If a real release fails before the final Maven Central upload, inspect whether the prepare commits or tag were pushed. +If a release fails before the final Maven Central upload, inspect whether the prepare commits or tag were pushed. If they were pushed, either rerun from the same source state after fixing the workflow or clean up the failed release state deliberately. Do not reuse a version that may already have reached Maven Central; Maven Central coordinates are immutable. diff --git a/core/pom.xml b/core/pom.xml index 618ef9a0..ab3a7e82 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -404,22 +404,6 @@ - - release-dry-run-deploy - - - - org.apache.maven.plugins - maven-deploy-plugin - 3.1.4 - - false - dry-run::default::file://${project.build.directory}/dry-run-repo - - - - - jacoco From 043f3ec62562f8d2e6aed5a9f5e0537a69684ab7 Mon Sep 17 00:00:00 2001 From: Steven Sklar Date: Wed, 27 May 2026 21:13:52 -0400 Subject: [PATCH 07/15] ugh --- .github/actions/stage-native-libs/action.yml | 44 -------------------- .github/workflows/maven_central_release.yml | 36 +++++++++++++++- 2 files changed, 34 insertions(+), 46 deletions(-) delete mode 100644 .github/actions/stage-native-libs/action.yml diff --git a/.github/actions/stage-native-libs/action.yml b/.github/actions/stage-native-libs/action.yml deleted file mode 100644 index 91b479e7..00000000 --- a/.github/actions/stage-native-libs/action.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Stage native libraries for Maven -description: > - Download the per-platform native library artifacts uploaded by the build-* jobs - and stage them under core/target/native-libs/io/questdb/client/bin// - in the layout the include-native-artifacts Maven profile expects. - -runs: - using: composite - steps: - - name: Download native artifacts - uses: actions/download-artifact@v4 - with: - pattern: native-* - path: core/target/downloaded-native-artifacts - merge-multiple: false - - - name: Stage native artifacts for Maven - shell: bash - run: | - mkdir -p core/target/native-libs/io/questdb/client/bin/darwin-aarch64 - mkdir -p core/target/native-libs/io/questdb/client/bin/darwin-x86-64 - mkdir -p core/target/native-libs/io/questdb/client/bin/linux-aarch64 - mkdir -p core/target/native-libs/io/questdb/client/bin/linux-x86-64 - mkdir -p core/target/native-libs/io/questdb/client/bin/windows-x86-64 - cp core/target/downloaded-native-artifacts/native-darwin-aarch64/libquestdb.dylib core/target/native-libs/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib - cp core/target/downloaded-native-artifacts/native-darwin-x86-64/libquestdb.dylib core/target/native-libs/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib - cp core/target/downloaded-native-artifacts/native-linux-aarch64/libquestdb.so core/target/native-libs/io/questdb/client/bin/linux-aarch64/libquestdb.so - cp core/target/downloaded-native-artifacts/native-linux-x86-64/libquestdb.so core/target/native-libs/io/questdb/client/bin/linux-x86-64/libquestdb.so - cp core/target/downloaded-native-artifacts/native-windows-x86-64/libquestdb.dll core/target/native-libs/io/questdb/client/bin/windows-x86-64/libquestdb.dll - - expected=( - core/target/native-libs/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib - core/target/native-libs/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib - core/target/native-libs/io/questdb/client/bin/linux-aarch64/libquestdb.so - core/target/native-libs/io/questdb/client/bin/linux-x86-64/libquestdb.so - core/target/native-libs/io/questdb/client/bin/windows-x86-64/libquestdb.dll - ) - - for file_name in "${expected[@]}"; do - if [[ ! -s "${file_name}" ]]; then - echo "::error::Missing native artifact ${file_name}." - exit 1 - fi - done diff --git a/.github/workflows/maven_central_release.yml b/.github/workflows/maven_central_release.yml index b355c15b..9db69f2a 100644 --- a/.github/workflows/maven_central_release.yml +++ b/.github/workflows/maven_central_release.yml @@ -380,8 +380,40 @@ jobs: exit 1 fi - - name: Stage native libraries for Maven - uses: ./.github/actions/stage-native-libs + - name: Download native artifacts + uses: actions/download-artifact@v4 + with: + pattern: native-* + path: core/target/downloaded-native-artifacts + merge-multiple: false + + - name: Stage native artifacts for Maven + run: | + mkdir -p core/target/native-libs/io/questdb/client/bin/darwin-aarch64 + mkdir -p core/target/native-libs/io/questdb/client/bin/darwin-x86-64 + mkdir -p core/target/native-libs/io/questdb/client/bin/linux-aarch64 + mkdir -p core/target/native-libs/io/questdb/client/bin/linux-x86-64 + mkdir -p core/target/native-libs/io/questdb/client/bin/windows-x86-64 + cp core/target/downloaded-native-artifacts/native-darwin-aarch64/libquestdb.dylib core/target/native-libs/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib + cp core/target/downloaded-native-artifacts/native-darwin-x86-64/libquestdb.dylib core/target/native-libs/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib + cp core/target/downloaded-native-artifacts/native-linux-aarch64/libquestdb.so core/target/native-libs/io/questdb/client/bin/linux-aarch64/libquestdb.so + cp core/target/downloaded-native-artifacts/native-linux-x86-64/libquestdb.so core/target/native-libs/io/questdb/client/bin/linux-x86-64/libquestdb.so + cp core/target/downloaded-native-artifacts/native-windows-x86-64/libquestdb.dll core/target/native-libs/io/questdb/client/bin/windows-x86-64/libquestdb.dll + + expected=( + core/target/native-libs/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib + core/target/native-libs/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib + core/target/native-libs/io/questdb/client/bin/linux-aarch64/libquestdb.so + core/target/native-libs/io/questdb/client/bin/linux-x86-64/libquestdb.so + core/target/native-libs/io/questdb/client/bin/windows-x86-64/libquestdb.dll + ) + + for file_name in "${expected[@]}"; do + if [[ ! -s "${file_name}" ]]; then + echo "::error::Missing native artifact ${file_name}." + exit 1 + fi + done - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 From 5841acf1f27b43df1a75beb582bd65e71420eb73 Mon Sep 17 00:00:00 2001 From: Steve Sklar Date: Thu, 28 May 2026 13:55:15 -0400 Subject: [PATCH 08/15] add overwrite to pom --- core/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/core/pom.xml b/core/pom.xml index ab3a7e82..2665f244 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -302,6 +302,7 @@ copy-resources + true ${project.build.outputDirectory} From 3015d80281c52c9759614daa4d080faba76b2c50 Mon Sep 17 00:00:00 2001 From: Steve Sklar Date: Thu, 28 May 2026 14:07:21 -0400 Subject: [PATCH 09/15] address review comments --- .github/workflows/maven_central_release.yml | 22 +++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/maven_central_release.yml b/.github/workflows/maven_central_release.yml index 9db69f2a..49ba4813 100644 --- a/.github/workflows/maven_central_release.yml +++ b/.github/workflows/maven_central_release.yml @@ -121,6 +121,12 @@ jobs: ref: ${{ needs.prepare.outputs.release_tag }} submodules: true + - name: Set up Java 11 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: temurin + java-version: "11" + - name: Install toolchains run: | brew uninstall cmake || true @@ -130,8 +136,8 @@ jobs: run: | cd core export MACOSX_DEPLOYMENT_TARGET=13.0 - cmake -B build/release -DCMAKE_BUILD_TYPE=Release - cmake --build build/release --config Release + cmake -B cmake-build-release -DCMAKE_BUILD_TYPE=Release + cmake --build cmake-build-release --config Release - name: Stage native library run: | @@ -139,7 +145,7 @@ jobs: cp core/target/classes/io/questdb/client/bin-local/libquestdb.dylib native-artifacts/${{ matrix.platform }}/libquestdb.dylib - name: Upload native library - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: native-${{ matrix.platform }} path: native-artifacts/${{ matrix.platform }}/libquestdb.dylib @@ -204,7 +210,7 @@ jobs: cp core/target/classes/io/questdb/client/bin-local/libquestdb.so native-artifacts/linux-x86-64/libquestdb.so - name: Upload native library - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: native-linux-x86-64 path: native-artifacts/linux-x86-64/libquestdb.so @@ -247,7 +253,7 @@ jobs: cp core/target/classes/io/questdb/client/bin-local/libquestdb.so native-artifacts/linux-aarch64/libquestdb.so - name: Upload native library - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: native-linux-aarch64 path: native-artifacts/linux-aarch64/libquestdb.so @@ -280,7 +286,7 @@ jobs: - name: Download Windows jni_md.h from JDK 25 run: | cd core - curl https://raw.githubusercontent.com/openjdk/jdk25u/master/src/java.base/windows/native/include/jni_md.h > "$JAVA_HOME/include/jni_md.h" + curl -fsSL https://raw.githubusercontent.com/openjdk/jdk25u/master/src/java.base/windows/native/include/jni_md.h > "$JAVA_HOME/include/jni_md.h" - name: Build native library run: | @@ -301,7 +307,7 @@ jobs: cp core/target/classes/io/questdb/client/bin-local/libquestdb.dll native-artifacts/windows-x86-64/libquestdb.dll - name: Upload native library - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: native-windows-x86-64 path: native-artifacts/windows-x86-64/libquestdb.dll @@ -381,7 +387,7 @@ jobs: fi - name: Download native artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: pattern: native-* path: core/target/downloaded-native-artifacts From 6dbd6705d07374f6bb61ae11175bbe421845ff21 Mon Sep 17 00:00:00 2001 From: Steve Sklar Date: Thu, 28 May 2026 15:10:02 -0400 Subject: [PATCH 10/15] Rework release to verify before publishing Restructure the Maven Central release workflow into a pipeline that proves the release good before doing anything irreversible: resolve versions, build the five native libraries, run the full test suite against them, and validate the signed bundle with the Central Portal before pushing the tag or publishing. Push only the release tag (never a commit to main); land the next snapshot bump as a follow-up PR. Upload as a droppable VALIDATED deployment, publish it through the Portal API, and do not block on the asynchronous propagation to Maven Central. Add a shared native-artifact staging script, harden the shell (pipefail, guarded objdump), pin the versions-set plugin, and flip central-publishing to autoPublish=false. Rewrite the release docs for the new flow. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/scripts/stage-native-artifacts.sh | 33 ++ .github/workflows/maven_central_release.yml | 479 ++++++++++++++------ README.md | 23 +- artifacts/release/README.md | 98 ++-- core/pom.xml | 6 +- 5 files changed, 465 insertions(+), 174 deletions(-) create mode 100755 .github/scripts/stage-native-artifacts.sh diff --git a/.github/scripts/stage-native-artifacts.sh b/.github/scripts/stage-native-artifacts.sh new file mode 100755 index 00000000..8717e028 --- /dev/null +++ b/.github/scripts/stage-native-artifacts.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Copy the per-platform native libraries downloaded as GitHub Actions artifacts +# into the layout the `include-native-artifacts` Maven profile expects +# (core/target/native-libs/io/questdb/client/bin//), and fail if any +# expected library is missing or empty. +set -euo pipefail + +downloaded="core/target/downloaded-native-artifacts" +staged="core/target/native-libs/io/questdb/client/bin" + +# platform -> library filename +declare -A libs=( + [darwin-aarch64]=libquestdb.dylib + [darwin-x86-64]=libquestdb.dylib + [linux-aarch64]=libquestdb.so + [linux-x86-64]=libquestdb.so + [windows-x86-64]=libquestdb.dll +) + +for platform in "${!libs[@]}"; do + lib="${libs[$platform]}" + src="${downloaded}/native-${platform}/${lib}" + dst_dir="${staged}/${platform}" + + if [[ ! -s "${src}" ]]; then + echo "::error::Missing or empty native artifact: ${src}" + exit 1 + fi + + mkdir -p "${dst_dir}" + cp "${src}" "${dst_dir}/${lib}" + echo "Staged ${platform}/${lib}" +done diff --git a/.github/workflows/maven_central_release.yml b/.github/workflows/maven_central_release.yml index 49ba4813..31d5cc1c 100644 --- a/.github/workflows/maven_central_release.yml +++ b/.github/workflows/maven_central_release.yml @@ -1,5 +1,25 @@ name: Release to Maven Central +# Release model (immutable-safe, verify-before-push): +# +# resolve -> build x5 -> verify -> publish (gated) -> open-bump-pr +# +# * Nothing irreversible (git tag push, Maven Central publish) happens until +# the full test suite has passed against the freshly built native libraries +# AND the signed bundle has been validated by the Central Portal. +# * The release tag is the LAST thing created and points at the exact verified +# tree. We never push commits to `main` -- the next-development snapshot bump +# lands as a normal pull request (main is PR-only by org ruleset). +# +# Org-settings prerequisites (one-time, NOT enforceable from this file): +# * `restrict-tag-pushing` ruleset: add `github-actions[bot]` as a bypass actor +# so the publish job can push the release tag. (The branch ruleset on `main` +# is intentionally NOT bypassed -- the snapshot bump goes through a PR.) +# * AWS secret referenced by MAVEN_RELEASE_AWS_SECRET_ARN must expose these +# JSON keys (parse-json-secrets turns them into env vars of the same name): +# MAVEN_GPG_PRIVATE_KEY, MAVEN_CENTRAL_USERNAME, MAVEN_CENTRAL_PASSWORD, +# and optionally MAVEN_GPG_PASSPHRASE (omit/empty for a passphrase-less key). + on: workflow_dispatch: inputs: @@ -13,7 +33,7 @@ on: required: false type: string next_development_version_override: - description: "Optional next development version override; normally inferred by maven-release-plugin" + description: "Optional next development version override; normally the release version with the patch bumped" required: false type: string @@ -24,25 +44,27 @@ concurrency: group: maven-central-release cancel-in-progress: false +defaults: + run: + # Explicit `bash` runs as `bash --noprofile --norc -eo pipefail {0}`, i.e. it + # adds `pipefail` on top of errexit. Without this, a failing command on the + # left of a pipe (sed/objdump/git) is masked by a succeeding tail/grep/head. + shell: bash + jobs: - prepare: + resolve: runs-on: ubuntu-latest - timeout-minutes: 30 - permissions: - contents: write + timeout-minutes: 15 outputs: - release_tag: ${{ steps.release-tag.outputs.release_tag }} - env: - SOURCE_REF: ${{ inputs.source_ref }} - RELEASE_VERSION_OVERRIDE: ${{ inputs.release_version_override }} - NEXT_DEVELOPMENT_VERSION_OVERRIDE: ${{ inputs.next_development_version_override }} + release_version: ${{ steps.versions.outputs.release_version }} + next_development_version: ${{ steps.versions.outputs.next_development_version }} + source_sha: ${{ steps.versions.outputs.source_sha }} steps: - name: Check out source ref uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{ inputs.source_ref }} fetch-depth: 0 - submodules: true - name: Set up Java 11 uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 @@ -51,61 +73,76 @@ jobs: java-version: "11" cache: maven - - name: Prepare release + - name: Resolve versions and guard against re-release + id: versions + env: + RELEASE_VERSION_OVERRIDE: ${{ inputs.release_version_override }} + NEXT_DEVELOPMENT_VERSION_OVERRIDE: ${{ inputs.next_development_version_override }} run: | - git config user.name "GitHub Actions - Maven Release" - git config user.email "actions@github.com" - - mvn -B -ntp release:clean + set -euo pipefail - release_args=( - -DautoVersionSubmodules=true - -DpushChanges=false - ) + pom_version="$(mvn -B -q -N -DforceStdout help:evaluate -Dexpression=project.version)" + if [[ -z "${pom_version}" ]]; then + echo "::error::Could not read project version from ${{ inputs.source_ref }}." + exit 1 + fi if [[ -n "${RELEASE_VERSION_OVERRIDE}" ]]; then - release_args+=("-DreleaseVersion=${RELEASE_VERSION_OVERRIDE}") + release_version="${RELEASE_VERSION_OVERRIDE}" + else + release_version="${pom_version%-SNAPSHOT}" fi - if [[ -n "${NEXT_DEVELOPMENT_VERSION_OVERRIDE}" ]]; then - release_args+=("-DdevelopmentVersion=${NEXT_DEVELOPMENT_VERSION_OVERRIDE}") + if [[ "${release_version}" == *-SNAPSHOT ]]; then + echo "::error::Refusing to release a SNAPSHOT version (${release_version})." + exit 1 fi - - mvn -B -ntp release:prepare "${release_args[@]}" - - - name: Capture and verify release tag - id: release-tag - run: | - release_tag="$(sed -n 's/^scm.tag=//p' release.properties | tail -n 1)" - if [[ -z "${release_tag}" ]]; then - echo "::error::release.properties did not contain scm.tag." + if [[ ! "${release_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Release version '${release_version}' is not in X.Y.Z form." exit 1 fi - pom_version="$(git show "${release_tag}:pom.xml" | sed -n 's:.*\(.*\).*:\1:p' | head -n 1)" - if [[ -z "${pom_version}" ]]; then - echo "::error::Could not read project version from tag ${release_tag}." + if [[ -n "${NEXT_DEVELOPMENT_VERSION_OVERRIDE}" ]]; then + next_development_version="${NEXT_DEVELOPMENT_VERSION_OVERRIDE}" + else + IFS='.' read -r v_major v_minor v_patch <<< "${release_version}" + next_development_version="${v_major}.${v_minor}.$((v_patch + 1))-SNAPSHOT" + fi + if [[ "${next_development_version}" != *-SNAPSHOT ]]; then + echo "::error::Next development version '${next_development_version}' must end in -SNAPSHOT." exit 1 fi - if [[ "${pom_version}" == *-SNAPSHOT ]]; then - echo "::error::Refusing to release SNAPSHOT version ${pom_version}." + + # Guard 1: the release tag must not already exist. If a previous release's + # snapshot-bump PR was never merged, `main` is still at the old -SNAPSHOT + # and we would otherwise try to re-release a shipped version. Fail loudly. + if git ls-remote --exit-code --tags origin "refs/tags/${release_version}" >/dev/null 2>&1; then + echo "::error::Tag ${release_version} already exists. Merge the snapshot-bump PR (or bump the version) before releasing." exit 1 fi - if [[ "${release_tag}" != "${pom_version}" ]]; then - echo "::error::Generated tag ${release_tag} does not match POM version ${pom_version}." + + # Guard 2: the version must not already be on Maven Central. + central_pom="https://repo1.maven.org/maven2/org/questdb/questdb-client/${release_version}/questdb-client-${release_version}.pom" + http_code="$(curl -sS -o /dev/null -w '%{http_code}' "${central_pom}" || echo "000")" + if [[ "${http_code}" == "200" ]]; then + echo "::error::questdb-client ${release_version} is already published to Maven Central." exit 1 fi - echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT" + source_sha="$(git rev-parse HEAD)" - - name: Push release commits and tag - run: | - git push origin "HEAD:${SOURCE_REF}" - git push origin "refs/tags/${{ steps.release-tag.outputs.release_tag }}" + { + echo "release_version=${release_version}" + echo "next_development_version=${next_development_version}" + echo "source_sha=${source_sha}" + } >> "$GITHUB_OUTPUT" + + echo "Release ${release_version} from ${source_sha}; next development version ${next_development_version}." build-macos: - needs: prepare + needs: resolve strategy: + fail-fast: true matrix: include: - os: macos-14 @@ -115,10 +152,10 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 60 steps: - - name: Check out release tag + - name: Check out release source uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: - ref: ${{ needs.prepare.outputs.release_tag }} + ref: ${{ needs.resolve.outputs.source_sha }} submodules: true - name: Set up Java 11 @@ -139,10 +176,28 @@ jobs: cmake -B cmake-build-release -DCMAKE_BUILD_TYPE=Release cmake --build cmake-build-release --config Release + - name: Smoke-test native library + run: | + lib="core/target/classes/io/questdb/client/bin-local/libquestdb.dylib" + test -f "$lib" + otool -L "$lib" + # Loading the library proves the dynamic linker can resolve every + # dependency on the build platform before we ever ship it. + cat > LoadCheck.java <<'EOF' + public class LoadCheck { + public static void main(String[] args) { + System.load(new java.io.File(args[0]).getAbsolutePath()); + System.out.println("OK: loaded " + args[0]); + } + } + EOF + javac LoadCheck.java + java LoadCheck "$lib" + - name: Stage native library run: | - mkdir -p native-artifacts/${{ matrix.platform }} - cp core/target/classes/io/questdb/client/bin-local/libquestdb.dylib native-artifacts/${{ matrix.platform }}/libquestdb.dylib + mkdir -p "native-artifacts/${{ matrix.platform }}" + cp core/target/classes/io/questdb/client/bin-local/libquestdb.dylib "native-artifacts/${{ matrix.platform }}/libquestdb.dylib" - name: Upload native library uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 @@ -152,7 +207,7 @@ jobs: if-no-files-found: error build-linux-x86-64: - needs: prepare + needs: resolve runs-on: ubuntu-latest timeout-minutes: 60 container: @@ -179,24 +234,25 @@ jobs: tar -xf node-v20.9.0-linux-x64-glibc-217.tar.xz --strip-components 1 -C /node20217 ldd /__e/node20/bin/node - - name: Check out release tag + - name: Check out release source uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: - ref: ${{ needs.prepare.outputs.release_tag }} + ref: ${{ needs.resolve.outputs.source_sha }} submodules: true - name: Install up-to-date CMake run: | wget -nv https://github.com/Kitware/CMake/releases/download/v3.29.2/cmake-3.29.2-linux-x86_64.tar.gz tar -zxf cmake-3.29.2-linux-x86_64.tar.gz - echo "PATH=`pwd`/cmake-3.29.2-linux-x86_64/bin/:$PATH" >> "$GITHUB_ENV" + echo "PATH=$(pwd)/cmake-3.29.2-linux-x86_64/bin/:$PATH" >> "$GITHUB_ENV" - name: Install GraalVM JDK 25 run: | + # TODO(pin): replace /25/latest/ with an exact GraalVM build URL and verify a sha256. wget -nv -O graalvm.tar.gz https://download.oracle.com/graalvm/25/latest/graalvm-jdk-25_linux-x64_bin.tar.gz mkdir graalvm tar xfz graalvm.tar.gz -C graalvm --strip-components=1 - echo "JAVA_HOME=`pwd`/graalvm" >> "$GITHUB_ENV" + echo "JAVA_HOME=$(pwd)/graalvm" >> "$GITHUB_ENV" - name: Build native library run: | @@ -204,6 +260,26 @@ jobs: cmake -DCMAKE_BUILD_TYPE=Release -B cmake-build-release -S. cmake --build cmake-build-release --config Release + - name: Smoke-test native library + run: | + lib="core/target/classes/io/questdb/client/bin-local/libquestdb.so" + test -f "$lib" + # Fail if the linker reports any unresolved dependency. + if ldd "$lib" | grep -i "not found"; then + echo "::error::libquestdb.so has unresolved dependencies." + exit 1 + fi + cat > LoadCheck.java <<'EOF' + public class LoadCheck { + public static void main(String[] args) { + System.load(new java.io.File(args[0]).getAbsolutePath()); + System.out.println("OK: loaded " + args[0]); + } + } + EOF + "$JAVA_HOME/bin/javac" LoadCheck.java + "$JAVA_HOME/bin/java" LoadCheck "$lib" + - name: Stage native library run: | mkdir -p native-artifacts/linux-x86-64 @@ -217,16 +293,16 @@ jobs: if-no-files-found: error build-linux-aarch64: - needs: prepare + needs: resolve runs-on: ubuntu-22.04-arm timeout-minutes: 60 container: image: quay.io/pypa/manylinux_2_28_aarch64 steps: - - name: Check out release tag + - name: Check out release source uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: - ref: ${{ needs.prepare.outputs.release_tag }} + ref: ${{ needs.resolve.outputs.source_sha }} submodules: true - name: Install tooling @@ -236,10 +312,11 @@ jobs: - name: Install GraalVM JDK 25 run: | + # TODO(pin): replace /25/latest/ with an exact GraalVM build URL and verify a sha256. wget -v --timeout=180 -O graalvm.tar.gz https://download.oracle.com/graalvm/25/latest/graalvm-jdk-25_linux-aarch64_bin.tar.gz mkdir graalvm tar xfz graalvm.tar.gz -C graalvm --strip-components=1 - echo "JAVA_HOME=`pwd`/graalvm" >> "$GITHUB_ENV" + echo "JAVA_HOME=$(pwd)/graalvm" >> "$GITHUB_ENV" - name: Build native library run: | @@ -247,6 +324,25 @@ jobs: cmake -DCMAKE_TOOLCHAIN_FILE=./src/main/c/toolchains/linux-arm64.cmake -DCMAKE_BUILD_TYPE=Release -B cmake-build-release-arm64 -S. cmake --build cmake-build-release-arm64 --config Release + - name: Smoke-test native library + run: | + lib="core/target/classes/io/questdb/client/bin-local/libquestdb.so" + test -f "$lib" + if ldd "$lib" | grep -i "not found"; then + echo "::error::libquestdb.so has unresolved dependencies." + exit 1 + fi + cat > LoadCheck.java <<'EOF' + public class LoadCheck { + public static void main(String[] args) { + System.load(new java.io.File(args[0]).getAbsolutePath()); + System.out.println("OK: loaded " + args[0]); + } + } + EOF + "$JAVA_HOME/bin/javac" LoadCheck.java + "$JAVA_HOME/bin/java" LoadCheck "$lib" + - name: Stage native library run: | mkdir -p native-artifacts/linux-aarch64 @@ -260,14 +356,14 @@ jobs: if-no-files-found: error build-windows-x86-64: - needs: prepare + needs: resolve runs-on: ubuntu-24.04 timeout-minutes: 60 steps: - - name: Check out release tag + - name: Check out release source uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: - ref: ${{ needs.prepare.outputs.release_tag }} + ref: ${{ needs.resolve.outputs.source_sha }} submodules: true - name: Install tooling @@ -278,14 +374,16 @@ jobs: - name: Install GraalVM JDK 25 run: | + # TODO(pin): replace /25/latest/ with an exact GraalVM build URL and verify a sha256. wget -nv -O graalvm.tar.gz https://download.oracle.com/graalvm/25/latest/graalvm-jdk-25_linux-x64_bin.tar.gz mkdir graalvm tar xfz graalvm.tar.gz -C graalvm --strip-components=1 - echo "JAVA_HOME=`pwd`/graalvm" >> "$GITHUB_ENV" + echo "JAVA_HOME=$(pwd)/graalvm" >> "$GITHUB_ENV" - name: Download Windows jni_md.h from JDK 25 run: | cd core + # TODO(pin): pin to a jdk25u tag/commit instead of the moving `master` branch. curl -fsSL https://raw.githubusercontent.com/openjdk/jdk25u/master/src/java.base/windows/native/include/jni_md.h > "$JAVA_HOME/include/jni_md.h" - name: Build native library @@ -296,8 +394,13 @@ jobs: - name: Check CXX runtime dependency run: | - if x86_64-w64-mingw32-objdump -p ./core/target/classes/io/questdb/client/bin-local/libquestdb.dll | grep -q libstdc++; then - echo "Failure: CXX runtime dependency detected" + lib="./core/target/classes/io/questdb/client/bin-local/libquestdb.dll" + test -f "$lib" + # Capture objdump output first so a failing objdump trips errexit instead + # of being silently swallowed by `| grep -q` (which would falsely pass). + deps="$(x86_64-w64-mingw32-objdump -p "$lib")" + if printf '%s\n' "$deps" | grep -q 'libstdc++'; then + echo "::error::Failure: CXX runtime dependency detected" exit 1 fi @@ -313,42 +416,77 @@ jobs: path: native-artifacts/windows-x86-64/libquestdb.dll if-no-files-found: error - publish-central: + verify: needs: - - prepare + - resolve - build-macos - build-linux-x86-64 - build-linux-aarch64 - build-windows-x86-64 runs-on: ubuntu-latest + timeout-minutes: 45 + env: + RELEASE_VERSION: ${{ needs.resolve.outputs.release_version }} + steps: + - name: Check out release source + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + ref: ${{ needs.resolve.outputs.source_sha }} + + - name: Set up Java 11 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: temurin + java-version: "11" + cache: maven + + - name: Download native artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: native-* + path: core/target/downloaded-native-artifacts + merge-multiple: false + + - name: Stage native artifacts for Maven + run: ./.github/scripts/stage-native-artifacts.sh + + - name: Set release version + run: | + mvn -B -ntp org.codehaus.mojo:versions-maven-plugin:2.16.2:set -DnewVersion="${RELEASE_VERSION}" -DprocessAllModules=true -DgenerateBackupPoms=false + + - name: Verify release artifact (full test suite, native libs bundled) + run: | + # Tests on -- this is the gate. The bundled linux-x86-64 native library + # is exercised by the real test suite before anyone approves the publish. + mvn -B -ntp verify -P include-native-artifacts + + publish: + needs: + - resolve + - verify + runs-on: ubuntu-latest environment: maven-release - timeout-minutes: 30 + timeout-minutes: 45 permissions: - contents: read - id-token: write + contents: write # push the release tag (requires github-actions[bot] tag-ruleset bypass) + id-token: write # AWS OIDC env: + RELEASE_VERSION: ${{ needs.resolve.outputs.release_version }} + SOURCE_SHA: ${{ needs.resolve.outputs.source_sha }} MAVEN_RELEASE_AWS_REGION: ${{ vars.MAVEN_RELEASE_AWS_REGION }} MAVEN_RELEASE_AWS_ROLE_ARN: ${{ secrets.MAVEN_RELEASE_AWS_ROLE_ARN }} MAVEN_RELEASE_AWS_SECRET_ARN: ${{ secrets.MAVEN_RELEASE_AWS_SECRET_ARN }} steps: - name: Validate workflow configuration run: | - required_vars=( - MAVEN_RELEASE_AWS_REGION - ) - + required_vars=(MAVEN_RELEASE_AWS_REGION) for var_name in "${required_vars[@]}"; do if [[ -z "${!var_name:-}" ]]; then echo "::error::Repository variable ${var_name} is required." exit 1 fi done - - required_secrets=( - MAVEN_RELEASE_AWS_ROLE_ARN - MAVEN_RELEASE_AWS_SECRET_ARN - ) - + 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." @@ -356,10 +494,18 @@ jobs: fi done - - name: Check out release tag + - name: Check out release source uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: - ref: ${{ needs.prepare.outputs.release_tag }} + ref: ${{ needs.resolve.outputs.source_sha }} + fetch-depth: 0 + + - name: Re-assert the tag is still free + run: | + if git ls-remote --exit-code --tags origin "refs/tags/${RELEASE_VERSION}" >/dev/null 2>&1; then + echo "::error::Tag ${RELEASE_VERSION} appeared since resolve. Aborting." + exit 1 + fi - name: Set up Java 11 uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 @@ -368,24 +514,6 @@ jobs: java-version: "11" cache: maven - - name: Verify checked-out tag matches POM version - env: - RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }} - run: | - pom_version="$(mvn -B -q -N -DforceStdout help:evaluate -Dexpression=project.version)" - if [[ -z "${pom_version}" ]]; then - echo "::error::Could not read project version from checked-out tag." - exit 1 - fi - if [[ "${pom_version}" == *-SNAPSHOT ]]; then - echo "::error::Refusing to release SNAPSHOT version ${pom_version}." - exit 1 - fi - if [[ "${RELEASE_TAG}" != "${pom_version}" ]]; then - echo "::error::Release tag ${RELEASE_TAG} does not match POM version ${pom_version}." - exit 1 - fi - - name: Download native artifacts uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: @@ -394,32 +522,7 @@ jobs: merge-multiple: false - name: Stage native artifacts for Maven - run: | - mkdir -p core/target/native-libs/io/questdb/client/bin/darwin-aarch64 - mkdir -p core/target/native-libs/io/questdb/client/bin/darwin-x86-64 - mkdir -p core/target/native-libs/io/questdb/client/bin/linux-aarch64 - mkdir -p core/target/native-libs/io/questdb/client/bin/linux-x86-64 - mkdir -p core/target/native-libs/io/questdb/client/bin/windows-x86-64 - cp core/target/downloaded-native-artifacts/native-darwin-aarch64/libquestdb.dylib core/target/native-libs/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib - cp core/target/downloaded-native-artifacts/native-darwin-x86-64/libquestdb.dylib core/target/native-libs/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib - cp core/target/downloaded-native-artifacts/native-linux-aarch64/libquestdb.so core/target/native-libs/io/questdb/client/bin/linux-aarch64/libquestdb.so - cp core/target/downloaded-native-artifacts/native-linux-x86-64/libquestdb.so core/target/native-libs/io/questdb/client/bin/linux-x86-64/libquestdb.so - cp core/target/downloaded-native-artifacts/native-windows-x86-64/libquestdb.dll core/target/native-libs/io/questdb/client/bin/windows-x86-64/libquestdb.dll - - expected=( - core/target/native-libs/io/questdb/client/bin/darwin-aarch64/libquestdb.dylib - core/target/native-libs/io/questdb/client/bin/darwin-x86-64/libquestdb.dylib - core/target/native-libs/io/questdb/client/bin/linux-aarch64/libquestdb.so - core/target/native-libs/io/questdb/client/bin/linux-x86-64/libquestdb.so - core/target/native-libs/io/questdb/client/bin/windows-x86-64/libquestdb.dll - ) - - for file_name in "${expected[@]}"; do - if [[ ! -s "${file_name}" ]]; then - echo "::error::Missing native artifact ${file_name}." - exit 1 - fi - done + run: ./.github/scripts/stage-native-artifacts.sh - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 @@ -437,12 +540,7 @@ jobs: - name: Validate release credentials run: | - required_vars=( - MAVEN_GPG_PRIVATE_KEY - MAVEN_CENTRAL_USERNAME - MAVEN_CENTRAL_PASSWORD - ) - + required_vars=(MAVEN_GPG_PRIVATE_KEY MAVEN_CENTRAL_USERNAME MAVEN_CENTRAL_PASSWORD) for var_name in "${required_vars[@]}"; do if [[ -z "${!var_name:-}" ]]; then echo "::error::AWS secret ${MAVEN_RELEASE_AWS_SECRET_ARN} must define ${var_name}." @@ -481,9 +579,79 @@ jobs: printf '%s\n' "$MAVEN_GPG_PRIVATE_KEY" | gpg --batch --import echo "GNUPGHOME=$GNUPGHOME" >> "$GITHUB_ENV" - - name: Publish release to Maven Central + - name: Set release version + run: | + mvn -B -ntp org.codehaus.mojo:versions-maven-plugin:2.16.2:set -DnewVersion="${RELEASE_VERSION}" -DprocessAllModules=true -DgenerateBackupPoms=false + + - name: Upload signed bundle to Central (validate only, droppable) + id: upload run: | - mvn -B -ntp deploy -P release-artifacts,maven-central-publish,include-native-artifacts -DskipTests + # autoPublish=false + waitUntil=validated (set in core/pom.xml) makes the + # build block ONLY on validation (VALIDATING -> VALIDATED, a few minutes; + # the plugin's waitMaxTime ceiling is 1800s). It does NOT block on the + # actual publish to Maven Central. The deployment is left in a droppable + # VALIDATED state. Tests already ran in `verify` against this exact source + # + native libs, so skip them here. + mvn -B -ntp deploy \ + -P include-native-artifacts,release-artifacts,maven-central-publish \ + -DskipTests | tee deploy.log + + deployment_id="$(grep -oE 'deploymentId: [0-9a-fA-F-]{36}' deploy.log | head -n1 | awk '{print $2}')" + if [[ -z "${deployment_id}" ]]; then + echo "::error::Could not capture the Central deployment id from the build output." + exit 1 + fi + echo "deployment_id=${deployment_id}" >> "$GITHUB_OUTPUT" + echo "Validated deployment ${deployment_id}." + + - name: Publish the validated deployment to Maven Central + id: central-publish + env: + DEPLOYMENT_ID: ${{ steps.upload.outputs.deployment_id }} + run: | + token="$(printf '%s:%s' "$MAVEN_CENTRAL_USERNAME" "$MAVEN_CENTRAL_PASSWORD" | base64 | tr -d '\n')" + + # Commit point: flip VALIDATED -> PUBLISHING. A 2xx means Sonatype has + # accepted the deployment and WILL publish it. The actual upload to Maven + # Central (PUBLISHING -> PUBLISHED) and index propagation then proceed + # ASYNCHRONOUSLY -- typically 5-10 minutes, occasionally much longer. We + # deliberately do NOT block on PUBLISHED; a green run means "accepted for + # publishing", and Central visibility follows on its own schedule. + http_code="$(curl -sS -o publish-resp.txt -w '%{http_code}' -X POST \ + -H "Authorization: Bearer ${token}" \ + "https://central.sonatype.com/api/v1/publisher/deployment/${DEPLOYMENT_ID}")" + if [[ "${http_code}" != 2* ]]; then + echo "::error::Publish request for ${DEPLOYMENT_ID} returned HTTP ${http_code}." + cat publish-resp.txt || true + exit 1 + fi + echo "Publish accepted for ${DEPLOYMENT_ID} (HTTP ${http_code})." + + # Short, bounded peek (<= 2 min) only to catch an IMMEDIATE failure before + # we tag. A timeout here is NOT an error -- it just means publishing is + # still in progress, which is the normal slow path. + for _ in $(seq 1 8); do + state="$(curl -sS -X POST -H "Authorization: Bearer ${token}" \ + "https://central.sonatype.com/api/v1/publisher/status?id=${DEPLOYMENT_ID}" \ + | jq -r '.deploymentState // "UNKNOWN"')" + echo "Deployment ${DEPLOYMENT_ID} state: ${state}" + case "${state}" in + PUBLISHING|PUBLISHED) break ;; + FAILED) echo "::error::Central reported FAILED for ${DEPLOYMENT_ID}."; exit 1 ;; + *) sleep 15 ;; + esac + done + echo "Publishing is in progress; Maven Central propagation completes asynchronously." + + - name: Create and push release tag + run: | + # Reached only after Central owns the artifact: the tag pins the exact + # verified, published tree. We push ONLY the tag (never a commit to main). + git config user.name "GitHub Actions - Maven Release" + git config user.email "actions@github.com" + git commit -am "Release questdb-client ${RELEASE_VERSION}" + git tag -a "${RELEASE_VERSION}" -m "questdb-client ${RELEASE_VERSION}" + git push origin "refs/tags/${RELEASE_VERSION}" - name: Remove imported signing key if: always() @@ -491,3 +659,48 @@ jobs: if [[ -n "${GNUPGHOME:-}" && -d "${GNUPGHOME}" ]]; then rm -rf "$GNUPGHOME" fi + + open-bump-pr: + needs: + - resolve + - publish + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + pull-requests: write + env: + NEXT_DEVELOPMENT_VERSION: ${{ needs.resolve.outputs.next_development_version }} + RELEASE_VERSION: ${{ needs.resolve.outputs.release_version }} + SOURCE_REF: ${{ inputs.source_ref }} + GH_TOKEN: ${{ github.token }} + steps: + - name: Check out source ref + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + ref: ${{ inputs.source_ref }} + fetch-depth: 0 + + - name: Set up Java 11 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: temurin + java-version: "11" + cache: maven + + - name: Open next-development-version bump PR + run: | + branch="chore/bump-${NEXT_DEVELOPMENT_VERSION}" + mvn -B -ntp org.codehaus.mojo:versions-maven-plugin:2.16.2:set -DnewVersion="${NEXT_DEVELOPMENT_VERSION}" -DprocessAllModules=true -DgenerateBackupPoms=false + + git config user.name "GitHub Actions - Maven Release" + git config user.email "actions@github.com" + git checkout -b "${branch}" + git commit -am "Bump version to ${NEXT_DEVELOPMENT_VERSION}" + git push origin "${branch}" + + gh pr create \ + --base "${SOURCE_REF}" \ + --head "${branch}" \ + --title "Bump version to ${NEXT_DEVELOPMENT_VERSION}" \ + --body "Post-release housekeeping after publishing questdb-client ${RELEASE_VERSION}. Merge before the next release." diff --git a/README.md b/README.md index f46d2abe..97ef5ed0 100644 --- a/README.md +++ b/README.md @@ -240,23 +240,18 @@ mvn clean package -DskipTests ## Releasing -Maven Central publishing is owned by the manually triggered `Release to Maven Central` GitHub Actions workflow. Do not -publish from a local machine in the normal release path. +Maven Central publishing is owned by the manually triggered `Release to Maven Central` GitHub Actions workflow, run +from the Actions tab. Do not publish from a local machine and do not run `mvn deploy` in the normal release path. -The workflow runs `mvn release:prepare`, lets Maven infer the release version from the current `-SNAPSHOT` POM, builds -all native libraries from the generated release tag, stores them as GitHub Actions artifacts, downloads them into the -Maven build, and publishes the final JAR to Maven Central. +The workflow builds every platform's native library, runs the full test suite against those freshly built binaries, +and validates the signed bundle with the Central Portal **before** it pushes a git tag or publishes anything, so +nothing irreversible happens until the release is proven good. The release tag is pushed last and the next-development +version bump lands as a follow-up pull request, so `main` keeps its PR-only protection. -Run the workflow from the Actions tab. +The `publish` step is gated by the `maven-release` GitHub environment; configure it with required reviewers so the +workflow pauses for human approval before any credentials are used or anything is published. -The Maven Central publish job uses the `maven-release` GitHub environment as an additional gate; configure that -environment with required reviewers so the workflow also pauses for human approval before immutable Central publishing. - -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 workflow is also publishing creates competing Sonatype deployments for -the same coordinate. - -Full release procedure: [artifacts/release/README.md](artifacts/release/README.md). +Full release procedure, one-time setup, and failure handling: [artifacts/release/README.md](artifacts/release/README.md). ### Building Native Libraries diff --git a/artifacts/release/README.md b/artifacts/release/README.md index 2d45c22d..17e9137d 100644 --- a/artifacts/release/README.md +++ b/artifacts/release/README.md @@ -4,54 +4,100 @@ Steps to release `org.questdb:questdb-client` to Maven Central. The release is owned by the manually triggered [`Release to Maven Central`](../../.github/workflows/maven_central_release.yml) workflow. Do not create release tags -by hand, do not push version tags to trigger publishing, and do not run `mvn deploy` locally during the normal release -path. +by hand and do not run `mvn deploy` locally during the normal release path. + +The workflow is built so that nothing irreversible happens until the release has been proven good: it builds every +native library, runs the full test suite against those freshly built binaries, and validates the signed bundle with +the Central Portal **before** it ever pushes a git tag or publishes to Maven Central. The release tag is created last +and points at the exact tree that was verified and published. + +## One-time setup + +The `publish` job pushes the release tag using the built-in `GITHUB_TOKEN` (`github-actions[bot]`). The org-wide +`restrict-tag-pushing` ruleset blocks tag creation by default, so `github-actions[bot]` must be added as a **bypass +actor** on that ruleset (Organization settings -> Rules -> `restrict-tag-pushing` -> Bypass list). + +The branch ruleset on `main` is intentionally **not** bypassed. The next-development snapshot bump lands as an ordinary +pull request, so `main` keeps its "PR-only, squash, one approval" protection. + +The AWS secret referenced by `MAVEN_RELEASE_AWS_SECRET_ARN` must expose these JSON keys (they become environment +variables of the same name): `MAVEN_GPG_PRIVATE_KEY`, `MAVEN_CENTRAL_USERNAME`, `MAVEN_CENTRAL_PASSWORD`, and +optionally `MAVEN_GPG_PASSPHRASE` (omit or leave empty for a passphrase-less signing key). + +Configure the `maven-release` GitHub environment with required reviewers. The `publish` job is attached to that +environment, so the workflow pauses for human approval before any credentials are used and before anything is +published. ## Prepare release notes -Create a draft GitHub release with the intended version and notes. Do not create the git tag up front. The workflow -creates the tag through `mvn release:prepare`; finalize the GitHub release after Maven Central propagation. +Create a draft GitHub release with the intended version and notes. Do not create the git tag up front -- the workflow +creates it. Finalize the GitHub release after Maven Central propagation (see [Post-release](#post-release)). ## Publish -Start the `Release to Maven Central` workflow from the Actions tab. - -Use these inputs: +Start the `Release to Maven Central` workflow from the Actions tab with these inputs: - `source_ref`: branch/ref to release from, usually `main` - `release_version_override`: blank unless doing a non-standard version - `next_development_version_override`: blank unless doing a non-standard next snapshot -The release path uses the generated Maven tag as the source of truth. It pushes the `mvn release:prepare` commits -back to `source_ref`, pushes the generated release tag, builds native libraries from that tag, downloads the native -artifacts into the Maven build, signs the release artifacts, and uploads through the Sonatype Central Portal. +The workflow runs as a pipeline: -The final Central upload runs in the `publish-central` job, which is attached to the `maven-release` GitHub -environment. As a second layer of protection, configure that environment with required reviewers so the workflow also -pauses for human approval before the immutable Maven Central publish step. +1. **resolve** -- derives the release and next-development versions from the current `-SNAPSHOT` POM, and fails fast if + the tag already exists or the version is already on Maven Central. +2. **build (5 jobs)** -- builds the native library for each platform (darwin-aarch64, darwin-x86-64, linux-x86-64, + linux-aarch64, windows-x86-64) from the resolved source commit, and smoke-loads each one. +3. **verify** -- bundles all five native libraries and runs the full test suite against them with the release version + applied. This is the quality gate; it requires no credentials. +4. **publish** (gated by the `maven-release` environment) -- after approval: signs and uploads the bundle to the + Central Portal as a droppable `VALIDATED` deployment, then publishes it, then pushes the release tag. +5. **open-bump-pr** -- opens the next-development-version bump PR (post-release, see below). -The workflow returns once Sonatype has validated the upload and taken ownership of the artifacts. Physical propagation -to Maven Central happens asynchronously after the workflow finishes, so a green run does not guarantee the artifacts -are immediately visible on `central.sonatype.com`. +Approve the `publish` job when prompted. The run is green once the Central Portal has accepted the deployment for +publishing and the tag has been pushed. ## Versioning -In the normal path, leave both version override inputs blank. `mvn release:prepare` derives: +In the normal path, leave both override inputs blank. The workflow derives: - release version from the current POM, for example `1.3.2-SNAPSHOT` -> `1.3.2` -- release tag from the release version, via `tagNameFormat=@{project.version}` -- next development version from Maven Release Plugin defaults, for example `1.3.3-SNAPSHOT` +- release tag equal to the release version +- next development version by bumping the patch, for example `1.3.3-SNAPSHOT` -Use `release_version_override` and `next_development_version_override` only for non-standard releases. +Use `release_version_override` and `next_development_version_override` only for non-standard releases (for example a +new minor or major line). + +## The snapshot-bump PR + +`main` stays at its current `-SNAPSHOT` during the release; the `open-bump-pr` job opens a PR that bumps it to the next +development version. That PR is **post-release housekeeping** -- it does not affect the release you just shipped, and a +delay in merging it does not invalidate anything. + +It must, however, be merged **before the next release**. If it is not, `main` is still at the just-released +`-SNAPSHOT`, and the next run's `resolve` step will refuse to re-release a version whose tag already exists. Merge the +bump PR (squash, like any PR to `main`) once the release is confirmed. ## Failure handling -If a release fails before the final Maven Central upload, inspect whether the prepare commits or tag were pushed. -If they were pushed, either rerun from the same source state after fixing the workflow or clean up the failed release -state deliberately. Do not reuse a version that may already have reached Maven Central; Maven Central coordinates are -immutable. +The pipeline is ordered so failures are clean: + +- A failure in `resolve`, any `build`, or `verify` happens **before** anything is tagged or published. Fix the cause + and rerun; nothing was mutated. +- In `publish`, the bundle is uploaded as a droppable `VALIDATED` deployment first. If validation fails, nothing is + published and the deployment can be dropped from the Central Portal. The release tag is pushed only **after** the + Central Portal has accepted the deployment for publishing. + +If a run fails inside `publish` after the Central Portal accepted the deployment, the artifact is on its way to Maven +Central and the coordinate is immutable -- do not reuse the version. If the tag push is what failed, create the tag +manually on the published commit; do not start a fresh release for the same version. ## Post-release -Check [Maven Central](https://central.sonatype.com/artifact/org.questdb/questdb-client) until the new version is -listed, then finalize the GitHub release draft against the generated tag and add the release notes. +Publishing to Maven Central is asynchronous. After the Central Portal accepts the deployment, propagation to +`central.sonatype.com` and the public index typically takes a few minutes but can occasionally take longer, so a green +run does not mean the artifact is immediately downloadable. + +1. Merge the `open-bump-pr` pull request. +2. Watch [Maven Central](https://central.sonatype.com/artifact/org.questdb/questdb-client) until the new version is + listed. +3. Finalize the draft GitHub release against the pushed tag and add the release notes. diff --git a/core/pom.xml b/core/pom.xml index 2665f244..dcb2b8ea 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -398,7 +398,11 @@ true central - true + + false validated From ca2ba24c91910369ba637f25f03bf4ecf2432df8 Mon Sep 17 00:00:00 2001 From: Steve Sklar Date: Thu, 28 May 2026 15:35:19 -0400 Subject: [PATCH 11/15] Push tag before publishing and harden release shell Address self-review findings on the release workflow: - Push the release tag before the irreversible Central publish POST, not after. Validation already proved the bundle good; a tag is deletable, a publish is not, so a tag-push failure (e.g. missing bot bypass) now leaves nothing published and a clean rerun. - Guard deployment-id capture and the status peek with || true so a no-match or transient curl/jq error cannot abort the step under errexit; the status peek timeout stays non-fatal. - Only commit the version bump when versions:set actually changed the poms, so a no-op override does not abort after upload. - Make open-bump-pr idempotent: force-with-lease the branch and skip PR creation when one already exists. - Re-check the Central version (not just the tag) in publish after the environment gate. - Read the release version from core/pom.xml, the shipped artifact. - Build both macOS targets to completion (fail-fast: false). - Fix the README Maven coordinate (org.questdb:questdb-client) and align the release docs with the tag-before-publish order and test coverage. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/maven_central_release.yml | 98 ++++++++++++++------- README.md | 16 ++-- artifacts/release/README.md | 30 ++++--- 3 files changed, 93 insertions(+), 51 deletions(-) diff --git a/.github/workflows/maven_central_release.yml b/.github/workflows/maven_central_release.yml index 31d5cc1c..4b227eea 100644 --- a/.github/workflows/maven_central_release.yml +++ b/.github/workflows/maven_central_release.yml @@ -81,7 +81,9 @@ jobs: run: | set -euo pipefail - pom_version="$(mvn -B -q -N -DforceStdout help:evaluate -Dexpression=project.version)" + # Read the version of the published artifact (core/questdb-client), not the + # aggregator root -- core has its own and is what ships. + pom_version="$(mvn -B -q -N -f core/pom.xml -DforceStdout help:evaluate -Dexpression=project.version)" if [[ -z "${pom_version}" ]]; then echo "::error::Could not read project version from ${{ inputs.source_ref }}." exit 1 @@ -142,7 +144,9 @@ jobs: build-macos: needs: resolve strategy: - fail-fast: true + # Build both macOS targets to completion so a failure reports per-arch instead + # of cancelling the sibling; publish needs both anyway. + fail-fast: false matrix: include: - os: macos-14 @@ -500,12 +504,20 @@ jobs: ref: ${{ needs.resolve.outputs.source_sha }} fetch-depth: 0 - - name: Re-assert the tag is still free + - name: Re-assert the tag and Central version are still free run: | + # The environment gate can hold this job for a long time; re-check both + # guards just before we touch anything irreversible. if git ls-remote --exit-code --tags origin "refs/tags/${RELEASE_VERSION}" >/dev/null 2>&1; then echo "::error::Tag ${RELEASE_VERSION} appeared since resolve. Aborting." exit 1 fi + central_pom="https://repo1.maven.org/maven2/org/questdb/questdb-client/${RELEASE_VERSION}/questdb-client-${RELEASE_VERSION}.pom" + http_code="$(curl -sS -o /dev/null -w '%{http_code}' "${central_pom}" || echo "000")" + if [[ "${http_code}" == "200" ]]; then + echo "::error::questdb-client ${RELEASE_VERSION} is already on Maven Central. Aborting." + exit 1 + fi - name: Set up Java 11 uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 @@ -596,7 +608,9 @@ jobs: -P include-native-artifacts,release-artifacts,maven-central-publish \ -DskipTests | tee deploy.log - deployment_id="$(grep -oE 'deploymentId: [0-9a-fA-F-]{36}' deploy.log | head -n1 | awk '{print $2}')" + # `|| true` so a no-match does not abort under errexit before the friendly + # message (which also flags a plugin log-format change to whoever runs this). + deployment_id="$(grep -oE 'deploymentId: [0-9a-fA-F-]{36}' deploy.log | head -n1 | awk '{print $2}' || true)" if [[ -z "${deployment_id}" ]]; then echo "::error::Could not capture the Central deployment id from the build output." exit 1 @@ -604,6 +618,24 @@ jobs: echo "deployment_id=${deployment_id}" >> "$GITHUB_OUTPUT" echo "Validated deployment ${deployment_id}." + - name: Create and push release tag + run: | + # The bundle is VALIDATED but not yet published. Push the tag now, BEFORE the + # irreversible publish: a tag is deletable (a bypass actor can drop it), so a + # tag-push failure (e.g. the github-actions[bot] tag-ruleset bypass was not + # configured) leaves NOTHING published -- a clean, rerunnable state. The + # publish POST below is the single irreversible action and runs last. + git config user.name "GitHub Actions - Maven Release" + git config user.email "actions@github.com" + # versions:set normally rewrote the poms (SNAPSHOT -> release); only a version + # override matching the current poms makes it a no-op, so commit only if there + # is a change. Either way the tag pins the release-versioned tree. + if ! git diff --quiet; then + git commit -am "Release questdb-client ${RELEASE_VERSION}" + fi + git tag -a "${RELEASE_VERSION}" -m "questdb-client ${RELEASE_VERSION}" + git push origin "refs/tags/${RELEASE_VERSION}" + - name: Publish the validated deployment to Maven Central id: central-publish env: @@ -611,9 +643,9 @@ jobs: run: | token="$(printf '%s:%s' "$MAVEN_CENTRAL_USERNAME" "$MAVEN_CENTRAL_PASSWORD" | base64 | tr -d '\n')" - # Commit point: flip VALIDATED -> PUBLISHING. A 2xx means Sonatype has - # accepted the deployment and WILL publish it. The actual upload to Maven - # Central (PUBLISHING -> PUBLISHED) and index propagation then proceed + # The single irreversible step: flip VALIDATED -> PUBLISHING. A 2xx means + # Sonatype has accepted the deployment and WILL publish it. The actual upload + # to Maven Central (PUBLISHING -> PUBLISHED) and index propagation then proceed # ASYNCHRONOUSLY -- typically 5-10 minutes, occasionally much longer. We # deliberately do NOT block on PUBLISHED; a green run means "accepted for # publishing", and Central visibility follows on its own schedule. @@ -627,13 +659,14 @@ jobs: fi echo "Publish accepted for ${DEPLOYMENT_ID} (HTTP ${http_code})." - # Short, bounded peek (<= 2 min) only to catch an IMMEDIATE failure before - # we tag. A timeout here is NOT an error -- it just means publishing is - # still in progress, which is the normal slow path. + # Best-effort peek to surface an IMMEDIATE failure. A transient curl/jq error + # or a still-in-progress state is NOT fatal here -- we never wait out the + # (possibly hour-long) asynchronous publish/propagation. for _ in $(seq 1 8); do state="$(curl -sS -X POST -H "Authorization: Bearer ${token}" \ "https://central.sonatype.com/api/v1/publisher/status?id=${DEPLOYMENT_ID}" \ - | jq -r '.deploymentState // "UNKNOWN"')" + | jq -r '.deploymentState // "UNKNOWN"' 2>/dev/null || true)" + [[ -n "${state}" ]] || state="UNKNOWN" echo "Deployment ${DEPLOYMENT_ID} state: ${state}" case "${state}" in PUBLISHING|PUBLISHED) break ;; @@ -643,16 +676,6 @@ jobs: done echo "Publishing is in progress; Maven Central propagation completes asynchronously." - - name: Create and push release tag - run: | - # Reached only after Central owns the artifact: the tag pins the exact - # verified, published tree. We push ONLY the tag (never a commit to main). - git config user.name "GitHub Actions - Maven Release" - git config user.email "actions@github.com" - git commit -am "Release questdb-client ${RELEASE_VERSION}" - git tag -a "${RELEASE_VERSION}" -m "questdb-client ${RELEASE_VERSION}" - git push origin "refs/tags/${RELEASE_VERSION}" - - name: Remove imported signing key if: always() run: | @@ -691,16 +714,29 @@ jobs: - name: Open next-development-version bump PR run: | branch="chore/bump-${NEXT_DEVELOPMENT_VERSION}" - mvn -B -ntp org.codehaus.mojo:versions-maven-plugin:2.16.2:set -DnewVersion="${NEXT_DEVELOPMENT_VERSION}" -DprocessAllModules=true -DgenerateBackupPoms=false - git config user.name "GitHub Actions - Maven Release" git config user.email "actions@github.com" - git checkout -b "${branch}" - git commit -am "Bump version to ${NEXT_DEVELOPMENT_VERSION}" - git push origin "${branch}" + git checkout -B "${branch}" - gh pr create \ - --base "${SOURCE_REF}" \ - --head "${branch}" \ - --title "Bump version to ${NEXT_DEVELOPMENT_VERSION}" \ - --body "Post-release housekeeping after publishing questdb-client ${RELEASE_VERSION}. Merge before the next release." + mvn -B -ntp org.codehaus.mojo:versions-maven-plugin:2.16.2:set -DnewVersion="${NEXT_DEVELOPMENT_VERSION}" -DprocessAllModules=true -DgenerateBackupPoms=false + + # Idempotent: if ${SOURCE_REF} is already at the next version (e.g. this job + # is being re-run), there is nothing to bump. + if git diff --quiet; then + echo "${SOURCE_REF} is already at ${NEXT_DEVELOPMENT_VERSION}; nothing to bump." + exit 0 + fi + + git commit -am "Bump version to ${NEXT_DEVELOPMENT_VERSION}" + git push --force-with-lease origin "${branch}" + + # Don't fail if a bump PR for this branch already exists (re-run case). + if [[ -z "$(gh pr list --head "${branch}" --state open --json number --jq '.[].number')" ]]; then + gh pr create \ + --base "${SOURCE_REF}" \ + --head "${branch}" \ + --title "Bump version to ${NEXT_DEVELOPMENT_VERSION}" \ + --body "Post-release housekeeping after publishing questdb-client ${RELEASE_VERSION}. Merge before the next release." + else + echo "A bump PR for ${branch} already exists." + fi diff --git a/README.md b/README.md index 97ef5ed0..f2b4f61a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@
-[![Maven Central](https://img.shields.io/maven-central/v/org.questdb/client.svg)](https://central.sonatype.com/artifact/org.questdb/client) +[![Maven Central](https://img.shields.io/maven-central/v/org.questdb/questdb-client.svg)](https://central.sonatype.com/artifact/org.questdb/questdb-client) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
@@ -31,7 +31,7 @@ The client uses the [InfluxDB Line Protocol](https://questdb.com/docs/reference/ ```xml org.questdb - client + questdb-client 1.0.0 ``` @@ -39,10 +39,10 @@ The client uses the [InfluxDB Line Protocol](https://questdb.com/docs/reference/ **Gradle:** ```groovy -implementation 'org.questdb:client:1.0.0' +implementation 'org.questdb:questdb-client:1.0.0' ``` -Replace `1.0.0` with the latest version from [Maven Central](https://central.sonatype.com/artifact/org.questdb/client). +Replace `1.0.0` with the latest version from [Maven Central](https://central.sonatype.com/artifact/org.questdb/questdb-client). ### Start QuestDB @@ -243,10 +243,10 @@ mvn clean package -DskipTests Maven Central publishing is owned by the manually triggered `Release to Maven Central` GitHub Actions workflow, run from the Actions tab. Do not publish from a local machine and do not run `mvn deploy` in the normal release path. -The workflow builds every platform's native library, runs the full test suite against those freshly built binaries, -and validates the signed bundle with the Central Portal **before** it pushes a git tag or publishes anything, so -nothing irreversible happens until the release is proven good. The release tag is pushed last and the next-development -version bump lands as a follow-up pull request, so `main` keeps its PR-only protection. +The workflow builds every platform's native library, runs the full test suite with those freshly built binaries +bundled, and validates the signed bundle with the Central Portal **before** it pushes a git tag or publishes anything. +The Central publish is the single irreversible step and runs last; the next-development version bump lands as a +follow-up pull request, so `main` keeps its PR-only protection. The `publish` step is gated by the `maven-release` GitHub environment; configure it with required reviewers so the workflow pauses for human approval before any credentials are used or anything is published. diff --git a/artifacts/release/README.md b/artifacts/release/README.md index 17e9137d..c346f297 100644 --- a/artifacts/release/README.md +++ b/artifacts/release/README.md @@ -7,9 +7,9 @@ The release is owned by the manually triggered by hand and do not run `mvn deploy` locally during the normal release path. The workflow is built so that nothing irreversible happens until the release has been proven good: it builds every -native library, runs the full test suite against those freshly built binaries, and validates the signed bundle with -the Central Portal **before** it ever pushes a git tag or publishes to Maven Central. The release tag is created last -and points at the exact tree that was verified and published. +native library, runs the full test suite with those freshly built binaries bundled, and validates the signed bundle +with the Central Portal **before** it pushes a git tag or publishes to Maven Central. The tag is pushed only after +validation and points at the exact verified tree; the Central publish is the single irreversible step and runs last. ## One-time setup @@ -47,10 +47,12 @@ The workflow runs as a pipeline: the tag already exists or the version is already on Maven Central. 2. **build (5 jobs)** -- builds the native library for each platform (darwin-aarch64, darwin-x86-64, linux-x86-64, linux-aarch64, windows-x86-64) from the resolved source commit, and smoke-loads each one. -3. **verify** -- bundles all five native libraries and runs the full test suite against them with the release version - applied. This is the quality gate; it requires no credentials. +3. **verify** -- bundles all five native libraries and runs the full test suite with the release version applied. The + suite runs on a Linux runner, so it exercises the Linux x86-64 library directly; the other four libraries were + smoke-loaded on their own platforms in the build step. This is the quality gate; it requires no credentials. 4. **publish** (gated by the `maven-release` environment) -- after approval: signs and uploads the bundle to the - Central Portal as a droppable `VALIDATED` deployment, then publishes it, then pushes the release tag. + Central Portal as a droppable `VALIDATED` deployment, pushes the release tag, then performs the single irreversible + step of publishing the deployment. 5. **open-bump-pr** -- opens the next-development-version bump PR (post-release, see below). Approve the `publish` job when prompted. The run is green once the Central Portal has accepted the deployment for @@ -84,12 +86,16 @@ The pipeline is ordered so failures are clean: - A failure in `resolve`, any `build`, or `verify` happens **before** anything is tagged or published. Fix the cause and rerun; nothing was mutated. - In `publish`, the bundle is uploaded as a droppable `VALIDATED` deployment first. If validation fails, nothing is - published and the deployment can be dropped from the Central Portal. The release tag is pushed only **after** the - Central Portal has accepted the deployment for publishing. - -If a run fails inside `publish` after the Central Portal accepted the deployment, the artifact is on its way to Maven -Central and the coordinate is immutable -- do not reuse the version. If the tag push is what failed, create the tag -manually on the published commit; do not start a fresh release for the same version. + published and the deployment can be dropped from the Central Portal. +- The release tag is pushed next, while the deployment is still only `VALIDATED`. If the tag push fails (for example + the `restrict-tag-pushing` bypass for `github-actions[bot]` was not configured), nothing has been published yet -- + fix the cause and rerun. +- The Central publish runs last. If the run fails at this step after the tag was already pushed, the deployment is + still `VALIDATED` on the Central Portal: re-publish it from the Portal UI (the run logged its `deploymentId`), or + drop it and rerun after deleting the tag. + +Once the Central Portal has accepted the deployment for publishing, the coordinate is immutable -- do not reuse the +version. ## Post-release From 756147f5b5ed95bcc2b41762613f032bdf497c79 Mon Sep 17 00:00:00 2001 From: Steve Sklar Date: Fri, 29 May 2026 15:23:28 -0400 Subject: [PATCH 12/15] Fix bump-branch force push and roll back tag on publish failure Second self-review pass: - open-bump-pr pushed the bump branch with --force-with-lease, but the job never fetches that branch, so there is no lease reference and the push is rejected ("stale info") whenever the branch already exists (and on first creation). Use plain --force; the branch is a throwaway owned solely by this workflow. - Tag is now pushed before the publish POST, so a POST failure used to leave a tag that blocks reruns while nothing was published. Add an if: failure() step that deletes the tag; the publish job fails only when nothing reached Central, so this never drops a real release's tag. - Correct the release doc: the Windows DLL is cross-compiled on Linux and only objdump-checked, not load-tested like the macOS/Linux libraries. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/maven_central_release.yml | 15 ++++++++++++++- artifacts/release/README.md | 5 +++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/maven_central_release.yml b/.github/workflows/maven_central_release.yml index 4b227eea..b281786f 100644 --- a/.github/workflows/maven_central_release.yml +++ b/.github/workflows/maven_central_release.yml @@ -676,6 +676,16 @@ jobs: done echo "Publishing is in progress; Maven Central propagation completes asynchronously." + - name: Roll back release tag if nothing was published + if: failure() + run: | + # This job fails only when nothing reached Maven Central: validation failed, + # the publish POST was rejected, or Central reported FAILED. In every such + # case the tag (if it was pushed) points at an unpublished release, so drop it + # to keep reruns clean. Best-effort: a no-op if the tag was never pushed. + # (A successful POST exits 0, so this never deletes a tag for a real publish.) + git push origin ":refs/tags/${RELEASE_VERSION}" || true + - name: Remove imported signing key if: always() run: | @@ -728,7 +738,10 @@ jobs: fi git commit -am "Bump version to ${NEXT_DEVELOPMENT_VERSION}" - git push --force-with-lease origin "${branch}" + # Plain --force: this branch is a throwaway owned solely by this workflow, + # and the job never fetches it, so --force-with-lease has no lease ref and + # would be rejected ("stale info") when the branch already exists. + git push --force origin "${branch}" # Don't fail if a bump PR for this branch already exists (re-run case). if [[ -z "$(gh pr list --head "${branch}" --state open --json number --jq '.[].number')" ]]; then diff --git a/artifacts/release/README.md b/artifacts/release/README.md index c346f297..1aac2da9 100644 --- a/artifacts/release/README.md +++ b/artifacts/release/README.md @@ -48,8 +48,9 @@ The workflow runs as a pipeline: 2. **build (5 jobs)** -- builds the native library for each platform (darwin-aarch64, darwin-x86-64, linux-x86-64, linux-aarch64, windows-x86-64) from the resolved source commit, and smoke-loads each one. 3. **verify** -- bundles all five native libraries and runs the full test suite with the release version applied. The - suite runs on a Linux runner, so it exercises the Linux x86-64 library directly; the other four libraries were - smoke-loaded on their own platforms in the build step. This is the quality gate; it requires no credentials. + suite runs on a Linux runner, so it exercises the Linux x86-64 library directly. The macOS and Linux aarch64 + libraries are load-tested in their own build jobs; the Windows DLL is cross-compiled on Linux and so is only checked + for unwanted runtime dependencies (`objdump`), not loaded. This is the quality gate; it requires no credentials. 4. **publish** (gated by the `maven-release` environment) -- after approval: signs and uploads the bundle to the Central Portal as a droppable `VALIDATED` deployment, pushes the release tag, then performs the single irreversible step of publishing the deployment. From 4bc96600c1a5b2e195342a068f18df493913b5a8 Mon Sep 17 00:00:00 2001 From: Steve Sklar Date: Fri, 29 May 2026 15:41:11 -0400 Subject: [PATCH 13/15] Never roll back the release tag once publish is accepted The tag-rollback step was gated only on if: failure(), which can fire after the publish POST already returned 2xx (the deployment is then irreversibly Sonatype's) -- via the status peek exiting on FAILED or a job timeout mid-peek -- deleting the tag of a release that will publish. Set a published=true step output immediately after the 2xx accept and gate the rollback on `failure() && steps.central-publish.outputs.published != 'true'`. Outputs persist even if the step later fails, so the tag can never be rolled back once the publish was accepted. Rollback now runs only for failures before the POST, where nothing reached Central. Also warn, instead of silently swallowing, when the tag deletion itself fails. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/maven_central_release.yml | 25 ++++++++++++++------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/.github/workflows/maven_central_release.yml b/.github/workflows/maven_central_release.yml index b281786f..12797ae6 100644 --- a/.github/workflows/maven_central_release.yml +++ b/.github/workflows/maven_central_release.yml @@ -658,6 +658,11 @@ jobs: exit 1 fi echo "Publish accepted for ${DEPLOYMENT_ID} (HTTP ${http_code})." + # Mark the publish as committed BEFORE the peek loop. From here the deployment + # is irreversibly Sonatype's, so the release tag must NEVER be rolled back -- + # even if a later status read, an exit on FAILED, or a job timeout marks this + # step/job failed. Outputs written here persist even if the step later exits 1. + echo "published=true" >> "$GITHUB_OUTPUT" # Best-effort peek to surface an IMMEDIATE failure. A transient curl/jq error # or a still-in-progress state is NOT fatal here -- we never wait out the @@ -677,14 +682,18 @@ jobs: echo "Publishing is in progress; Maven Central propagation completes asynchronously." - name: Roll back release tag if nothing was published - if: failure() - run: | - # This job fails only when nothing reached Maven Central: validation failed, - # the publish POST was rejected, or Central reported FAILED. In every such - # case the tag (if it was pushed) points at an unpublished release, so drop it - # to keep reruns clean. Best-effort: a no-op if the tag was never pushed. - # (A successful POST exits 0, so this never deletes a tag for a real publish.) - git push origin ":refs/tags/${RELEASE_VERSION}" || true + # Gated on the publish marker, NOT just failure(): once the POST is accepted + # (published=true) the deployment is irreversibly Sonatype's, so the tag must + # survive even if a later status read, timeout, or cleanup fails. This step runs + # only for failures BEFORE the publish was accepted -- where nothing reached + # Central and the tag (if it was pushed) is safe to drop, keeping reruns clean. + if: failure() && steps.central-publish.outputs.published != 'true' + run: | + if git push origin ":refs/tags/${RELEASE_VERSION}"; then + echo "Rolled back tag ${RELEASE_VERSION} (nothing was published)." + else + echo "::warning::Could not delete tag ${RELEASE_VERSION} (it may never have been pushed). Remove it manually before rerunning." + fi - name: Remove imported signing key if: always() From 24068b9e1156687ffbce8e87758934f13b93ac7a Mon Sep 17 00:00:00 2001 From: Steve Sklar Date: Fri, 29 May 2026 15:53:20 -0400 Subject: [PATCH 14/15] Skip bump PR for non-branch source_ref and document GPG key format - open-bump-pr opened the PR with --base "${SOURCE_REF}", which fails for a tag or SHA ref and would redden a run whose release already succeeded. Skip the bump with a notice when source_ref is not a branch; the bump is post-release housekeeping and can be done manually. - Document that MAVEN_GPG_PRIVATE_KEY must be the ASCII-armored key as a JSON-escaped string (newlines as \n) so parse-json-secrets restores real newlines for gpg --import -- the most common Central release failure. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/maven_central_release.yml | 9 +++++++++ artifacts/release/README.md | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/.github/workflows/maven_central_release.yml b/.github/workflows/maven_central_release.yml index 12797ae6..699226af 100644 --- a/.github/workflows/maven_central_release.yml +++ b/.github/workflows/maven_central_release.yml @@ -732,6 +732,15 @@ jobs: - name: Open next-development-version bump PR run: | + # The bump PR needs a branch to target. If the release was run from a tag or + # SHA (a non-standard path), there is no branch to open a PR against -- skip + # gracefully rather than fail this post-release housekeeping job and redden a + # run whose release already succeeded. + if ! git ls-remote --exit-code --heads origin "${SOURCE_REF}" >/dev/null 2>&1; then + echo "::notice::source_ref '${SOURCE_REF}' is not a branch; skipping the automatic snapshot bump. Bump the development version manually." + exit 0 + fi + branch="chore/bump-${NEXT_DEVELOPMENT_VERSION}" git config user.name "GitHub Actions - Maven Release" git config user.email "actions@github.com" diff --git a/artifacts/release/README.md b/artifacts/release/README.md index 1aac2da9..c2a01511 100644 --- a/artifacts/release/README.md +++ b/artifacts/release/README.md @@ -24,6 +24,12 @@ The AWS secret referenced by `MAVEN_RELEASE_AWS_SECRET_ARN` must expose these JS variables of the same name): `MAVEN_GPG_PRIVATE_KEY`, `MAVEN_CENTRAL_USERNAME`, `MAVEN_CENTRAL_PASSWORD`, and optionally `MAVEN_GPG_PASSPHRASE` (omit or leave empty for a passphrase-less signing key). +Store `MAVEN_GPG_PRIVATE_KEY` as the **ASCII-armored** private key inside the JSON string value, with the newlines +encoded as `\n` (i.e. a normal JSON-escaped string). `parse-json-secrets` decodes those `\n` back into real newlines +when it sets the environment variable, so `gpg --import` receives a valid armored key. A key stored with literal +backslash-n, or pasted as a raw multi-line blob that breaks the JSON, imports as garbage and signing fails during +`mvn deploy` -- the most common cause of a failed release. Verify with a test run before relying on it. + Configure the `maven-release` GitHub environment with required reviewers. The `publish` job is attached to that environment, so the workflow pauses for human approval before any credentials are used and before anything is published. From 6f26f1b021cb5a916f6651494d3b14703ea73d8c Mon Sep 17 00:00:00 2001 From: Steven Sklar Date: Mon, 1 Jun 2026 11:49:13 -0400 Subject: [PATCH 15/15] Fix release tag ruleset bypass --- .github/workflows/maven_central_release.yml | 36 +++++++++++++++------ README.md | 3 ++ artifacts/release/README.md | 17 +++++++--- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/.github/workflows/maven_central_release.yml b/.github/workflows/maven_central_release.yml index 699226af..1b5d0d10 100644 --- a/.github/workflows/maven_central_release.yml +++ b/.github/workflows/maven_central_release.yml @@ -12,9 +12,15 @@ name: Release to Maven Central # lands as a normal pull request (main is PR-only by org ruleset). # # Org-settings prerequisites (one-time, NOT enforceable from this file): -# * `restrict-tag-pushing` ruleset: add `github-actions[bot]` as a bypass actor -# so the publish job can push the release tag. (The branch ruleset on `main` -# is intentionally NOT bypassed -- the snapshot bump goes through a PR.) +# * `restrict-tag-pushing` ruleset: add the dedicated Maven release GitHub App +# as a bypass actor so the publish job can push/delete the release tag. The +# built-in `GITHUB_TOKEN` (`github-actions[bot]`) is not usable for this bypass. +# The branch ruleset on `main` is intentionally NOT bypassed -- the snapshot +# bump goes through a PR. +# * Repository variable MAVEN_RELEASE_GITHUB_APP_CLIENT_ID and `maven-release` +# environment secret MAVEN_RELEASE_GITHUB_APP_PRIVATE_KEY must identify that +# GitHub App. The app must be installed on this repository with +# Contents: read/write. # * AWS secret referenced by MAVEN_RELEASE_AWS_SECRET_ARN must expose these # JSON keys (parse-json-secrets turns them into env vars of the same name): # MAVEN_GPG_PRIVATE_KEY, MAVEN_CENTRAL_USERNAME, MAVEN_CENTRAL_PASSWORD, @@ -472,7 +478,7 @@ jobs: environment: maven-release timeout-minutes: 45 permissions: - contents: write # push the release tag (requires github-actions[bot] tag-ruleset bypass) + contents: read # GITHUB_TOKEN only; tag push uses the release GitHub App token. id-token: write # AWS OIDC env: RELEASE_VERSION: ${{ needs.resolve.outputs.release_version }} @@ -482,15 +488,18 @@ jobs: MAVEN_RELEASE_AWS_SECRET_ARN: ${{ secrets.MAVEN_RELEASE_AWS_SECRET_ARN }} steps: - name: Validate workflow configuration + env: + MAVEN_RELEASE_GITHUB_APP_CLIENT_ID: ${{ vars.MAVEN_RELEASE_GITHUB_APP_CLIENT_ID }} + MAVEN_RELEASE_GITHUB_APP_PRIVATE_KEY: ${{ secrets.MAVEN_RELEASE_GITHUB_APP_PRIVATE_KEY }} run: | - required_vars=(MAVEN_RELEASE_AWS_REGION) + required_vars=(MAVEN_RELEASE_AWS_REGION MAVEN_RELEASE_GITHUB_APP_CLIENT_ID) for var_name in "${required_vars[@]}"; do if [[ -z "${!var_name:-}" ]]; then echo "::error::Repository variable ${var_name} is required." exit 1 fi done - required_secrets=(MAVEN_RELEASE_AWS_ROLE_ARN MAVEN_RELEASE_AWS_SECRET_ARN) + required_secrets=(MAVEN_RELEASE_AWS_ROLE_ARN MAVEN_RELEASE_AWS_SECRET_ARN MAVEN_RELEASE_GITHUB_APP_PRIVATE_KEY) for secret_name in "${required_secrets[@]}"; do if [[ -z "${!secret_name:-}" ]]; then echo "::error::GitHub secret ${secret_name} is required." @@ -498,11 +507,20 @@ jobs: fi done + - name: Create release GitHub App token + id: release-app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.MAVEN_RELEASE_GITHUB_APP_CLIENT_ID }} + private-key: ${{ secrets.MAVEN_RELEASE_GITHUB_APP_PRIVATE_KEY }} + permission-contents: write + - name: Check out release source uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{ needs.resolve.outputs.source_sha }} fetch-depth: 0 + token: ${{ steps.release-app-token.outputs.token }} - name: Re-assert the tag and Central version are still free run: | @@ -622,9 +640,9 @@ jobs: run: | # The bundle is VALIDATED but not yet published. Push the tag now, BEFORE the # irreversible publish: a tag is deletable (a bypass actor can drop it), so a - # tag-push failure (e.g. the github-actions[bot] tag-ruleset bypass was not - # configured) leaves NOTHING published -- a clean, rerunnable state. The - # publish POST below is the single irreversible action and runs last. + # tag-push failure (e.g. the release GitHub App tag-ruleset bypass was not + # configured) leaves NOTHING published -- a clean, rerunnable state. The publish + # POST below is the single irreversible action and runs last. git config user.name "GitHub Actions - Maven Release" git config user.email "actions@github.com" # versions:set normally rewrote the poms (SNAPSHOT -> release); only a version diff --git a/README.md b/README.md index f2b4f61a..ab127c6e 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,9 @@ follow-up pull request, so `main` keeps its PR-only protection. The `publish` step is gated by the `maven-release` GitHub environment; configure it with required reviewers so the workflow pauses for human approval before any credentials are used or anything is published. +The release tag push uses a dedicated Maven release GitHub App that must be allowed to bypass the org +`restrict-tag-pushing` ruleset; the built-in `GITHUB_TOKEN`/`github-actions[bot]` cannot be added for that bypass. + Full release procedure, one-time setup, and failure handling: [artifacts/release/README.md](artifacts/release/README.md). ### Building Native Libraries diff --git a/artifacts/release/README.md b/artifacts/release/README.md index c2a01511..931079ec 100644 --- a/artifacts/release/README.md +++ b/artifacts/release/README.md @@ -13,9 +13,16 @@ validation and points at the exact verified tree; the Central publish is the sin ## One-time setup -The `publish` job pushes the release tag using the built-in `GITHUB_TOKEN` (`github-actions[bot]`). The org-wide -`restrict-tag-pushing` ruleset blocks tag creation by default, so `github-actions[bot]` must be added as a **bypass -actor** on that ruleset (Organization settings -> Rules -> `restrict-tag-pushing` -> Bypass list). +The `publish` job pushes and, on pre-publish failure, deletes the release tag using a dedicated GitHub App installation +token. The org-wide `restrict-tag-pushing` ruleset blocks tag changes by default, and GitHub does not expose the +built-in `GITHUB_TOKEN` identity (`github-actions[bot]`) as a usable bypass actor. Create a dedicated Maven release +GitHub App instead, install it on this repository, grant it **Contents: read/write**, and add that app as a **bypass +actor** on the ruleset (Organization settings -> Rules -> `restrict-tag-pushing` -> Bypass list). + +Store the app credentials for the workflow: + +- repository variable `MAVEN_RELEASE_GITHUB_APP_CLIENT_ID`: the app's client ID +- `maven-release` environment secret `MAVEN_RELEASE_GITHUB_APP_PRIVATE_KEY`: a private key for the app The branch ruleset on `main` is intentionally **not** bypassed. The next-development snapshot bump lands as an ordinary pull request, so `main` keeps its "PR-only, squash, one approval" protection. @@ -95,8 +102,8 @@ The pipeline is ordered so failures are clean: - In `publish`, the bundle is uploaded as a droppable `VALIDATED` deployment first. If validation fails, nothing is published and the deployment can be dropped from the Central Portal. - The release tag is pushed next, while the deployment is still only `VALIDATED`. If the tag push fails (for example - the `restrict-tag-pushing` bypass for `github-actions[bot]` was not configured), nothing has been published yet -- - fix the cause and rerun. + the `restrict-tag-pushing` bypass for the Maven release GitHub App was not configured), nothing has been published + yet -- fix the cause and rerun. - The Central publish runs last. If the run fails at this step after the tag was already pushed, the deployment is still `VALIDATED` on the Central Portal: re-publish it from the Portal UI (the run logged its `deploymentId`), or drop it and rerun after deleting the tag.