From cec8be1fc78b591dbcabe1ed03f86b8eb6c4e3e8 Mon Sep 17 00:00:00 2001 From: mwaisaida-13 Date: Mon, 28 Sep 2026 15:03:29 +0100 Subject: [PATCH 1/3] ci(java): add the Maven Central release workflow - pom.xml: add the developer and scm metadata Maven Central requires, and a release profile that signs every artifact and publishes through the Central Portal. Normal builds never sign. - java_dev.py: add verify (tag matches a non-SNAPSHOT pom version with a CHANGELOG entry) and smoke (polls Maven Central for the published files). - java-release.yml: on a java-v* release, test every supported Java version, then sign and publish from the maven environment and smoke-test Maven Central. --- .github/workflows/java-release.yml | 109 +++++++++++++++++++++++++++++ java/pom.xml | 55 +++++++++++++++ scripts/java_dev.py | 77 ++++++++++++++++++-- 3 files changed, 237 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/java-release.yml diff --git a/.github/workflows/java-release.yml b/.github/workflows/java-release.yml new file mode 100644 index 0000000..0c3ff51 --- /dev/null +++ b/.github/workflows/java-release.yml @@ -0,0 +1,109 @@ +name: Publish Java package to Maven Central + +# A java-v* release tests every supported Java version, then signs the artifacts and +# publishes them to Maven Central through the Central Portal, after approval. + +on: + release: + types: [published] + +permissions: + contents: read + +# cancel-in-progress: false — cancelling mid-upload can leave a deployment half done. +concurrency: + group: java-release-${{ github.ref }} + cancel-in-progress: false + +jobs: + # Source of truth for the test matrix: java.supported.versions in pom.xml. + # java-v* releases only — test/publish inherit this via `needs`. + versions: + name: java versions + if: startsWith(github.ref_name, 'java-v') + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + data: ${{ steps.set.outputs.data }} + steps: + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Load supported versions from pom.xml + id: set + run: echo "data=$(python3 scripts/java_dev.py list-supported-versions)" >> "$GITHUB_OUTPUT" + + # Full support matrix gates the release. Ubuntu only: the CI workflow already covers + # Windows and macOS. No dependency caching in release jobs. + test: + name: test (Java ${{ matrix.java-version }}) + needs: versions + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false # report every version, not just the first failure + matrix: + java-version: ${{ fromJson(needs.versions.outputs.data).all }} + defaults: + run: + working-directory: java + steps: + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Java ${{ matrix.java-version }} + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + distribution: corretto + java-version: ${{ matrix.java-version }} + + - name: Build and test + run: mvn --batch-mode --no-transfer-progress install + + - name: Run installed-artifact integration tests + run: mvn --batch-mode --no-transfer-progress --file integration-tests/pom.xml verify + + publish: + name: publish to Maven Central + needs: [versions, test] + # A fork that copies this workflow and its tags cannot publish. + if: startsWith(github.ref_name, 'java-v') && github.repository_owner == 'aws' + runs-on: ubuntu-latest + # Publishing can take up to 60 minutes and the smoke test up to ~45. + timeout-minutes: 120 + # Holds the Central token and signing key. Its required-reviewer gate is the + # approval to publish. + environment: maven + steps: + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + # Tag must match the pom version, which must not be a SNAPSHOT, and the + # CHANGELOG must have an entry. + - name: Verify the tag matches the pom version + run: python3 scripts/java_dev.py verify "$GITHUB_REF_NAME" + + # Writes a settings.xml server `central` that reads its credentials from the + # environment variables named below, and imports the signing key. + - name: Set up Java with the Central credentials and signing key + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + distribution: corretto + java-version: ${{ fromJson(needs.versions.outputs.data).max }} + server-id: central + server-username: CENTRAL_TOKEN_USERNAME + server-password: CENTRAL_TOKEN_PASSWORD + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + + # Tests already ran in the test job. The plugin uploads, waits for validation, + # publishes, and fails this step if any of that fails. + - name: Sign and publish to Maven Central + env: + CENTRAL_TOKEN_USERNAME: ${{ secrets.CENTRAL_TOKEN_USERNAME }} + CENTRAL_TOKEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN_PASSWORD }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: mvn --batch-mode --no-transfer-progress --file java/pom.xml -P release -DskipTests deploy + + # Retries for up to ~45 minutes: Maven Central lags behind a publish. + - name: Smoke-test Maven Central + run: python3 scripts/java_dev.py smoke "${GITHUB_REF_NAME#java-v}" diff --git a/java/pom.xml b/java/pom.xml index 353bf6d..a3ca33f 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -23,6 +23,17 @@ + + + Amazon Web Services + + + + + scm:git:https://github.com/aws/developer-toolkit-elasticache.git + https://github.com/aws/developer-toolkit-elasticache/tree/main/java + + UTF-8 8 @@ -178,4 +189,48 @@ + + + + + release + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.8 + + + sign-artifacts + verify + + sign + + + true + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.11.0 + true + + central + true + published + + 3600 + ${project.artifactId} ${project.version} + + + + + + diff --git a/scripts/java_dev.py b/scripts/java_dev.py index 1162329..2959c35 100644 --- a/scripts/java_dev.py +++ b/scripts/java_dev.py @@ -1,14 +1,36 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +# ruff: noqa: INP001, T201, S310, S314, PLR2004 # CLI: prints, parses our own pom, fixed https URLs import argparse +import http.client import json +import time +import urllib.error +import urllib.request import xml.etree.ElementTree as ET from pathlib import Path ROOT = Path(__file__).resolve().parents[1] POM = ROOT / "java" / "pom.xml" +CHANGELOG = ROOT / "java" / "CHANGELOG.md" MAVEN_NAMESPACE = {"m": "http://maven.apache.org/POM/4.0.0"} +TAG_PREFIX = "java-v" +CENTRAL = "https://repo1.maven.org/maven2" +# Maven Central usually serves a newly published release within 10-30 minutes. +SMOKE_ATTEMPTS = 45 +SMOKE_RETRY_SECONDS = 60 + + +def fail(message: str) -> None: + raise SystemExit(f"::error::{message}") + + +def pom_field(name: str) -> str: + value = ET.parse(POM).getroot().findtext(f"m:{name}", namespaces=MAVEN_NAMESPACE) + if not value: + fail(f"<{name}> is missing from java/pom.xml") + return value def list_supported_versions() -> None: @@ -25,20 +47,67 @@ def list_supported_versions() -> None: or not versions or not all(isinstance(version, str) and version for version in versions) ): - raise SystemExit( - "java.supported.versions must be a non-empty JSON string array" - ) + raise SystemExit("java.supported.versions must be a non-empty JSON string array") result = {"all": versions, "min": versions[0], "max": versions[-1]} print(json.dumps(result, separators=(",", ":"))) +def verify(tag: str) -> None: + version = pom_field("version") + if tag != f"{TAG_PREFIX}{version}": + fail(f"Release tag version does not match java/pom.xml version {version}.") + if version.endswith("-SNAPSHOT"): + fail(f"java/pom.xml version {version} is a SNAPSHOT; set the release version.") + if not CHANGELOG.is_file() or f"## [{version}]" not in CHANGELOG.read_text(): + fail(f"java/CHANGELOG.md has no [{version}] entry.") + print(f"Publishing version {version}.") + + +def smoke(version: str) -> None: + group = pom_field("groupId") + artifact = pom_field("artifactId") + base = ( + f"{CENTRAL}/{group.replace('.', '/')}/{artifact}/{version}/{artifact}-{version}" + ) + files = [".pom", ".jar", "-sources.jar", "-javadoc.jar", ".jar.asc"] + for attempt in range(1, SMOKE_ATTEMPTS + 1): + missing = [suffix for suffix in files if not exists(base + suffix)] + if not missing: + print(f"Smoke test passed for {group}:{artifact}:{version}.") + return + if attempt == SMOKE_ATTEMPTS: + fail(f"{group}:{artifact}:{version} is not on Maven Central: {missing}.") + print( + f"attempt {attempt}/{SMOKE_ATTEMPTS}: not on Maven Central yet " + f"({', '.join(missing)}); retrying in {SMOKE_RETRY_SECONDS}s", + flush=True, + ) + time.sleep(SMOKE_RETRY_SECONDS) + + +def exists(url: str) -> bool: + request = urllib.request.Request(url, method="HEAD") + try: + with urllib.request.urlopen(request, timeout=30) as response: + return response.status == 200 + except (OSError, http.client.HTTPException): + return False + + def main() -> None: parser = argparse.ArgumentParser() - parser.add_argument("command", choices=["list-supported-versions"]) + commands = parser.add_subparsers(dest="command", required=True) + commands.add_parser("list-supported-versions") + commands.add_parser("verify").add_argument("tag") + commands.add_parser("smoke").add_argument("version") args = parser.parse_args() if args.command == "list-supported-versions": list_supported_versions() + elif args.command == "verify": + verify(args.tag) + elif args.command == "smoke": + smoke(args.version) if __name__ == "__main__": From 4facd385e29cbda1e363cf92a7f56d5e1846f158 Mon Sep 17 00:00:00 2001 From: mwaisaida-13 Date: Mon, 28 Sep 2026 15:10:19 +0100 Subject: [PATCH 2/3] ci(java): check dependent pom versions and complete developer metadata verify now also requires java/examples/pom.xml and java/integration-tests/pom.xml to depend on the release version, since the examples are not built by the release workflow. The developers block uses the same fields as other AWS libraries on Maven Central. --- java/pom.xml | 5 +++++ scripts/java_dev.py | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/java/pom.xml b/java/pom.xml index a3ca33f..8406856 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -25,7 +25,12 @@ + amazonwebservices Amazon Web Services + https://aws.amazon.com + + developer + diff --git a/scripts/java_dev.py b/scripts/java_dev.py index 2959c35..2371747 100644 --- a/scripts/java_dev.py +++ b/scripts/java_dev.py @@ -14,6 +14,10 @@ ROOT = Path(__file__).resolve().parents[1] POM = ROOT / "java" / "pom.xml" CHANGELOG = ROOT / "java" / "CHANGELOG.md" +DEPENDENT_POMS = [ + ROOT / "java" / "examples" / "pom.xml", + ROOT / "java" / "integration-tests" / "pom.xml", +] MAVEN_NAMESPACE = {"m": "http://maven.apache.org/POM/4.0.0"} TAG_PREFIX = "java-v" CENTRAL = "https://repo1.maven.org/maven2" @@ -60,6 +64,18 @@ def verify(tag: str) -> None: fail(f"java/pom.xml version {version} is a SNAPSHOT; set the release version.") if not CHANGELOG.is_file() or f"## [{version}]" not in CHANGELOG.read_text(): fail(f"java/CHANGELOG.md has no [{version}] entry.") + # The examples and integration tests depend on the published artifact. + for pom in DEPENDENT_POMS: + depends_on = ( + ET.parse(pom) + .getroot() + .findtext( + f"m:dependencies/m:dependency[m:artifactId='{pom_field('artifactId')}']/m:version", + namespaces=MAVEN_NAMESPACE, + ) + ) + if depends_on != version: + fail(f"{pom.relative_to(ROOT)} depends on {depends_on}, not {version}.") print(f"Publishing version {version}.") From c5ad45188126199b306d633423cf23d4ae2ace85 Mon Sep 17 00:00:00 2001 From: mwaisaida-13 Date: Mon, 28 Sep 2026 17:43:08 +0100 Subject: [PATCH 3/3] ci(java): address review comments - setup-java's gpg-passphrase names MAVEN_GPG_PASSPHRASE explicitly, so the settings.xml it writes and maven-gpg-plugin read the same variable. - The smoke test requires the signature of every artifact, not just the jar. --- .github/workflows/java-release.yml | 3 ++- scripts/java_dev.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/java-release.yml b/.github/workflows/java-release.yml index 0c3ff51..195c2e3 100644 --- a/.github/workflows/java-release.yml +++ b/.github/workflows/java-release.yml @@ -83,7 +83,7 @@ jobs: - name: Verify the tag matches the pom version run: python3 scripts/java_dev.py verify "$GITHUB_REF_NAME" - # Writes a settings.xml server `central` that reads its credentials from the + # Writes a settings.xml whose `central` server and GPG passphrase read the # environment variables named below, and imports the signing key. - name: Set up Java with the Central credentials and signing key uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 @@ -94,6 +94,7 @@ jobs: server-username: CENTRAL_TOKEN_USERNAME server-password: CENTRAL_TOKEN_PASSWORD gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE # Tests already ran in the test job. The plugin uploads, waits for validation, # publishes, and fails this step if any of that fails. diff --git a/scripts/java_dev.py b/scripts/java_dev.py index 2371747..62587d5 100644 --- a/scripts/java_dev.py +++ b/scripts/java_dev.py @@ -85,7 +85,8 @@ def smoke(version: str) -> None: base = ( f"{CENTRAL}/{group.replace('.', '/')}/{artifact}/{version}/{artifact}-{version}" ) - files = [".pom", ".jar", "-sources.jar", "-javadoc.jar", ".jar.asc"] + artifacts = [".pom", ".jar", "-sources.jar", "-javadoc.jar"] + files = artifacts + [suffix + ".asc" for suffix in artifacts] for attempt in range(1, SMOKE_ATTEMPTS + 1): missing = [suffix for suffix in files if not exists(base + suffix)] if not missing: