diff --git a/.github/workflows/maven_central_release.yml b/.github/workflows/maven_central_release.yml
new file mode 100644
index 00000000..174b018e
--- /dev/null
+++ b/.github/workflows/maven_central_release.yml
@@ -0,0 +1,142 @@
+name: Release to Maven Central
+
+on:
+ push:
+ tags:
+ - '[0-9]+.[0-9]+.[0-9]+'
+
+permissions:
+ contents: read
+ id-token: write
+
+concurrency:
+ group: maven-central-release
+ cancel-in-progress: false
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ environment: maven-release
+ timeout-minutes: 30
+ env:
+ 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
+ )
+
+ 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
+ )
+
+ 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:
+ ref: ${{ github.ref }}
+
+ - name: Set up Java 11
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
+ with:
+ distribution: temurin
+ 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: 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_GPG_PASSPHRASE
+ 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: |
+ mkdir -p "$HOME/.m2"
+ cat > "$HOME/.m2/settings.xml" <<'EOF'
+
+
+
+ central
+ ${env.MAVEN_CENTRAL_USERNAME}
+ ${env.MAVEN_CENTRAL_PASSWORD}
+
+
+ gpg.passphrase
+ ${env.MAVEN_GPG_PASSPHRASE}
+
+
+
+ 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: Publish release to Maven Central
+ run: |
+ mvn -B -ntp deploy -P maven-central-release -DskipTests
+
+ - name: Remove imported signing key
+ if: always()
+ run: |
+ if [[ -n "${GNUPGHOME:-}" && -d "${GNUPGHOME}" ]]; then
+ rm -rf "$GNUPGHOME"
+ fi
\ No newline at end of file
diff --git a/RELEASE.md b/RELEASE.md
deleted file mode 100644
index 38fa0356..00000000
--- a/RELEASE.md
+++ /dev/null
@@ -1,108 +0,0 @@
-# Release Guide
-
-This document describes how to release `org.questdb:client` to Maven Central.
-
-## Overview
-
-Releases are performed using the [Maven Release Plugin](https://maven.apache.org/maven-release/maven-release-plugin/) combined with the [Sonatype Central Publishing Plugin](https://central.sonatype.org/publish/publish-portal-maven/). The `maven-central-release` profile handles signing, Javadoc generation, source attachment, and publishing.
-
-## Prerequisites
-
-### 1. GPG Key
-
-A GPG key is required to sign the release artifacts. If you don't have one:
-
-```bash
-gpg --gen-key
-gpg --keyserver keyserver.ubuntu.com --send-keys
-```
-
-More details on GPG key generation can be found in the [Sonatype guide](https://central.sonatype.org/publish/requirements/gpg/).
-
-### 2. Sonatype Credentials
-
-You need credentials for the Sonatype Central Portal (https://central.sonatype.com/).
-
-### 3. Maven `settings.xml`
-
-Configure your `~/.m2/settings.xml` with the Sonatype server credentials:
-
-```xml
-
-
-
- central
- YOUR_SONATYPE_USERNAME
- YOUR_SONATYPE_PASSWORD
-
-
-
-```
-
-More details can be found in the [Sonatype guide](https://central.sonatype.org/publish/publish-portal-maven/).
-
-### 4. Repository Access
-
-You need push access to the `questdb/java-questdb-client` repository on GitHub.
-
-## Release Process
-
-### Step 1: Prepare the Release
-
-This bumps the version, creates a tag, and commits the changes:
-
-```bash
-mvn release:prepare
-```
-
-The plugin will prompt for:
-
-- **Release version** (e.g., `9.3.1`) — the version to release
-- **SCM tag** (e.g., `9.3.1`) — the Git tag name (uses the `tagNameFormat` of `@{project.version}`)
-- **Next development version** (e.g., `9.3.2-SNAPSHOT`) — the next snapshot version
-
-This creates two commits:
-
-1. `[maven-release-plugin] prepare release 9.3.1` — sets the release version
-2. `[maven-release-plugin] prepare for next development iteration` — sets the next snapshot version
-
-And a Git tag (e.g., `9.3.1`).
-
-### Step 2: Perform the Release
-
-This builds, signs, and publishes the artifacts to Maven Central:
-
-```bash
-mvn release:perform
-```
-
-The `maven-central-release` profile is activated automatically. It:
-
-- Compiles the source
-- Generates Javadoc
-- Attaches sources JAR
-- Signs all artifacts with GPG
-- Publishes to Maven Central via the Sonatype Central Publishing Plugin
-- Waits until the artifacts are published (`waitUntil=published`)
-
-### Step 3: Push Tags
-
-If not pushed automatically:
-
-```bash
-git push origin main --tags
-```
-
-## Post-Release
-
-### Verify on Maven Central
-
-Check that the new version appears on [Maven Central](https://central.sonatype.com/artifact/org.questdb/client). Propagation may take some time after publishing.
-
-### Create a GitHub Release
-
-1. Go to [GitHub Releases](https://github.com/questdb/java-questdb-client/releases).
-2. Click **Draft a new release**.
-3. Select the tag created by the release plugin.
-4. Add release notes describing the changes.
-5. Publish the release.
diff --git a/artifacts/release/README.md b/artifacts/release/README.md
new file mode 100644
index 00000000..918fab65
--- /dev/null
+++ b/artifacts/release/README.md
@@ -0,0 +1,109 @@
+# 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.
+
+**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.
+
+## Edit 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 release branch
+
+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.
+
+```bash
+git fetch
+git checkout main
+git pull
+git checkout -b release/1.2.2
+```
+
+Make sure your working tree is clean.
+
+## Clear previous release "memory"
+
+```bash
+mvn release:clean
+```
+
+Removes any `release.properties` and `*.releaseBackup` files left over from a previous attempt.
+
+## Roll versions and create the tag
+
+`release:prepare` will:
+
+- 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
+
+```bash
+mvn -B release:prepare \
+ -DautoVersionSubmodules=true \
+ -DpushChanges=false \
+ -DreleaseVersion=1.2.2 \
+ -DdevelopmentVersion=1.2.3-SNAPSHOT \
+ -Dtag=1.2.2
+```
+
+`-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.
+
+If `release:prepare` fails partway through:
+
+```bash
+mvn release:rollback
+git tag -d 1.2.2
+```
+
+`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).
+
+## Push the release branch and tag
+
+```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.
+
+## 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.
diff --git a/core/pom.xml b/core/pom.xml
index 7b9a2c20..addab542 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -61,8 +61,9 @@
- scm:git:https://github.com/questdb/java-questdb-client.git
+ https://github.com/questdb/java-questdb-client
scm:git:https://github.com/questdb/java-questdb-client.git
+ scm:git:https://github.com/questdb/java-questdb-client.git
HEAD
@@ -352,12 +353,12 @@
org.sonatype.central
central-publishing-maven-plugin
- 0.8.0
+ 0.9.0
true
central
true
- published
+ validated