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 45c3281f..1b5d0d10 100644 --- a/.github/workflows/maven_central_release.yml +++ b/.github/workflows/maven_central_release.yml @@ -1,46 +1,505 @@ 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: - 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 the release version with the patch bumped" + required: false + type: string permissions: contents: read - id-token: write 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: - release: + 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 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: 30 + 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 - ) - + 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." @@ -48,10 +507,35 @@ jobs: fi done - - name: Check out tag + - 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: ${{ github.ref }} + 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 @@ -60,17 +544,15 @@ jobs: java-version: "11" cache: maven - - name: Verify tag matches POM version - 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}." - 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 + - 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 @@ -88,12 +570,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}." @@ -103,7 +580,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" @@ -132,9 +609,109 @@ 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: | + # 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: | - mvn -B -ntp deploy -P maven-central-release -DskipTests + 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() @@ -142,3 +719,73 @@ 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: | + # 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 diff --git a/README.md b/README.md index 36a14a9d..ab127c6e 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 @@ -238,6 +238,24 @@ cd java-questdb-client mvn clean package -DskipTests ``` +## Releasing + +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 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. + +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 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..931079ec 100644 --- a/artifacts/release/README.md +++ b/artifacts/release/README.md @@ -1,109 +1,123 @@ # 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 and do not run `mvn deploy` locally during the normal release path. -## Edit release notes +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 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. -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. +## One-time setup -## Create a release branch +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). -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. +Store the app credentials for the workflow: -```bash -git fetch -git checkout main -git pull -git checkout -b release/1.2.2 -``` +- 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 -Make sure your working tree is clean. +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. -## Clear previous release "memory" +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). -```bash -mvn release:clean -``` +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. -Removes any `release.properties` and `*.releaseBackup` files left over from a previous attempt. +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. -## Roll versions and create the tag +## Prepare release notes -`release:prepare` will: +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)). -- 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 +## Publish -```bash -mvn -B release:prepare \ - -DautoVersionSubmodules=true \ - -DpushChanges=false \ - -DreleaseVersion=1.2.2 \ - -DdevelopmentVersion=1.2.3-SNAPSHOT \ - -Dtag=1.2.2 -``` +Start the `Release to Maven Central` workflow from the Actions tab with these inputs: -`-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. +- `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 -If `release:prepare` fails partway through: +The workflow runs as a pipeline: -```bash -mvn release:rollback -git tag -d 1.2.2 -``` +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 with the release version applied. The + 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. +5. **open-bump-pr** -- opens the next-development-version bump PR (post-release, see below). -`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). +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. -## Push the release branch and tag +## Versioning -```bash -git push origin release/1.2.2 -git push origin 1.2.2 -``` +In the normal path, leave both override inputs blank. The workflow derives: -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). +- release version from the current POM, for example `1.3.2-SNAPSHOT` -> `1.3.2` +- release tag equal to the release version +- next development version by bumping the patch, for example `1.3.3-SNAPSHOT` -## Publish to Maven Central +Use `release_version_override` and `next_development_version_override` only for non-standard releases (for example a +new minor or major line). -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: +## The snapshot-bump PR -- 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 +`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. -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). +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. -## Merge the release branch to `main` +## Failure handling -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. +The pipeline is ordered so failures are clean: -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. +- 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 next, while the deployment is still only `VALIDATED`. If the tag push fails (for example + 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. + +Once the Central Portal has accepted the deployment for publishing, the coordinate is immutable -- do not reuse the +version. ## 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. +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 67bfb492..035b9d7c 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -287,7 +287,41 @@ - maven-central-release + include-native-artifacts + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + copy-release-native-artifacts + process-resources + + copy-resources + + + true + ${project.build.outputDirectory} + + + ${project.build.directory}/native-libs + false + + io/questdb/client/bin/** + + + + + + + + + + + + release-artifacts @@ -350,6 +384,13 @@ + + + + + maven-central-publish + + org.sonatype.central central-publishing-maven-plugin @@ -357,7 +398,11 @@ true central - true + + false validated diff --git a/pom.xml b/pom.xml index 4449467f..ced15d96 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}