From 5f65fdbbebea8cb66ed78e92fe9a672ef6778aa2 Mon Sep 17 00:00:00 2001 From: Nas Kavian Date: Mon, 24 Aug 2026 21:11:31 -0400 Subject: [PATCH] feat(release): publish Java SDK to Maven Central --- .github/workflows/ci.yml | 4 + .github/workflows/release.yml | 167 ++++++++++++++++++ DEVELOPMENT.md | 20 ++- README.md | 38 ++++ examples/pom.xml | 2 +- odp-agent/pom.xml | 2 +- odp-core/pom.xml | 2 +- odp-directory/pom.xml | 2 +- odp-service/pom.xml | 2 +- pom.xml | 69 +++++++- scripts/verify-consumer.sh | 45 +++++ testdata/consumer/pom.xml | 48 +++++ .../offeringprotocol/example/Consumer.java | 15 ++ tools/odp-conformance/pom.xml | 2 +- 14 files changed, 408 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100755 scripts/verify-consumer.sh create mode 100644 testdata/consumer/pom.xml create mode 100644 testdata/consumer/src/main/java/org/offeringprotocol/example/Consumer.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ed41b4..5ff45c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,10 @@ jobs: - run: ./mvnw --batch-mode --no-transfer-progress verify + - name: Verify clean consumer + if: matrix.java == '17' + run: ./scripts/verify-consumer.sh + - name: Run shared conformance harness run: ./scripts/run-conformance.sh env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1f74f59 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,167 @@ +name: Release + +on: + workflow_dispatch: + +concurrency: + group: odp-java-release + cancel-in-progress: false + +permissions: + attestations: write + contents: write + id-token: write + +jobs: + release: + if: github.repository == 'offering-protocol/odp-java' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Validate release + id: release + env: + GH_TOKEN: ${{ github.token }} + run: | + if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then + echo "Releases must run from main." >&2 + exit 1 + fi + version=$(./mvnw --batch-mode --no-transfer-progress help:evaluate -Dexpression=project.version -DforceStdout -q) + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "The project version must be a stable semantic version such as 0.1.0." >&2 + exit 1 + fi + tag="v$version" + if gh release view "$tag" >/dev/null 2>&1; then + echo "Release $tag already exists." >&2 + exit 1 + fi + if git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null 2>&1; then + tag_commit=$(git rev-list -n 1 "$tag") + if [[ "$tag_commit" != "$GITHUB_SHA" ]]; then + echo "Tag $tag does not identify the selected main commit." >&2 + exit 1 + fi + fi + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + artifact="https://repo1.maven.org/maven2/org/offeringprotocol/odp-core/$version/odp-core-$version.pom" + if curl --fail --silent --show-error --head "$artifact" >/dev/null; then + echo "published=true" >> "$GITHUB_OUTPUT" + else + echo "published=false" >> "$GITHUB_OUTPUT" + fi + + - name: Check out ODP specifications + uses: actions/checkout@v7 + with: + repository: offering-protocol/odp-specs + path: .conformance/odp-specs + + - name: Check out Node.js reference implementation + uses: actions/checkout@v7 + with: + repository: offering-protocol/odp-node + path: .conformance/odp-node + + - uses: actions/setup-java@v6 + with: + distribution: temurin + java-version: "17" + cache: maven + server-id: central + server-username-env-var: CENTRAL_USERNAME + server-password-env-var: CENTRAL_PASSWORD + + - name: Set up pnpm + uses: pnpm/action-setup@v6 + with: + version: 11.1.3 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + + - name: Install Node.js reference implementation + run: pnpm --dir .conformance/odp-node install --frozen-lockfile + + - name: Verify release + run: ./mvnw --batch-mode --no-transfer-progress verify + + - name: Run shared conformance harness + run: ./scripts/run-conformance.sh + env: + ODP_SPECS_DIR: .conformance/odp-specs + + - name: Run Node.js interoperability + run: ./scripts/run-node-interoperability.sh + env: + ODP_NODE_DIR: .conformance/odp-node + + - name: Verify clean consumer + run: ./scripts/verify-consumer.sh + + - name: Publish Maven artifacts + if: steps.release.outputs.published != 'true' + run: ./mvnw --batch-mode --no-transfer-progress -Prelease -Dgpg.signer=bc deploy + env: + CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} + CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} + MAVEN_GPG_KEY: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} + + - name: Attest Maven artifacts + uses: actions/attest-build-provenance@v4 + with: + subject-path: | + odp-core/target/odp-core-${{ steps.release.outputs.version }}*.jar + odp-directory/target/odp-directory-${{ steps.release.outputs.version }}*.jar + odp-agent/target/odp-agent-${{ steps.release.outputs.version }}*.jar + odp-service/target/odp-service-${{ steps.release.outputs.version }}*.jar + + - name: Wait for Maven Central availability + env: + VERSION: ${{ steps.release.outputs.version }} + run: | + artifact="https://repo1.maven.org/maven2/org/offeringprotocol/odp-core/$VERSION/odp-core-$VERSION.pom" + for attempt in {1..60}; do + if curl --fail --silent --show-error --head "$artifact" >/dev/null; then + exit 0 + fi + sleep 10 + done + echo "Maven Central did not expose $artifact within 10 minutes." >&2 + exit 1 + + - name: Verify Maven Central consumer + run: ./scripts/verify-consumer.sh + env: + ODP_CONSUMER_SOURCE: central + + - name: Create release tag + env: + TAG: ${{ steps.release.outputs.tag }} + run: | + if git rev-parse --verify "refs/tags/$TAG" >/dev/null 2>&1; then + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag --annotate "$TAG" --message "ODP Java $TAG" "$GITHUB_SHA" + git push origin "$TAG" + + - name: Publish GitHub release + run: >- + gh release create "${{ steps.release.outputs.tag }}" + .conformance/reports/agent.json#odp-java-agent-conformance.json + .conformance/reports/service.json#odp-java-service-conformance.json + --generate-notes + --title "ODP Java ${{ steps.release.outputs.tag }}" + --verify-tag + env: + GH_TOKEN: ${{ github.token }} diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 372d5cf..411fb5f 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -45,6 +45,20 @@ conformance behavior there before implementing or changing it in Java. ## Releases -The Maven reactor produces independently publishable artifacts under `org.offeringprotocol`. -Release publication requires sources, Javadocs, signatures, provenance, shared conformance, and -clean external-consumer verification. +All publishable modules share the parent reactor's stable semantic version. A release publishes the +`odp-java` parent and the `odp-core`, `odp-directory`, `odp-agent`, and `odp-service` artifacts to +Maven Central. Examples and conformance tooling remain repository-only modules. + +The Release workflow runs manually from `main`. It requires the `org.offeringprotocol` Maven Central +namespace and these repository secrets: + +- `CENTRAL_USERNAME`: Central Portal user-token username. +- `CENTRAL_PASSWORD`: Central Portal user-token password. +- `MAVEN_GPG_PRIVATE_KEY`: ASCII-armored private signing key. +- `MAVEN_GPG_PASSPHRASE`: signing-key passphrase. + +Before publication, the workflow requires a stable project version, the complete Maven gate, shared +Agent and Service conformance, Node.js interoperability, and isolated consumer compilation. It then +creates the matching `v` tag, publishes signed binary, source, Javadoc, and POM artifacts, +attests the Maven artifacts, verifies Maven Central consumption, and creates the GitHub release with +conformance evidence. diff --git a/README.md b/README.md index e232926..5014a41 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Offering Discovery Protocol for Java [![CI](https://github.com/offering-protocol/odp-java/actions/workflows/ci.yml/badge.svg)](https://github.com/offering-protocol/odp-java/actions/workflows/ci.yml) +[![Maven Central](https://img.shields.io/maven-central/v/org.offeringprotocol/odp-agent)](https://central.sonatype.com/namespace/org.offeringprotocol) [![Java](https://img.shields.io/badge/Java-17%2B-ED8B00?logo=openjdk&logoColor=white)](https://openjdk.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE) @@ -27,6 +28,37 @@ directory behavior. All artifacts use the Maven group `org.offeringprotocol` and require Java 17 or newer. +## Installation + +Applications should depend on the module matching their role. Maven resolves its required ODP +modules transitively. + +For an Agent application: + +```xml + + org.offeringprotocol + odp-agent + 0.1.0 + +``` + +For a Service integration: + +```xml + + org.offeringprotocol + odp-service + 0.1.0 + +``` + +Gradle applications use the same coordinates: + +```kotlin +implementation("org.offeringprotocol:odp-agent:0.1.0") +``` + ## Examples Run the small Service and Agent examples in separate terminals: @@ -48,6 +80,12 @@ The Maven Wrapper provides the complete merge gate: ./mvnw verify ``` +Verify the published module boundaries from an isolated consumer project with: + +```sh +./scripts/verify-consumer.sh +``` + Format Java sources with: ```sh diff --git a/examples/pom.xml b/examples/pom.xml index ca4aa80..d31d5f8 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -7,7 +7,7 @@ org.offeringprotocol odp-java - 0.1.0-SNAPSHOT + 0.1.0 odp-examples diff --git a/odp-agent/pom.xml b/odp-agent/pom.xml index 81f0e3b..cdb904f 100644 --- a/odp-agent/pom.xml +++ b/odp-agent/pom.xml @@ -7,7 +7,7 @@ org.offeringprotocol odp-java - 0.1.0-SNAPSHOT + 0.1.0 odp-agent diff --git a/odp-core/pom.xml b/odp-core/pom.xml index 925f596..bde7cb8 100644 --- a/odp-core/pom.xml +++ b/odp-core/pom.xml @@ -7,7 +7,7 @@ org.offeringprotocol odp-java - 0.1.0-SNAPSHOT + 0.1.0 odp-core diff --git a/odp-directory/pom.xml b/odp-directory/pom.xml index fcb80e4..50adbe0 100644 --- a/odp-directory/pom.xml +++ b/odp-directory/pom.xml @@ -7,7 +7,7 @@ org.offeringprotocol odp-java - 0.1.0-SNAPSHOT + 0.1.0 odp-directory diff --git a/odp-service/pom.xml b/odp-service/pom.xml index bc801f5..ddca995 100644 --- a/odp-service/pom.xml +++ b/odp-service/pom.xml @@ -7,7 +7,7 @@ org.offeringprotocol odp-java - 0.1.0-SNAPSHOT + 0.1.0 odp-service diff --git a/pom.xml b/pom.xml index 0e84989..337126c 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.offeringprotocol odp-java - 0.1.0-SNAPSHOT + 0.1.0 pom Offering Discovery Protocol for Java @@ -56,6 +56,10 @@ 3.0.7 0.8.15 3.6.3 + 0.11.0 + 3.2.8 + 3.12.0 + 3.4.0 2.97.0 3.6.0 @@ -271,4 +275,67 @@ + + + release + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.central + central-publishing-maven-plugin + ${central-publishing-maven-plugin.version} + true + + true + ODP Java ${project.version} + odp-conformance,odp-examples + central + published + + + + + + + diff --git a/scripts/verify-consumer.sh b/scripts/verify-consumer.sh new file mode 100755 index 0000000..122bc6d --- /dev/null +++ b/scripts/verify-consumer.sh @@ -0,0 +1,45 @@ +#!/bin/sh +set -eu + +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +repository=$(mktemp -d) +trap 'rm -rf "$repository"' EXIT INT TERM + +version=$( + "$root/mvnw" --batch-mode --no-transfer-progress \ + --file "$root/pom.xml" \ + help:evaluate \ + -Dexpression=project.version \ + -DforceStdout \ + -q +) + +if [ "${ODP_CONSUMER_SOURCE:-local}" = "local" ]; then + "$root/mvnw" --batch-mode --no-transfer-progress \ + --file "$root/pom.xml" \ + -DskipTests \ + package + + install_artifact() { + file=$1 + pom=$2 + "$root/mvnw" --batch-mode --no-transfer-progress \ + org.apache.maven.plugins:maven-install-plugin:3.1.4:install-file \ + -Dfile="$file" \ + -DpomFile="$pom" \ + -DlocalRepositoryPath="$repository" + } + + install_artifact "$root/pom.xml" "$root/pom.xml" + for artifact in odp-core odp-directory odp-agent odp-service; do + install_artifact \ + "$root/$artifact/target/$artifact-$version.jar" \ + "$root/$artifact/pom.xml" + done +fi + +"$root/mvnw" --batch-mode --no-transfer-progress \ + --file "$root/testdata/consumer/pom.xml" \ + -Dmaven.repo.local="$repository" \ + -Dodp.version="$version" \ + verify diff --git a/testdata/consumer/pom.xml b/testdata/consumer/pom.xml new file mode 100644 index 0000000..f0efc7f --- /dev/null +++ b/testdata/consumer/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.offeringprotocol.example + odp-java-consumer + 1.0.0 + + + 17 + UTF-8 + + + + + org.offeringprotocol + odp-core + ${odp.version} + + + org.offeringprotocol + odp-directory + ${odp.version} + + + org.offeringprotocol + odp-agent + ${odp.version} + + + org.offeringprotocol + odp-service + ${odp.version} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.15.0 + + + + diff --git a/testdata/consumer/src/main/java/org/offeringprotocol/example/Consumer.java b/testdata/consumer/src/main/java/org/offeringprotocol/example/Consumer.java new file mode 100644 index 0000000..98da8fe --- /dev/null +++ b/testdata/consumer/src/main/java/org/offeringprotocol/example/Consumer.java @@ -0,0 +1,15 @@ +package org.offeringprotocol.example; + +import java.util.List; +import org.offeringprotocol.odp.agent.OdpAgent; +import org.offeringprotocol.odp.core.Odp; +import org.offeringprotocol.odp.directory.DirectoryClient; +import org.offeringprotocol.odp.service.OdpService; + +public final class Consumer { + private Consumer() {} + + public static void main(String[] args) { + System.out.println(List.of(Odp.class, DirectoryClient.class, OdpAgent.class, OdpService.class)); + } +} diff --git a/tools/odp-conformance/pom.xml b/tools/odp-conformance/pom.xml index fd81f34..780d1dd 100644 --- a/tools/odp-conformance/pom.xml +++ b/tools/odp-conformance/pom.xml @@ -7,7 +7,7 @@ org.offeringprotocol odp-java - 0.1.0-SNAPSHOT + 0.1.0 ../../pom.xml