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