diff --git a/Makefile b/Makefile
index b4f16f969..12e92428e 100644
--- a/Makefile
+++ b/Makefile
@@ -278,6 +278,11 @@ check/java/package: ## Build and verify publishable Maven archives without publi
check/java/package: build/java
go run ./java/bin/check-packages/main.go
+.PHONY: check/java/release
+check/java/release: ## Build a signed Maven Central bundle without uploading (requires an OpenPGP key)
+check/java/release: generate/fixtures verify/java-migrations
+ $(java_mvn) -f java/pom.xml -Prelease clean deploy -Driver.test.database=sqlite -DexcludedGroups=postgres -Dmaven.install.skip=true -Dcentral.skipUpload=true
+
.PHONY: check/js/dependencies
check/js/dependencies: ## Audit JavaScript advisories and production dependency licenses
pnpm -C js audit
diff --git a/java/CHANGELOG.md b/java/CHANGELOG.md
index d3f3fd72a..e8014dc07 100644
--- a/java/CHANGELOG.md
+++ b/java/CHANGELOG.md
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.1.0-alpha.1] - 2026-10-24
+
+### Added
+
+- First alpha release of River for Java. `com.riverqueue:river` provides a Java 21 client and worker runtime for Postgres and SQLite, sharing River's database schema and job protocol with other River clients. Includes typed jobs, virtual-thread workers, transactional inserts, unique, scheduled, periodic, and resumable jobs, queue management, hooks, middleware, events, and migrations.
+- Standalone migration CLI, published as `com.riverqueue:river-cli` with classifier `all`. The executable JAR bundles both JDBC drivers and supports applying, reverting, listing, and exporting migrations.
+
### Changed
- **Breaking:** Replaced `Extension` and `Client.withExtension` with operation-specific hook and middleware interfaces, matching River for Go: `HookInsertBegin`, `HookPeriodicJobsStart`, `HookWorkBegin`, `HookWorkEnd`, `JobInsertMiddleware`, and `WorkerMiddleware`. Register them with `Client.withPlugins` to activate all interfaces an instance implements, or with `withHooks` and `withMiddleware` to activate only that side. Plugins run before separately registered hooks and middleware. Each operation-specific interface supports lambdas and checked exceptions. The `afterClaim` callback was removed; use worker middleware for work before the handler. [PR #1519](https://github.com/riverqueue/river/pull/1519).
diff --git a/java/Makefile b/java/Makefile
index 66c39bc8c..c103031c3 100644
--- a/java/Makefile
+++ b/java/Makefile
@@ -1,4 +1,4 @@
-.PHONY: test test/conformance test/postgres test/sqlite test/tools lint lint/tools format package
+.PHONY: test test/conformance test/postgres test/sqlite test/tools lint lint/tools format package package/release
test:
$(MAKE) -C .. test/java
test/conformance:
@@ -17,3 +17,5 @@ format:
mvn spotless:apply
package:
$(MAKE) -C .. check/java/package
+package/release:
+ $(MAKE) -C .. check/java/release
diff --git a/java/README.md b/java/README.md
index 462f9054c..f31742519 100644
--- a/java/README.md
+++ b/java/README.md
@@ -6,13 +6,13 @@ retry, or work them using the same database.
## Installation
-Java 21 or newer is required. Add `com.riverqueue:river:0.48.0-alpha.1` and a JDBC driver to your application's dependencies. Jackson 3 is included transitively.
+Java 21 or newer is required. Add `com.riverqueue:river:0.1.0-alpha.1` and a JDBC driver to your application's dependencies. Jackson 3 is included transitively.
```xml
com.riverqueue
river
- 0.48.0-alpha.1
+ 0.1.0-alpha.1
org.postgresql
@@ -83,7 +83,7 @@ runs with Java 21 or newer. It needs no application classpath.
For a local build, see the [development guide](docs/development.md#building-from-source). Set `RIVER_CLI` to the executable JAR, then run:
```sh
-RIVER_CLI=/path/to/river-cli-0.48.0-alpha.1-all.jar
+RIVER_CLI=/path/to/river-cli-0.1.0-alpha.1-all.jar
export DATABASE_URL=postgres://localhost/myapp
# For SQLite: export DATABASE_URL=jdbc:sqlite:/absolute/path/myapp.db
@@ -94,7 +94,7 @@ java -jar "$RIVER_CLI" migrate-up
```
The Maven executable artifact is `com.riverqueue:river-cli` with version
-`0.48.0-alpha.1` and classifier `all`. Copy that single JAR into your deployment image to run
+`0.1.0-alpha.1` and classifier `all`. Copy that single JAR into your deployment image to run
migrations before starting workers. `--database-url URL` overrides `DATABASE_URL`.
`--schema background_jobs` selects a Postgres schema; an actual migration up
creates it if needed. Listing and dry runs do not create schemas or migration tables.
diff --git a/java/cli/pom.xml b/java/cli/pom.xml
index 3746eb427..57fdf6b21 100644
--- a/java/cli/pom.xml
+++ b/java/cli/pom.xml
@@ -1,11 +1,11 @@
4.0.0
- com.riverqueueriver-java0.48.0-alpha.1../pom.xml
- river-cli0.48.0-alpha.1
+ com.riverqueueriver-java0.1.0-alpha.1../pom.xml
+ river-cli0.1.0-alpha.1
River Java migration CLI
- Mozilla Public License 2.0https://mozilla.org/MPL/2.0/repo
+ Executable database migration tool for River on Postgres and SQLite
- com.riverqueueriver0.48.0-alpha.1
+ com.riverqueueriver0.1.0-alpha.1
org.postgresqlpostgresql
org.xerialsqlite-jdbc
org.junit.jupiterjunit-jupitertest
diff --git a/java/cli/src/test/java/com/riverqueue/cli/ExecutableIT.java b/java/cli/src/test/java/com/riverqueue/cli/ExecutableIT.java
index a4b0296bf..27465e02f 100644
--- a/java/cli/src/test/java/com/riverqueue/cli/ExecutableIT.java
+++ b/java/cli/src/test/java/com/riverqueue/cli/ExecutableIT.java
@@ -18,7 +18,7 @@ private String command(String... args) throws Exception {
List.of(
Path.of(System.getProperty("java.home"), "bin", "java").toString(),
"-jar",
- "target/river-cli-0.48.0-alpha.1-all.jar"));
+ "target/river-cli-0.1.0-alpha.1-all.jar"));
command.addAll(List.of(args));
var output = directory.resolve("process.log").toFile();
var builder = new ProcessBuilder(command).redirectErrorStream(true).redirectOutput(output);
@@ -36,7 +36,7 @@ private String command(String... args) throws Exception {
@Test
void bundledJarRunsWithoutExternalClasspath() throws Exception {
- assertTrue(command("--version").contains("0.48.0-alpha.1"));
+ assertTrue(command("--version").contains("0.1.0-alpha.1"));
assertTrue(command("migrate-up").contains("008 [up]"));
assertTrue(command("migrate-list").contains("applied"));
assertTrue(command("migrate-down", "--target-version", "0").contains("001 [down]"));
diff --git a/java/conformance/bin/build.py b/java/conformance/bin/build.py
index 5b980a06f..3d1b53aa5 100644
--- a/java/conformance/bin/build.py
+++ b/java/conformance/bin/build.py
@@ -13,9 +13,9 @@
with (artifacts.parent / "build.lock").open("w") as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
subprocess.run(["mvn", "-q", "-f", str(root / "pom.xml"), "package", "-DskipTests"], check=True)
- jar = root / "conformance/target/river-conformance-0.48.0-alpha.1.jar"
+ jar = root / "conformance/target/river-conformance-0.1.0-alpha.1.jar"
# A stale shaded jar can otherwise silently retain an older dependency's classes.
- with zipfile.ZipFile(root / "river/target/river-0.48.0-alpha.1.jar") as library, zipfile.ZipFile(jar) as bundled:
+ with zipfile.ZipFile(root / "river/target/river-0.1.0-alpha.1.jar") as library, zipfile.ZipFile(jar) as bundled:
for entry in library.namelist():
if entry.startswith("com/riverqueue/") and not entry.endswith("/"):
if library.read(entry) != bundled.read(entry):
diff --git a/java/conformance/bin/run.py b/java/conformance/bin/run.py
index 0fa9a33ca..ba71d1479 100644
--- a/java/conformance/bin/run.py
+++ b/java/conformance/bin/run.py
@@ -53,7 +53,7 @@ def main():
manifest_path = reference / "conformance/manifest.json"
manifest = json.loads(manifest_path.read_text())
manifest["implementations"]["java"] = {
- "package": "com.riverqueue:river", "registry": "maven", "version": "0.48.0-alpha.1"
+ "package": "com.riverqueue:river", "registry": "maven", "version": "0.1.0-alpha.1"
}
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n")
environment = os.environ | {
diff --git a/java/conformance/candidate.json b/java/conformance/candidate.json
index 11db53e39..700a3a931 100644
--- a/java/conformance/candidate.json
+++ b/java/conformance/candidate.json
@@ -14,7 +14,7 @@
"postgres-full-v1",
"sqlite-runtime-v1"
],
- "version": "0.48.0-alpha.1",
+ "version": "0.1.0-alpha.1",
"start_options": [
"elect_interval_ms",
"rescuer_interval_ms",
diff --git a/java/conformance/pom.xml b/java/conformance/pom.xml
index 1e300d437..b7d35fc46 100644
--- a/java/conformance/pom.xml
+++ b/java/conformance/pom.xml
@@ -1,9 +1,14 @@
4.0.0
- com.riverqueueriver-java0.48.0-alpha.1
+ com.riverqueueriver-java0.1.0-alpha.1
river-conformance
- truetrue
+
+ true
+ true
+ true
+ true
+
com.zaxxerHikariCP7.1.0
com.riverqueueriver${project.version}
diff --git a/java/conformance/src/main/java/com/riverqueue/conformance/Adapter.java b/java/conformance/src/main/java/com/riverqueue/conformance/Adapter.java
index 5a70cd7e8..21a76d53c 100644
--- a/java/conformance/src/main/java/com/riverqueue/conformance/Adapter.java
+++ b/java/conformance/src/main/java/com/riverqueue/conformance/Adapter.java
@@ -211,7 +211,7 @@ private JsonNode handle(String method, JsonNode params, String rawParams) throws
database.dialect() == Database.Dialect.POSTGRES ? "postgres" : "sqlite"),
Map.entry("capabilities", capabilities),
Map.entry("implementation", "java"),
- Map.entry("implementation_version", "0.48.0-alpha.1"),
+ Map.entry("implementation_version", "0.1.0-alpha.1"),
Map.entry("methods", methods),
Map.entry("migration_lines", Map.of("main", 8)),
Map.entry("profile", profile),
diff --git a/java/conformance/src/main/resources/manifest.json b/java/conformance/src/main/resources/manifest.json
index 99a4058e4..472e3bd89 100644
--- a/java/conformance/src/main/resources/manifest.json
+++ b/java/conformance/src/main/resources/manifest.json
@@ -50,7 +50,7 @@
"java": {
"package": "com.riverqueue:river",
"registry": "maven",
- "version": "0.48.0-alpha.1"
+ "version": "0.1.0-alpha.1"
}
},
"migration": {
diff --git a/java/docs/development.md b/java/docs/development.md
index eb8f58977..708c96149 100644
--- a/java/docs/development.md
+++ b/java/docs/development.md
@@ -13,7 +13,193 @@ make generate/fixtures
mvn --batch-mode --no-transfer-progress -f java/pom.xml install
```
-The executable migration CLI is built at `java/cli/target/river-cli-0.48.0-alpha.1-all.jar`. For a build without running tests, use `make build/java`.
+The executable migration CLI is built at `java/cli/target/river-cli-0.1.0-alpha.1-all.jar`. For a build without running tests, use `make build/java`.
+
+## Maven Central artifacts
+
+Normal builds attach sources and Javadoc JARs for the library and CLI. The
+`release` Maven profile additionally signs the artifacts and builds a Central
+Publisher Portal bundle with checksums. It includes three coordinates, all under
+`com.riverqueue` and using the version in `java/pom.xml`:
+
+| Artifact | Contents |
+| --- | --- |
+| `river-java` | Parent POM with shared dependencies and publication metadata |
+| `river` | Library, sources, Javadoc, and POM |
+| `river-cli` | Thin CLI, executable `all` JAR, sources, Javadoc, and POM |
+
+The parent POM must be published with its children so consumers can resolve their
+dependencies. `river-conformance` is explicitly excluded from Central publication
+and skips signing, Javadoc, and local installation.
+
+### Local tooling and signing
+
+Use JDK 21 or 25, Maven 3.9 or newer, and the Go version from `go.work` for fixture
+generation. Signing uses the Maven GPG plugin's pure Java Bouncy Castle signer
+(`signer=bc`) with agent access disabled. Neither GnuPG executables nor
+`gpg-agent` are needed. Maven downloads the pinned publishing, signing, and
+Javadoc plugins automatically; no separate Sonatype CLI is needed. Select the
+supported JDK with both `JAVA_HOME` and `PATH`, even if Homebrew's Maven
+installation brings a newer JDK.
+
+Supply an exported OpenPGP private key, such as an ASCII-armored file beginning
+with `-----BEGIN PGP PRIVATE KEY BLOCK-----`. SSH keys are not supported. For new
+keys without GnuPG, [PGPainless](https://pgpainless.readthedocs.io/en/stable/pgpainless-cli/usage.html)
+provides a Java/Bouncy Castle OpenPGP tool. For publication, distribute the public
+key to a [keyserver supported by Central](https://central.sonatype.org/publish/requirements/gpg/#distributing-your-public-key).
+The [keys.openpgp.org upload form](https://keys.openpgp.org/upload) accepts public
+key files directly in the browser.
+
+Use `-Dgpg.keyFilePath=/absolute/path/to/private-key.asc` for a local key file
+outside the checkout. Supply its passphrase through `MAVEN_GPG_PASSPHRASE`, using
+your secret manager or a hidden terminal prompt. For example, enter the passphrase
+at the `read` command, then build and test the signed bundle:
+
+```sh
+read -r -s MAVEN_GPG_PASSPHRASE
+export MAVEN_GPG_PASSPHRASE
+make check/java/release MVN='mvn -Dgpg.keyFilePath=/absolute/path/to/private-key.asc'
+unset MAVEN_GPG_PASSPHRASE
+```
+
+For CI, supply the private key's full contents through `MAVEN_GPG_KEY` and its
+passphrase through `MAVEN_GPG_PASSPHRASE`, then run `make check/java/release`.
+The environment key takes precedence over the key file. If a file or environment
+key contains multiple keys, select the signing key with
+`-Dgpg.keyFingerprint=FINGERPRINT` or `MAVEN_GPG_KEY_FINGERPRINT`.
+The plugin rejects passphrases supplied as Maven properties or in `settings.xml`.
+
+Maintainers can use separate signing keys and publishing tokens. Each maintainer
+supplies their own key locally; the project does not pin a shared key or
+fingerprint. Each public key must be available from a supported keyserver, and
+each publisher needs access to `com.riverqueue` in the Central Portal.
+
+From `java/`, the equivalent target is `make package/release`. This performs a
+clean build, runs SQLite and CLI tests and formatting checks, signs the parent and
+both public modules, and writes `java/target/central-publishing/central-bundle.zip`.
+It neither uploads the bundle nor installs these artifacts into the local Maven
+repository.
+No Central account or publishing token is required for this check. It does require
+an exported signing key and, for an encrypted key, its passphrase.
+
+### Publishing setup
+
+Before uploading, verify the `com.riverqueue` namespace in the
+[Central Publisher Portal](https://central.sonatype.com) using a DNS TXT record
+on `riverqueue.com`, and create a
+[publishing token](https://central.sonatype.org/publish/generate-portal-token/).
+Merge this server entry into your local `~/.m2/settings.xml`, with the token's
+username and password supplied through environment variables:
+
+```xml
+
+
+
+ central
+ ${env.CENTRAL_USERNAME}
+ ${env.CENTRAL_PASSWORD}
+
+
+
+```
+
+The profile defaults to `central.skipUpload=true`. With uploading enabled, Maven
+waits for Central validation; `autoPublish=false` leaves final publication to the
+Portal. Ordinary builds do not activate signing or Central publishing. See the
+[Central Maven plugin guide](https://central.sonatype.org/publish/publish-portal-maven/)
+for the upload and validation lifecycle.
+
+## Releasing a new version
+
+Run these commands from the repository root, in the same shell, with JDK 21 or 25,
+Maven, Go, `golangci-lint`, Python 3, and `gh` available. Complete the signing and
+publishing setup above first. Stop if any command fails. Java versions have no
+leading `v`; tags use `java/vX.Y.Z`.
+
+1. Start from a clean checkout, fetch changes and tags, and choose the release version:
+
+ ```sh
+ git checkout master && git pull --rebase
+ git fetch --tags
+ export VERSION=0.1.0-alpha.1
+ git checkout -b "$USER-java-$VERSION"
+ ```
+
+2. Update the POMs, CLI tests, documentation examples, and adapter version references together:
+
+ ```sh
+ python3 - <<'PY'
+ import json, os, pathlib, subprocess, xml.etree.ElementTree as ET
+ old = ET.parse("java/pom.xml").findtext("{http://maven.apache.org/POM/4.0.0}version")
+ files = subprocess.check_output([
+ "git", "grep", "-Il", "-F", old, "--", "java",
+ ":!java/CHANGELOG.md", ":!java/VALIDATION.md",
+ ":!java/conformance/src/main/resources/manifest.json",
+ ]).decode().splitlines()
+ for name in files:
+ path = pathlib.Path(name)
+ path.write_text(path.read_text().replace(old, os.environ["VERSION"]))
+ path = pathlib.Path("java/conformance/src/main/resources/manifest.json")
+ manifest = json.loads(path.read_text())
+ manifest["implementations"]["java"]["version"] = os.environ["VERSION"]
+ path.write_text(json.dumps(manifest, indent=2) + "\n")
+ PY
+ git diff -- java
+ ```
+
+ Move `java/CHANGELOG.md` entries from `Unreleased` into a `[$VERSION] - YYYY-MM-DD` section. For the first release, describe the initial library and CLI; for a stable release, remove "Prerelease" from the README introduction.
+
+3. Check the release and open a PR. Keep CI enabled and have the PR reviewed and merged after the Java checks pass:
+
+ ```sh
+ make lint/java test/java/sqlite check/java/package
+ git add java
+ git commit -m "[java] Release v$VERSION"
+ git push -u origin HEAD
+ gh pr create --fill
+ ```
+
+4. After merge, pull the release commit into a clean checkout, confirm its version, and create the local tag:
+
+ ```sh
+ git checkout master && git pull --rebase &&
+ test -z "$(git status --porcelain)" &&
+ test "$(python3 -c 'import xml.etree.ElementTree as E; print(E.parse("java/pom.xml").findtext("{http://maven.apache.org/POM/4.0.0}version"))')" = "$VERSION" &&
+ git tag "java/v$VERSION" -m "release java/v$VERSION"
+ ```
+
+5. Load your private key, then enter its passphrase, the Central token username, and the Central token password at the three `read` commands. Build, test, sign, and upload the release for validation:
+
+ ```sh
+ export MAVEN_GPG_KEY="$(cat /absolute/path/to/private-key.asc)"
+ read -r -s MAVEN_GPG_PASSPHRASE
+ read -r CENTRAL_USERNAME
+ read -r -s CENTRAL_PASSWORD
+ export MAVEN_GPG_PASSPHRASE CENTRAL_USERNAME CENTRAL_PASSWORD
+ test -z "$(git status --porcelain)" &&
+ test "$(git rev-parse HEAD)" = "$(git rev-parse "java/v$VERSION^{commit}")" &&
+ make generate/fixtures verify/java-migrations &&
+ ./java/bin/check-maven-warnings.sh mvn --batch-mode --no-transfer-progress \
+ -Dstyle.color=never -f java/pom.xml -Prelease clean deploy \
+ -Driver.test.database=sqlite -DexcludedGroups=postgres \
+ -Dmaven.install.skip=true -Dcentral.skipUpload=false
+ unset MAVEN_GPG_KEY MAVEN_GPG_PASSPHRASE CENTRAL_USERNAME CENTRAL_PASSWORD
+ ```
+
+6. In the [Central Portal deployments page](https://central.sonatype.com/publishing/deployments), open the validated deployment, confirm it contains `river-java`, `river`, and `river-cli` at `$VERSION`, and click **Publish**. Wait for **Published**. If uploading timed out, check this page before retrying.
+
+7. Verify the published CLI and its transitive library/parent dependencies using a fresh Maven cache. If Central has not propagated the release yet, wait and rerun with a new temporary directory:
+
+ ```sh
+ export JAVA_RELEASE_CHECK_DIR="$(mktemp -d)"
+ (cd "$JAVA_RELEASE_CHECK_DIR" && mvn --batch-mode --no-transfer-progress \
+ -Dmaven.repo.local="$JAVA_RELEASE_CHECK_DIR/m2" dependency:get \
+ -Dartifact="com.riverqueue:river-cli:$VERSION:jar:all") &&
+ java -jar "$JAVA_RELEASE_CHECK_DIR/m2/com/riverqueue/river-cli/$VERSION/river-cli-$VERSION-all.jar" --version &&
+ git push origin "java/v$VERSION"
+ ```
+
+8. Create a [GitHub release](https://github.com/riverqueue/river/releases/new) for `java/v$VERSION`, copy that version's Java changelog notes into the body, and attach the downloaded `river-cli-$VERSION-all.jar`. Mark alpha, beta, and RC versions as prereleases.
## Tests and checks
@@ -95,6 +281,12 @@ messages from packaging and other plugins. Local checks and CI use these same
targets. Direct Maven invocations also reject compiler warnings through the
parent POM's `failOnWarning` setting.
+Javadoc also fails on warnings. Its doclint configuration checks references,
+syntax, HTML, and accessibility, but permits missing comments and tags because
+the existing API documentation does not annotate every parameter and return value.
+Automatic external documentation links are disabled so Javadoc generation does
+not depend on another documentation site being reachable.
+
Executable JARs merge dependency `META-INF/LICENSE` and `META-INF/NOTICE` text,
retain service registrations, and generate their own executable manifests.
Dependency module descriptors and signatures are excluded from these combined
diff --git a/java/pom.xml b/java/pom.xml
index 94aef9a28..af0654302 100644
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -1,11 +1,25 @@
-
+
4.0.0
com.riverqueue
river-java
- 0.48.0-alpha.1
+ 0.1.0-alpha.1
pom
River for Java
+ Transactional jobs on Postgres and SQLite, interoperable with River clients in other languages
+ https://riverqueue.com
+ Mozilla Public License 2.0https://mozilla.org/MPL/2.0/repo
+
+ Riverqueue contributors
+ https://github.com/riverqueue/river/graphs/contributors
+ River
+ https://riverqueue.com
+
+
+ scm:git:https://github.com/riverqueue/river.git
+ scm:git:ssh://git@github.com/riverqueue/river.git
+ https://github.com/riverqueue/river
+
rivercliconformance
21
@@ -31,6 +45,49 @@
org.apache.maven.pluginsmaven-surefire-plugin3.5.4--enable-native-access=ALL-UNNAMED
org.apache.maven.pluginsmaven-source-plugin3.3.1jar-no-fork
+
+ org.apache.maven.pluginsmaven-javadoc-plugin3.12.0
+
+
+ all,-missing
+ true
+
+ false
+
+ javadocjar
+
com.diffplug.spotlessspotless-maven-plugin2.44.31.28.0verifycheck
+
+
+ release
+
+
+ true
+
+
+
+ org.apache.maven.pluginsmaven-gpg-plugin3.2.8
+
+ true
+ bc
+
+ false
+
+ signverifysign
+
+
+ org.sonatype.centralcentral-publishing-maven-plugin0.12.0
+ true
+
+ false
+ river-conformance
+ central
+ ${central.skipUpload}
+ validated
+
+
+
+
+
diff --git a/java/river/pom.xml b/java/river/pom.xml
index 5d7582502..43e195b4d 100644
--- a/java/river/pom.xml
+++ b/java/river/pom.xml
@@ -1,9 +1,9 @@
4.0.0
- com.riverqueueriver-java0.48.0-alpha.1
+ com.riverqueueriver-java0.1.0-alpha.1
river
+ River for Java library
Transactional jobs interoperable with River clients in other languages
- Mozilla Public License 2.0https://mozilla.org/MPL/2.0/repo
tools.jackson.corejackson-databind
org.postgresqlpostgresqltrue