diff --git a/.github/workflows/maven_central_release.yml b/.github/workflows/maven_central_release.yml
index 45c3281f..09401d18 100644
--- a/.github/workflows/maven_central_release.yml
+++ b/.github/workflows/maven_central_release.yml
@@ -64,7 +64,7 @@ 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
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..ace57205 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