Skip to content

Release to Maven Central #7

Release to Maven Central

Release to Maven Central #7

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 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,
# and optionally MAVEN_GPG_PASSPHRASE (omit/empty for a passphrase-less key).
on:
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 the release version with the patch bumped"
required: false
type: string
permissions:
contents: read
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:
resolve:
runs-on: ubuntu-latest
timeout-minutes: 15
outputs:
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
- name: Set up Java 11
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: temurin
java-version: "11"
cache: maven
- 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: |
set -euo pipefail
# Read the version of the published artifact (core/questdb-client), not the
# aggregator root -- core has its own <version> 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
fi
if [[ -n "${RELEASE_VERSION_OVERRIDE}" ]]; then
release_version="${RELEASE_VERSION_OVERRIDE}"
else
release_version="${pom_version%-SNAPSHOT}"
fi
if [[ "${release_version}" == *-SNAPSHOT ]]; then
echo "::error::Refusing to release a SNAPSHOT version (${release_version})."
exit 1
fi
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
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
# 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
# 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
source_sha="$(git rev-parse HEAD)"
{
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: resolve
strategy:
# 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
platform: darwin-aarch64
- os: macos-15-intel
platform: darwin-x86-64
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- name: Check out release source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ needs.resolve.outputs.source_sha }}
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
brew install make cmake gcc nasm
- name: Build native library
run: |
cd core
export MACOSX_DEPLOYMENT_TARGET=13.0
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"
- name: Upload native library
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: native-${{ matrix.platform }}
path: native-artifacts/${{ matrix.platform }}/libquestdb.dylib
if-no-files-found: error
build-linux-x86-64:
needs: resolve
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 source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
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"
- 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"
- 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: 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
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: native-linux-x86-64
path: native-artifacts/linux-x86-64/libquestdb.so
if-no-files-found: error
build-linux-aarch64:
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 source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ needs.resolve.outputs.source_sha }}
submodules: true
- name: Install tooling
run: |
yum update -y
yum install wget nasm zstd -y
- 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"
- 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: 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
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: native-linux-aarch64
path: native-artifacts/linux-aarch64/libquestdb.so
if-no-files-found: error
build-windows-x86-64:
needs: resolve
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- name: Check out release source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ needs.resolve.outputs.source_sha }}
submodules: true
- 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: |
# 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"
- 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
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: |
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
- 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: native-windows-x86-64
path: native-artifacts/windows-x86-64/libquestdb.dll
if-no-files-found: error
verify:
needs:
- 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: 45
permissions:
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 }}
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
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 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 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."
exit 1
fi
done
- name: Validate release GitHub App private key
env:
RAW_PRIVATE_KEY: ${{ secrets.MAVEN_RELEASE_GITHUB_APP_PRIVATE_KEY }}
run: |
private_key="${RAW_PRIVATE_KEY//$'\r'/}"
private_key="${private_key//\\n/$'\n'}"
if [[ "${private_key}" != *"-----BEGIN PRIVATE KEY-----"* && "${private_key}" != *"-----BEGIN RSA PRIVATE KEY-----"* ]]; then
echo "::error::MAVEN_RELEASE_GITHUB_APP_PRIVATE_KEY must be the GitHub App private key PEM, including BEGIN/END lines. Do not use the app client secret or webhook secret."
exit 1
fi
if ! printf '%s\n' "${private_key}" | openssl pkey -noout >/dev/null 2>&1; then
echo "::error::MAVEN_RELEASE_GITHUB_APP_PRIVATE_KEY is present but is not a parseable PEM private key."
exit 1
fi
- 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 }}
owner: ${{ github.repository_owner }}
repositories: ${{ github.event.repository.name }}
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: |
# 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
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: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1
with:
aws-region: ${{ env.MAVEN_RELEASE_AWS_REGION }}
role-to-assume: ${{ env.MAVEN_RELEASE_AWS_ROLE_ARN }}
role-session-name: java-questdb-client-release
- name: Fetch release credentials
uses: aws-actions/aws-secretsmanager-get-secrets@a9a7eb4e2f2871d30dc5b892576fde60a2ecc802 # v2.0.10
with:
secret-ids: |
,${{ env.MAVEN_RELEASE_AWS_SECRET_ARN }}
parse-json-secrets: true
- name: Validate release credentials
run: |
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}."
exit 1
fi
done
- name: Configure Maven settings.xml
run: |
if [[ -z "${MAVEN_GPG_PASSPHRASE:-}" ]]; then
echo "MAVEN_GPG_PASSPHRASE=" >> "$GITHUB_ENV"
fi
mkdir -p "$HOME/.m2"
cat > "$HOME/.m2/settings.xml" <<'EOF'
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>central</id>
<username>${env.MAVEN_CENTRAL_USERNAME}</username>
<password>${env.MAVEN_CENTRAL_PASSWORD}</password>
</server>
<server>
<id>gpg.passphrase</id>
<passphrase>${env.MAVEN_GPG_PASSPHRASE}</passphrase>
</server>
</servers>
</settings>
EOF
- name: Import release signing key
run: |
export GNUPGHOME="$(mktemp -d)"
chmod 700 "$GNUPGHOME"
printf '%s\n' "$MAVEN_GPG_PRIVATE_KEY" | gpg --batch --import
echo "GNUPGHOME=$GNUPGHOME" >> "$GITHUB_ENV"
- 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: |
# 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
# `|| 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
fi
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 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
# 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:
DEPLOYMENT_ID: ${{ steps.upload.outputs.deployment_id }}
run: |
token="$(printf '%s:%s' "$MAVEN_CENTRAL_USERNAME" "$MAVEN_CENTRAL_PASSWORD" | base64 | tr -d '\n')"
# 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.
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})."
# 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
# (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"' 2>/dev/null || true)"
[[ -n "${state}" ]] || state="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: Roll back release tag if nothing was published
# 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()
run: |
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: |
# 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"
git checkout -B "${branch}"
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}"
# 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
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