diff --git a/.github/workflows/java-publish-maven.yml b/.github/workflows/java-publish-maven.yml index d1ef78f66b..ff0b96f2b8 100644 --- a/.github/workflows/java-publish-maven.yml +++ b/.github/workflows/java-publish-maven.yml @@ -3,6 +3,16 @@ name: "Java Publish to Maven Central" env: HUSKY: 0 +# This workflow is a read-only consumer of the commit being released. It builds +# every native classifier and the primary Java SDK artifact from a single +# immutable source SHA, injects the release version with -Drevision=, and +# publishes to Maven Central. It never commits to, tags, or otherwise mutates +# the repository. The cross-language `vX.Y.Z` GitHub Release and the +# `java/vX.Y.Z` traceability tag are created by .github/workflows/publish.yml +# only after publication succeeds. +# Only validated commits from main's history may be published. +# Keep dependency caches isolated from these independently selected sources. + on: workflow_dispatch: inputs: @@ -10,8 +20,8 @@ on: description: "Release version (e.g., 1.0.0). If empty, derives from pom.xml by removing -SNAPSHOT" required: false type: string - developmentVersion: - description: "Next development version (e.g., 1.0.1-SNAPSHOT). If empty, increments patch version" + sourceSha: + description: "Full commit SHA from main's history. Defaults to the triggering commit; dispatch this workflow from main." required: false type: string prerelease: @@ -25,8 +35,8 @@ on: description: "Release version (e.g., 1.0.0). If empty, derives from pom.xml by removing -SNAPSHOT" required: false type: string - developmentVersion: - description: "Next development version (e.g., 1.0.1-SNAPSHOT). If empty, increments patch version" + sourceSha: + description: "Full commit SHA from main's history. Defaults to the triggering commit; dispatch this workflow from main." required: false type: string prerelease: @@ -38,11 +48,13 @@ on: mavenPublished: description: "Whether the Java package was published to Maven Central" value: ${{ jobs.deploy-maven.outputs.published }} + version: + description: "The published release version" + value: ${{ jobs.resolve-source.outputs.release_version }} + sourceSha: + description: "The immutable source commit that was published" + value: ${{ jobs.resolve-source.outputs.validated_source }} secrets: - JAVA_RELEASE_TOKEN: - required: true - JAVA_RELEASE_GITHUB_TOKEN: - required: true JAVA_MAVEN_CENTRAL_USERNAME: required: true JAVA_MAVEN_CENTRAL_PASSWORD: @@ -60,152 +72,86 @@ concurrency: cancel-in-progress: false jobs: - preflight: - name: Preflight checks - runs-on: ubuntu-latest - steps: - - name: Verify JAVA_RELEASE_TOKEN can push to repository - run: | - PUSH=$(gh api repos/${{ github.repository }} --jq '.permissions.push // false') - if [ "$PUSH" != "true" ]; then - echo "::error::JAVA_RELEASE_TOKEN lacks push permission on ${{ github.repository }}. It is required for pushing release commits and tags to main." - exit 1 - fi - echo "JAVA_RELEASE_TOKEN push access OK" - env: - GITHUB_TOKEN: ${{ secrets.JAVA_RELEASE_TOKEN }} - - prepare-release: - name: Prepare Java release - needs: preflight + resolve-source: + name: Resolve immutable release source runs-on: ubuntu-latest permissions: - contents: write + contents: read defaults: run: shell: bash working-directory: ./java outputs: + validated_source: ${{ steps.source.outputs.validated_source }} release_version: ${{ steps.versions.outputs.release_version }} - development_version: ${{ steps.versions.outputs.development_version }} - release_tag: ${{ steps.release-identity.outputs.release_tag }} - tag_commit: ${{ steps.release-identity.outputs.tag_commit }} - pre_prepare_commit: ${{ steps.update-docs.outputs.pre_prepare_commit }} - post_prepare_commit: ${{ steps.release-identity.outputs.post_prepare_commit }} - docs_commit: ${{ steps.update-docs.outputs.docs_commit_sha }} steps: + - name: Require a main-branch publication + working-directory: . + env: + WORKFLOW_REF: ${{ github.ref }} + run: | + if [ "$WORKFLOW_REF" != "refs/heads/main" ]; then + echo "::error::Java publication must be dispatched from main." + exit 1 + fi + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: main + ref: ${{ github.sha }} fetch-depth: 0 - token: ${{ secrets.JAVA_RELEASE_TOKEN }} + persist-credentials: false - - name: Configure Git for Maven Release - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: 22 + package-manager-cache: false - - uses: ./.github/actions/setup-copilot + - name: Validate and check out the release source + id: source + working-directory: . + env: + REQUESTED_SOURCE: ${{ inputs.sourceSha || github.sha }} + WORKFLOW_REF: ${{ github.ref }} + run: | + set -euo pipefail + VALIDATED_SOURCE=$(node java/scripts/resolve-release-source.mjs) + git checkout --detach "$VALIDATED_SOURCE" + echo "validated_source=$VALIDATED_SOURCE" >> "$GITHUB_OUTPUT" - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 with: java-version: "25" distribution: "microsoft" - cache: "maven" - - name: Determine versions + - name: Determine release version id: versions + env: + REQUESTED_VERSION: ${{ inputs.releaseVersion }} run: | - CURRENT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + CURRENT_VERSION=$(mvn -N help:evaluate -Dexpression=project.version -q -DforceStdout) echo "Current pom.xml version: $CURRENT_VERSION" - if [ -n "${{ inputs.releaseVersion }}" ]; then - RELEASE_VERSION="${{ inputs.releaseVersion }}" + if [ -n "$REQUESTED_VERSION" ]; then + RELEASE_VERSION="$REQUESTED_VERSION" else RELEASE_VERSION="${CURRENT_VERSION%-SNAPSHOT}" fi echo "Release version: $RELEASE_VERSION" - if [ -n "${{ inputs.developmentVersion }}" ]; then - DEV_VERSION="${{ inputs.developmentVersion }}" - if [[ "$DEV_VERSION" != *-SNAPSHOT ]]; then - echo "::error::developmentVersion '${DEV_VERSION}' must end with '-SNAPSHOT'." - exit 1 - fi - else - if ! echo "$RELEASE_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-(preview|(beta-)?java(-preview)?)\.[0-9]+)?$'; then - echo "Error: RELEASE_VERSION '$RELEASE_VERSION' is invalid." >&2 - exit 1 - fi - BASE_VERSION=$(echo "$RELEASE_VERSION" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+') - QUALIFIER=$(echo "$RELEASE_VERSION" | sed "s|^${BASE_VERSION}||") - IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE_VERSION" - DEV_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))${QUALIFIER}-SNAPSHOT" + if ! echo "$RELEASE_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-(preview|(beta-)?java(-preview)?)\.[0-9]+)?$'; then + echo "::error::RELEASE_VERSION '$RELEASE_VERSION' is invalid." + exit 1 + fi + if [[ "$RELEASE_VERSION" == *-SNAPSHOT ]]; then + echo "::error::RELEASE_VERSION '$RELEASE_VERSION' must not be a SNAPSHOT." + exit 1 fi echo "release_version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT" - echo "development_version=$DEV_VERSION" >> "$GITHUB_OUTPUT" - - - name: Update documentation with release version - id: update-docs - run: | - VERSION="${{ steps.versions.outputs.release_version }}" - DEV_VERSION="${{ steps.versions.outputs.development_version }}" - - for attempt in 1 2 3; do - git fetch origin main - git reset --hard origin/main - if [ -z "${{ inputs.releaseVersion }}" ]; then - LIVE_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) - if [ "${LIVE_VERSION%-SNAPSHOT}" != "$VERSION" ]; then - echo "::error::main advanced from release version $VERSION to ${LIVE_VERSION%-SNAPSHOT}; restart the release with the current version." - exit 1 - fi - fi - ./scripts/test-update-documentation-versions.sh - ./scripts/update-documentation-versions.sh "$VERSION" "$DEV_VERSION" README.md sdk/jbang-example.java - git add README.md sdk/jbang-example.java - git commit -m "docs: update version references to ${VERSION}" - - if git push origin HEAD:main; then - echo "pre_prepare_commit=$(git rev-parse HEAD^)" >> "$GITHUB_OUTPUT" - echo "docs_commit_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - break - fi - - if [ "$attempt" -eq 3 ]; then - echo "::error::main continued to advance while preparing the Java release." - exit 1 - fi - echo "main advanced during release preparation; retrying from the latest commit." - done - - - name: Prepare Release - run: | - mvn -B release:prepare \ - -DreleaseVersion=${{ steps.versions.outputs.release_version }} \ - -DdevelopmentVersion=${{ steps.versions.outputs.development_version }} \ - -DtagNameFormat=java/v@{project.version} \ - -DpushChanges=true \ - -Darguments="-DskipTests" - env: - MAVEN_USERNAME: ${{ secrets.JAVA_MAVEN_CENTRAL_USERNAME }} - MAVEN_PASSWORD: ${{ secrets.JAVA_MAVEN_CENTRAL_PASSWORD }} - JAVA_GPG_PASSPHRASE: ${{ secrets.JAVA_GPG_PASSPHRASE }} - - - name: Record immutable release identity - id: release-identity - run: | - TAG="java/v${{ steps.versions.outputs.release_version }}" - TAG_COMMIT=$(git rev-parse "${TAG}^{commit}") - POST_PREPARE_COMMIT=$(git rev-parse HEAD) - echo "release_tag=$TAG" >> "$GITHUB_OUTPUT" - echo "tag_commit=$TAG_COMMIT" >> "$GITHUB_OUTPUT" - echo "post_prepare_commit=$POST_PREPARE_COMMIT" >> "$GITHUB_OUTPUT" build-linux-arm64-classifier: name: Build Linux ARM64 native classifier - needs: prepare-release + needs: resolve-source runs-on: ubuntu-24.04-arm permissions: contents: read @@ -216,7 +162,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: ${{ needs.prepare-release.outputs.release_tag }} + ref: ${{ needs.resolve-source.outputs.validated_source }} fetch-depth: 1 persist-credentials: false @@ -224,23 +170,23 @@ jobs: with: java-version: "25" distribution: "microsoft" - cache: "maven" - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 22 + package-manager-cache: false - name: Build and validate linux-arm64 classifier run: | set -euo pipefail SOURCE_COMMIT=$(git rev-parse HEAD) - if [ "$SOURCE_COMMIT" != "${{ needs.prepare-release.outputs.tag_commit }}" ]; then - echo "::error::Checked out $SOURCE_COMMIT instead of the prepared tag commit." + if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.validated_source }}" ]; then + echo "::error::Checked out $SOURCE_COMMIT instead of the resolved release source." exit 1 fi + VERSION="${{ needs.resolve-source.outputs.release_version }}" node copilot-native/scripts/validate-native-host.mjs linux-arm64 - mvn -B -pl copilot-native package -DskipTests -Dcopilot.native.libc=glibc - VERSION="${{ needs.prepare-release.outputs.release_version }}" + mvn -B -pl copilot-native package -DskipTests -Drevision="$VERSION" -Dcopilot.native.libc=glibc JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-linux-arm64.jar" PRIMARY_JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION.jar" test -f "$JAR" @@ -257,14 +203,14 @@ jobs: with: name: java-native-linux-arm64-release-${{ github.run_id }}-${{ github.run_attempt }} path: | - java/copilot-native/target/copilot-sdk-java-runtime-${{ needs.prepare-release.outputs.release_version }}-linux-arm64.jar - java/copilot-native/target/linux-arm64-${{ needs.prepare-release.outputs.release_version }}.sha256 + java/copilot-native/target/copilot-sdk-java-runtime-${{ needs.resolve-source.outputs.release_version }}-linux-arm64.jar + java/copilot-native/target/linux-arm64-${{ needs.resolve-source.outputs.release_version }}.sha256 if-no-files-found: error retention-days: 1 build-windows-classifier: name: Build Windows native classifier - needs: prepare-release + needs: resolve-source runs-on: windows-latest permissions: contents: read @@ -275,7 +221,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: ${{ needs.prepare-release.outputs.release_tag }} + ref: ${{ needs.resolve-source.outputs.validated_source }} fetch-depth: 1 persist-credentials: false @@ -283,21 +229,21 @@ jobs: with: java-version: "25" distribution: "microsoft" - cache: "maven" - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 22 + package-manager-cache: false - name: Build and validate win32-x64 classifier run: | $sourceCommit = git rev-parse HEAD - if ($sourceCommit -ne '${{ needs.prepare-release.outputs.tag_commit }}') { - throw "Checked out $sourceCommit instead of the prepared tag commit." + if ($sourceCommit -ne '${{ needs.resolve-source.outputs.validated_source }}') { + throw "Checked out $sourceCommit instead of the resolved release source." } + $version = '${{ needs.resolve-source.outputs.release_version }}' node copilot-native/scripts/validate-native-host.mjs win32-x64 - mvn -B -pl copilot-native package -DskipTests - $version = '${{ needs.prepare-release.outputs.release_version }}' + mvn -B -pl copilot-native package -DskipTests "-Drevision=$version" $jar = "copilot-native/target/copilot-sdk-java-runtime-$version-win32-x64.jar" $primaryJar = "copilot-native/target/copilot-sdk-java-runtime-$version.jar" if (-not (Test-Path -LiteralPath $jar -PathType Leaf)) { @@ -314,14 +260,14 @@ jobs: with: name: java-native-win32-x64-release-${{ github.run_id }}-${{ github.run_attempt }} path: | - java/copilot-native/target/copilot-sdk-java-runtime-${{ needs.prepare-release.outputs.release_version }}-win32-x64.jar - java/copilot-native/target/win32-x64-${{ needs.prepare-release.outputs.release_version }}.sha256 + java/copilot-native/target/copilot-sdk-java-runtime-${{ needs.resolve-source.outputs.release_version }}-win32-x64.jar + java/copilot-native/target/win32-x64-${{ needs.resolve-source.outputs.release_version }}.sha256 if-no-files-found: error retention-days: 1 build-windows-arm64-classifier: name: Build Windows ARM64 native classifier - needs: prepare-release + needs: resolve-source runs-on: windows-11-arm permissions: contents: read @@ -332,7 +278,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: ${{ needs.prepare-release.outputs.release_tag }} + ref: ${{ needs.resolve-source.outputs.validated_source }} fetch-depth: 1 persist-credentials: false @@ -340,21 +286,21 @@ jobs: with: java-version: "25" distribution: "microsoft" - cache: "maven" - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 22 + package-manager-cache: false - name: Build and validate win32-arm64 classifier run: | $sourceCommit = git rev-parse HEAD - if ($sourceCommit -ne '${{ needs.prepare-release.outputs.tag_commit }}') { - throw "Checked out $sourceCommit instead of the prepared tag commit." + if ($sourceCommit -ne '${{ needs.resolve-source.outputs.validated_source }}') { + throw "Checked out $sourceCommit instead of the resolved release source." } + $version = '${{ needs.resolve-source.outputs.release_version }}' node copilot-native/scripts/validate-native-host.mjs win32-arm64 - mvn -B -pl copilot-native package -DskipTests - $version = '${{ needs.prepare-release.outputs.release_version }}' + mvn -B -pl copilot-native package -DskipTests "-Drevision=$version" $jar = "copilot-native/target/copilot-sdk-java-runtime-$version-win32-arm64.jar" $primaryJar = "copilot-native/target/copilot-sdk-java-runtime-$version.jar" if (-not (Test-Path -LiteralPath $jar -PathType Leaf)) { @@ -371,14 +317,14 @@ jobs: with: name: java-native-win32-arm64-release-${{ github.run_id }}-${{ github.run_attempt }} path: | - java/copilot-native/target/copilot-sdk-java-runtime-${{ needs.prepare-release.outputs.release_version }}-win32-arm64.jar - java/copilot-native/target/win32-arm64-${{ needs.prepare-release.outputs.release_version }}.sha256 + java/copilot-native/target/copilot-sdk-java-runtime-${{ needs.resolve-source.outputs.release_version }}-win32-arm64.jar + java/copilot-native/target/win32-arm64-${{ needs.resolve-source.outputs.release_version }}.sha256 if-no-files-found: error retention-days: 1 build-darwin-classifier: name: Build Darwin native classifier - needs: prepare-release + needs: resolve-source runs-on: macos-26 permissions: contents: read @@ -389,7 +335,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: ${{ needs.prepare-release.outputs.release_tag }} + ref: ${{ needs.resolve-source.outputs.validated_source }} fetch-depth: 1 persist-credentials: false @@ -397,23 +343,23 @@ jobs: with: java-version: "25" distribution: "microsoft" - cache: "maven" - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 22 + package-manager-cache: false - name: Build and validate darwin-arm64 classifier run: | set -euo pipefail SOURCE_COMMIT=$(git rev-parse HEAD) - if [ "$SOURCE_COMMIT" != "${{ needs.prepare-release.outputs.tag_commit }}" ]; then - echo "::error::Checked out $SOURCE_COMMIT instead of the prepared tag commit." + if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.validated_source }}" ]; then + echo "::error::Checked out $SOURCE_COMMIT instead of the resolved release source." exit 1 fi + VERSION="${{ needs.resolve-source.outputs.release_version }}" node copilot-native/scripts/validate-native-host.mjs darwin-arm64 - mvn -B -pl copilot-native package -DskipTests - VERSION="${{ needs.prepare-release.outputs.release_version }}" + mvn -B -pl copilot-native package -DskipTests -Drevision="$VERSION" JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-darwin-arm64.jar" PRIMARY_JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION.jar" test -f "$JAR" @@ -430,8 +376,8 @@ jobs: with: name: java-native-darwin-arm64-release-${{ github.run_id }}-${{ github.run_attempt }} path: | - java/copilot-native/target/copilot-sdk-java-runtime-${{ needs.prepare-release.outputs.release_version }}-darwin-arm64.jar - java/copilot-native/target/darwin-arm64-${{ needs.prepare-release.outputs.release_version }}.sha256 + java/copilot-native/target/copilot-sdk-java-runtime-${{ needs.resolve-source.outputs.release_version }}-darwin-arm64.jar + java/copilot-native/target/darwin-arm64-${{ needs.resolve-source.outputs.release_version }}.sha256 if-no-files-found: error retention-days: 1 @@ -439,7 +385,7 @@ jobs: name: Deploy Java release to Maven Central needs: [ - prepare-release, + resolve-source, build-linux-arm64-classifier, build-windows-classifier, build-windows-arm64-classifier, @@ -453,22 +399,19 @@ jobs: shell: bash working-directory: ./java outputs: - version: ${{ needs.prepare-release.outputs.release_version }} + version: ${{ needs.resolve-source.outputs.release_version }} published: ${{ steps.publish-maven.outcome == 'success' }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: ${{ needs.prepare-release.outputs.release_tag }} + ref: ${{ needs.resolve-source.outputs.validated_source }} fetch-depth: 1 persist-credentials: false - - uses: ./.github/actions/setup-copilot - - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 with: java-version: "25" distribution: "microsoft" - cache: "maven" server-id: central server-username: MAVEN_USERNAME server-password: MAVEN_PASSWORD @@ -478,6 +421,7 @@ jobs: - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 22 + package-manager-cache: false - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: @@ -503,11 +447,11 @@ jobs: id: linux-arm64-artifact run: | SOURCE_COMMIT=$(git rev-parse HEAD) - if [ "$SOURCE_COMMIT" != "${{ needs.prepare-release.outputs.tag_commit }}" ]; then - echo "::error::Checked out $SOURCE_COMMIT instead of the prepared tag commit." + if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.validated_source }}" ]; then + echo "::error::Checked out $SOURCE_COMMIT instead of the resolved release source." exit 1 fi - VERSION="${{ needs.prepare-release.outputs.release_version }}" + VERSION="${{ needs.resolve-source.outputs.release_version }}" ARTIFACT_DIRECTORY="${{ runner.temp }}/java-native-linux-arm64" JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-linux-arm64.jar" MANIFEST="$ARTIFACT_DIRECTORY/linux-arm64-$VERSION.sha256" @@ -524,11 +468,11 @@ jobs: id: windows-artifact run: | SOURCE_COMMIT=$(git rev-parse HEAD) - if [ "$SOURCE_COMMIT" != "${{ needs.prepare-release.outputs.tag_commit }}" ]; then - echo "::error::Checked out $SOURCE_COMMIT instead of the prepared tag commit." + if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.validated_source }}" ]; then + echo "::error::Checked out $SOURCE_COMMIT instead of the resolved release source." exit 1 fi - VERSION="${{ needs.prepare-release.outputs.release_version }}" + VERSION="${{ needs.resolve-source.outputs.release_version }}" ARTIFACT_DIRECTORY="${{ runner.temp }}/java-native-win32-x64" JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-win32-x64.jar" MANIFEST="$ARTIFACT_DIRECTORY/win32-x64-$VERSION.sha256" @@ -545,11 +489,11 @@ jobs: id: darwin-artifact run: | SOURCE_COMMIT=$(git rev-parse HEAD) - if [ "$SOURCE_COMMIT" != "${{ needs.prepare-release.outputs.tag_commit }}" ]; then - echo "::error::Checked out $SOURCE_COMMIT instead of the prepared tag commit." + if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.validated_source }}" ]; then + echo "::error::Checked out $SOURCE_COMMIT instead of the resolved release source." exit 1 fi - VERSION="${{ needs.prepare-release.outputs.release_version }}" + VERSION="${{ needs.resolve-source.outputs.release_version }}" ARTIFACT_DIRECTORY="${{ runner.temp }}/java-native-darwin-arm64" JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-darwin-arm64.jar" MANIFEST="$ARTIFACT_DIRECTORY/darwin-arm64-$VERSION.sha256" @@ -566,11 +510,11 @@ jobs: id: windows-arm64-artifact run: | SOURCE_COMMIT=$(git rev-parse HEAD) - if [ "$SOURCE_COMMIT" != "${{ needs.prepare-release.outputs.tag_commit }}" ]; then - echo "::error::Checked out $SOURCE_COMMIT instead of the prepared tag commit." + if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.validated_source }}" ]; then + echo "::error::Checked out $SOURCE_COMMIT instead of the resolved release source." exit 1 fi - VERSION="${{ needs.prepare-release.outputs.release_version }}" + VERSION="${{ needs.resolve-source.outputs.release_version }}" ARTIFACT_DIRECTORY="${{ runner.temp }}/java-native-win32-arm64" JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-win32-arm64.jar" MANIFEST="$ARTIFACT_DIRECTORY/win32-arm64-$VERSION.sha256" @@ -586,8 +530,8 @@ jobs: - name: Build Linux classifier and deploy complete release id: publish-maven run: | - VERSION="${{ needs.prepare-release.outputs.release_version }}" - mvn -B deploy -DskipTests -Prelease -Dcopilot.native.libc=glibc \ + VERSION="${{ needs.resolve-source.outputs.release_version }}" + mvn -B deploy -DskipTests -Prelease -Drevision="$VERSION" -Dcopilot.native.libc=glibc \ "-Dcopilot.native.external.linux.arm64.classifier.path=${{ steps.linux-arm64-artifact.outputs.linux_arm64_jar }}" \ "-Dcopilot.native.external.win32.classifier.path=${{ steps.windows-artifact.outputs.windows_jar }}" \ "-Dcopilot.native.external.win32.arm64.classifier.path=${{ steps.windows-arm64-artifact.outputs.windows_arm64_jar }}" \ @@ -597,9 +541,9 @@ jobs: node copilot-native/scripts/validate-native-artifact.mjs \ classifier linux-x64 "$LINUX_JAR" "$(basename "$LINUX_JAR")" .. LINUX_SHA=$(sha256sum "$LINUX_JAR" | cut -d ' ' -f 1) - GROUP_ID=$(mvn -q -pl copilot-native help:evaluate -Dexpression=project.groupId -DforceStdout) - ARTIFACT_ID=$(mvn -q -pl copilot-native help:evaluate -Dexpression=project.artifactId -DforceStdout) - POM_VERSION=$(mvn -q -pl copilot-native help:evaluate -Dexpression=project.version -DforceStdout) + GROUP_ID=$(mvn -q -pl copilot-native help:evaluate -Dexpression=project.groupId -Drevision="$VERSION" -DforceStdout) + ARTIFACT_ID=$(mvn -q -pl copilot-native help:evaluate -Dexpression=project.artifactId -Drevision="$VERSION" -DforceStdout) + POM_VERSION=$(mvn -q -pl copilot-native help:evaluate -Dexpression=project.version -Drevision="$VERSION" -DforceStdout) if [ -z "$GROUP_ID" ] || [ -z "$ARTIFACT_ID" ] || [ "$POM_VERSION" != "$VERSION" ]; then echo "::error::Unexpected copilot-native Maven coordinates: $GROUP_ID:$ARTIFACT_ID:$POM_VERSION (expected version $VERSION)" exit 1 @@ -607,9 +551,7 @@ jobs: { echo "### Maven Central Release" echo "- **Version:** $VERSION" - echo "- **Source tag:** \`${{ needs.prepare-release.outputs.release_tag }}\`" - echo "- **Source commit:** \`${{ needs.prepare-release.outputs.tag_commit }}\`" - echo "- **Next development version:** ${{ needs.prepare-release.outputs.development_version }}" + echo "- **Source commit:** \`${{ needs.resolve-source.outputs.validated_source }}\`" echo "- **Repository:** Maven Central" echo "" echo "#### Maven Coordinates" @@ -636,165 +578,3 @@ jobs: MAVEN_USERNAME: ${{ secrets.JAVA_MAVEN_CENTRAL_USERNAME }} MAVEN_PASSWORD: ${{ secrets.JAVA_MAVEN_CENTRAL_PASSWORD }} JAVA_GPG_PASSPHRASE: ${{ secrets.JAVA_GPG_PASSPHRASE }} - - rollback-release: - name: Roll back failed Java release preparation - needs: - [ - prepare-release, - build-linux-arm64-classifier, - build-windows-classifier, - build-windows-arm64-classifier, - build-darwin-classifier, - deploy-maven, - ] - if: ${{ failure() && needs.prepare-release.outputs.docs_commit != '' }} - runs-on: ubuntu-latest - permissions: - contents: write - defaults: - run: - shell: bash - working-directory: ./java - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - token: ${{ secrets.JAVA_RELEASE_TOKEN }} - - - name: Safely revert release commits and tag - env: - DOCS_COMMIT: ${{ needs.prepare-release.outputs.docs_commit }} - PREPARE_RESULT: ${{ needs.prepare-release.result }} - PRE_PREPARE_COMMIT: ${{ needs.prepare-release.outputs.pre_prepare_commit }} - POST_PREPARE_COMMIT: ${{ needs.prepare-release.outputs.post_prepare_commit }} - RELEASE_TAG: java/v${{ needs.prepare-release.outputs.release_version }} - TAG_COMMIT: ${{ needs.prepare-release.outputs.tag_commit }} - run: | - set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git fetch origin main --tags - MAIN_COMMIT=$(git rev-parse origin/main) - echo "Rollback inspection: main=$MAIN_COMMIT docs=$DOCS_COMMIT pre=$PRE_PREPARE_COMMIT post=$POST_PREPARE_COMMIT tag=$RELEASE_TAG" - - unsafe_rollback() { - echo "::error::Unsafe rollback state: $*" >&2 - exit 1 - } - - if ! git cat-file -e "${PRE_PREPARE_COMMIT}^{commit}"; then - unsafe_rollback "Recorded pre-prepare commit does not exist: $PRE_PREPARE_COMMIT" - fi - if ! git cat-file -e "${DOCS_COMMIT}^{commit}"; then - unsafe_rollback "Recorded documentation commit does not exist: $DOCS_COMMIT" - fi - if ! git merge-base --is-ancestor "$PRE_PREPARE_COMMIT" "$MAIN_COMMIT"; then - unsafe_rollback "Recorded pre-prepare commit is not an ancestor of main." - fi - if [ "$(git rev-parse "${DOCS_COMMIT}^")" != "$PRE_PREPARE_COMMIT" ]; then - unsafe_rollback "Documentation commit is not immediately based on the recorded pre-prepare commit." - fi - - mapfile -t ROLLBACK_COMMITS < <(git rev-list --reverse "$PRE_PREPARE_COMMIT..$MAIN_COMMIT") - FIRST_PARENT_COUNT=$(git rev-list --count --first-parent "$PRE_PREPARE_COMMIT..$MAIN_COMMIT") - if [ "${#ROLLBACK_COMMITS[@]}" -ne "$FIRST_PARENT_COUNT" ]; then - unsafe_rollback "Release range contains merged history." - fi - if [ "${#ROLLBACK_COMMITS[@]}" -lt 1 ] || [ "${#ROLLBACK_COMMITS[@]}" -gt 3 ]; then - unsafe_rollback "Expected one to three release-preparation commits after the recorded base; found ${#ROLLBACK_COMMITS[@]}." - fi - if [ "${ROLLBACK_COMMITS[0]}" != "$DOCS_COMMIT" ]; then - unsafe_rollback "The first commit after the recorded base is not the recorded documentation commit." - fi - - EXPECTED_PARENT="$PRE_PREPARE_COMMIT" - for COMMIT in "${ROLLBACK_COMMITS[@]}"; do - if git rev-parse -q --verify "${COMMIT}^2" >/dev/null; then - unsafe_rollback "Release range contains merge commit $COMMIT." - fi - if [ "$(git rev-parse "${COMMIT}^")" != "$EXPECTED_PARENT" ]; then - unsafe_rollback "Release range is not a linear continuation of the recorded base." - fi - EXPECTED_PARENT="$COMMIT" - done - - RELEASE_VERSION="${RELEASE_TAG#java/v}" - if [ "$(git log -1 --format=%s "$DOCS_COMMIT")" != "docs: update version references to $RELEASE_VERSION" ]; then - unsafe_rollback "Recorded documentation commit has an unexpected subject." - fi - - RELEASE_PREPARE_COMMIT="" - if [ "${#ROLLBACK_COMMITS[@]}" -ge 2 ]; then - RELEASE_PREPARE_COMMIT="${ROLLBACK_COMMITS[1]}" - if [ "$(git log -1 --format=%s "$RELEASE_PREPARE_COMMIT")" != "[maven-release-plugin] prepare release $RELEASE_TAG" ]; then - unsafe_rollback "Release-version commit has an unexpected subject." - fi - fi - if [ "${#ROLLBACK_COMMITS[@]}" -eq 3 ] && [ "$(git log -1 --format=%s "${ROLLBACK_COMMITS[2]}")" != "[maven-release-plugin] prepare for next development iteration" ]; then - unsafe_rollback "Development-version commit has an unexpected subject." - fi - - TAG_OBJECT=$(git ls-remote --refs origin "refs/tags/$RELEASE_TAG" | awk '{print $1}') - if [ -n "$TAG_OBJECT" ]; then - git fetch --no-tags origin "+refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG" - REMOTE_TAG_COMMIT=$(git rev-parse "${RELEASE_TAG}^{commit}") - if [ -z "$RELEASE_PREPARE_COMMIT" ] || [ "$REMOTE_TAG_COMMIT" != "$RELEASE_PREPARE_COMMIT" ]; then - unsafe_rollback "Release tag does not point to the guarded release-version commit." - fi - elif [ -n "$TAG_COMMIT" ]; then - unsafe_rollback "Recorded release tag is absent from the remote." - fi - - if [ "$PREPARE_RESULT" = "success" ]; then - if [ -z "$POST_PREPARE_COMMIT" ] || [ -z "$TAG_COMMIT" ]; then - unsafe_rollback "Successful preparation did not record its immutable release identity." - fi - if [ "$MAIN_COMMIT" != "$POST_PREPARE_COMMIT" ]; then - unsafe_rollback "main has advanced beyond the recorded post-prepare commit." - fi - if [ -z "$TAG_OBJECT" ] || [ "$REMOTE_TAG_COMMIT" != "$TAG_COMMIT" ]; then - unsafe_rollback "Remote release tag does not match the recorded tag commit." - fi - fi - - git checkout -B release-rollback "$MAIN_COMMIT" - git revert --no-edit "$PRE_PREPARE_COMMIT..$MAIN_COMMIT" - if [ -n "$TAG_OBJECT" ]; then - git push --atomic \ - "--force-with-lease=refs/heads/main:$MAIN_COMMIT" \ - "--force-with-lease=refs/tags/$RELEASE_TAG:$TAG_OBJECT" \ - origin HEAD:refs/heads/main ":refs/tags/$RELEASE_TAG" - else - git push \ - "--force-with-lease=refs/heads/main:$MAIN_COMMIT" \ - origin HEAD:refs/heads/main - fi - echo "Release preparation rollback completed after guarded history inspection." - - deploy-site: - name: Deploy Documentation Site - needs: [preflight, deploy-maven] - if: github.ref == 'refs/heads/main' && needs.deploy-maven.outputs.published == 'true' - runs-on: ubuntu-latest - steps: - - name: Trigger site deployment on standalone repo - run: | - VERSION="${{ needs.deploy-maven.outputs.version }}" - TAG="java/v${VERSION}" - PUBLISH_AS_LATEST=true - if [ "${{ inputs.prerelease }}" = "true" ]; then - PUBLISH_AS_LATEST=false - fi - echo "Triggering site deployment for version ${VERSION} (tag: ${TAG})" - gh workflow run deploy-site.yml \ - --repo github/copilot-sdk-java \ - -f version="${VERSION}" \ - -f publish_as_latest="${PUBLISH_AS_LATEST}" \ - -f monorepo_tag="${TAG}" - { - echo "### Site Deployment" - echo "Triggered deploy-site.yml on github/copilot-sdk-java for version ${VERSION}" - } >> "$GITHUB_STEP_SUMMARY" - env: - GITHUB_TOKEN: ${{ secrets.JAVA_RELEASE_GITHUB_TOKEN }} diff --git a/.github/workflows/java-sdk-tests.yml b/.github/workflows/java-sdk-tests.yml index 514f17c758..b1a3d4b240 100644 --- a/.github/workflows/java-sdk-tests.yml +++ b/.github/workflows/java-sdk-tests.yml @@ -8,6 +8,7 @@ permissions: contents: read env: + JAVA_PUBLICATION_REVISION: "0.0.0-ci" MAVEN_OPTS: >- -Daether.connector.http.retryHandler.count=3 -Daether.connector.http.retryHandler.serviceUnavailable=429,502,503 @@ -107,9 +108,9 @@ jobs: id: build run: | set -euo pipefail + VERSION="$JAVA_PUBLICATION_REVISION" node copilot-native/scripts/validate-native-host.mjs linux-arm64 - mvn -B -pl copilot-native package -DskipTests -Dcopilot.native.libc=glibc - VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + mvn -B -pl copilot-native package -DskipTests -Dcopilot.native.libc=glibc "-Drevision=$VERSION" JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-linux-arm64.jar" PRIMARY_JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION.jar" test -f "$JAR" @@ -165,9 +166,9 @@ jobs: - name: Build and validate win32-x64 classifier id: build run: | + $version = $env:JAVA_PUBLICATION_REVISION node copilot-native/scripts/validate-native-host.mjs win32-x64 - mvn -B -pl copilot-native package -DskipTests - $version = mvn help:evaluate "-Dexpression=project.version" -q "-DforceStdout" + mvn -B -pl copilot-native package -DskipTests "-Drevision=$version" $jar = "copilot-native/target/copilot-sdk-java-runtime-$version-win32-x64.jar" $primaryJar = "copilot-native/target/copilot-sdk-java-runtime-$version.jar" if (-not (Test-Path -LiteralPath $jar -PathType Leaf)) { @@ -223,9 +224,9 @@ jobs: - name: Build and validate win32-arm64 classifier id: build run: | + $version = $env:JAVA_PUBLICATION_REVISION node copilot-native/scripts/validate-native-host.mjs win32-arm64 - mvn -B -pl copilot-native package -DskipTests - $version = mvn help:evaluate "-Dexpression=project.version" -q "-DforceStdout" + mvn -B -pl copilot-native package -DskipTests "-Drevision=$version" $jar = "copilot-native/target/copilot-sdk-java-runtime-$version-win32-arm64.jar" $primaryJar = "copilot-native/target/copilot-sdk-java-runtime-$version.jar" if (-not (Test-Path -LiteralPath $jar -PathType Leaf)) { @@ -282,9 +283,9 @@ jobs: id: build run: | set -euo pipefail + VERSION="$JAVA_PUBLICATION_REVISION" node copilot-native/scripts/validate-native-host.mjs darwin-arm64 - mvn -B -pl copilot-native package -DskipTests - VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + mvn -B -pl copilot-native package -DskipTests "-Drevision=$VERSION" JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-darwin-arm64.jar" PRIMARY_JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION.jar" test -f "$JAR" @@ -360,7 +361,7 @@ jobs: name: java-native-publication-darwin-arm64-${{ github.run_id }} path: ${{ github.workspace }}/java/native-publication-input/darwin - - name: Verify native inputs and deploy the complete local release + - name: Verify native inputs and install the complete local release run: | set -euo pipefail test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-linux-arm64.outputs.source_sha }}" @@ -368,6 +369,7 @@ jobs: test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-windows-arm64.outputs.source_sha }}" test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-darwin.outputs.source_sha }}" VERSION="${{ needs.java-native-publication-windows.outputs.version }}" + test "$VERSION" = "$JAVA_PUBLICATION_REVISION" test "$VERSION" = "${{ needs.java-native-publication-linux-arm64.outputs.version }}" test "$VERSION" = "${{ needs.java-native-publication-windows-arm64.outputs.version }}" test "$VERSION" = "${{ needs.java-native-publication-darwin.outputs.version }}" @@ -407,10 +409,9 @@ jobs: --quick-generate-key 'Copilot SDK local validation ' rsa2048 sign 1d LOCAL_REPOSITORY="$GITHUB_WORKSPACE/java/copilot-native/target/local-publication-repository" rm -rf "$LOCAL_REPOSITORY" - mvn -B -pl copilot-native deploy -Prelease -DskipTests \ + mvn -B -pl copilot-native install -Prelease -DskipTests \ + "-Drevision=$VERSION" \ -Dcopilot.native.libc=glibc \ - -Dcopilot.native.test.local.publication=true \ - -DskipPublishing=true \ "-Dcopilot.native.external.linux.arm64.classifier.path=$LINUX_ARM64_JAR" \ "-Dcopilot.native.external.win32.classifier.path=$WINDOWS_JAR" \ "-Dcopilot.native.external.win32.arm64.classifier.path=$WINDOWS_ARM64_JAR" \ @@ -418,6 +419,11 @@ jobs: "-Dmaven.repo.local=$LOCAL_REPOSITORY" node copilot-native/scripts/validate-local-publication.mjs \ "$LOCAL_REPOSITORY" copilot-sdk-java-runtime "$VERSION" .. --signatures + mvn -B -pl sdk install -Prelease -DskipTests \ + "-Drevision=$VERSION" \ + "-Dmaven.repo.local=$LOCAL_REPOSITORY" + node copilot-native/scripts/validate-local-publication.mjs \ + "$LOCAL_REPOSITORY" copilot-sdk-java "$VERSION" .. --signatures java-sdk: name: "Java SDK Tests (JDK ${{ matrix.test-jdk }})" @@ -451,6 +457,10 @@ jobs: if: matrix.test-jdk == '25' run: ./scripts/test-update-documentation-versions.sh + - name: Test release source validation + if: matrix.test-jdk == '25' + run: node --test scripts/resolve-release-source.test.mjs + - name: Build SDK and set up test harness run: mvn test-compile jar:jar diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5e1d277259..54a50a5f8e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -383,11 +383,11 @@ jobs: if: github.event.inputs.dist-tag != 'unstable' && github.ref == 'refs/heads/main' needs: version permissions: - contents: write - id-token: write + contents: read uses: ./.github/workflows/java-publish-maven.yml with: releaseVersion: ${{ needs.version.outputs.version }} + sourceSha: ${{ github.sha }} prerelease: ${{ github.event.inputs.dist-tag == 'prerelease' }} secrets: inherit @@ -485,3 +485,58 @@ jobs: fi env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Tag Java SDK + # Reuse a tag only when it identifies the source that was published. + if: github.event.inputs.dist-tag == 'latest' || github.event.inputs.dist-tag == 'prerelease' + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git fetch --tags origin + TAG_NAME="java/v${VERSION}" + if git show-ref --verify --quiet "refs/tags/$TAG_NAME"; then + TAG_COMMIT=$(git rev-parse --verify "refs/tags/${TAG_NAME}^{commit}") + if [ "$TAG_COMMIT" != "$SOURCE_SHA" ]; then + echo "::error::Tag $TAG_NAME points to $TAG_COMMIT, expected $SOURCE_SHA. Refusing to overwrite it." + exit 1 + fi + echo "Tag $TAG_NAME already points to the release source, skipping tag push" + else + STATUS=$? + if [ "$STATUS" -ne 1 ]; then + echo "::error::Could not inspect tag $TAG_NAME." + exit "$STATUS" + fi + git tag "$TAG_NAME" "$SOURCE_SHA" + git push origin "refs/tags/$TAG_NAME" + echo "Created and pushed tag $TAG_NAME" + fi + env: + VERSION: ${{ needs.version.outputs.version }} + SOURCE_SHA: ${{ github.sha }} + + deploy-java-site: + name: Deploy Java documentation site + needs: [version, github-release] + runs-on: ubuntu-latest + permissions: {} + steps: + - name: Trigger Java documentation site deploy + # A failed dispatch can be retried without recreating the GitHub release. + run: | + set -euo pipefail + TAG="java/v${VERSION}" + PUBLISH_AS_LATEST=true + if [ "$DIST_TAG" = "prerelease" ]; then + PUBLISH_AS_LATEST=false + fi + echo "Triggering site deployment for version ${VERSION} (tag: ${TAG})" + gh workflow run deploy-site.yml \ + --repo github/copilot-sdk-java \ + -f version="${VERSION}" \ + -f publish_as_latest="${PUBLISH_AS_LATEST}" \ + -f monorepo_tag="${TAG}" + env: + VERSION: ${{ needs.version.outputs.version }} + DIST_TAG: ${{ github.event.inputs.dist-tag }} + GITHUB_TOKEN: ${{ secrets.JAVA_RELEASE_GITHUB_TOKEN }} diff --git a/docs/developer-docs/secrets.md b/docs/developer-docs/secrets.md index 573f4f22e1..7a6feaf423 100644 --- a/docs/developer-docs/secrets.md +++ b/docs/developer-docs/secrets.md @@ -33,7 +33,7 @@ These secrets power the GitHub Agentic Workflows (gh-aw) used for issue triage, ## Java publishing secrets -These secrets are used by the Java SDK Maven Central publishing workflow (`java-publish-maven.yml`) and the snapshot publishing workflow (`java-publish-snapshot.yml`). +These secrets support Java SDK Maven Central publishing, snapshot publishing, and post-release documentation deployment. * **`JAVA_MAVEN_CENTRAL_USERNAME`**: Username generated by a Maven Central Portal user token. * Workflows: `java-publish-maven.yml`, `java-publish-snapshot.yml` @@ -47,11 +47,8 @@ These secrets are used by the Java SDK Maven Central publishing workflow (`java- * **`JAVA_GPG_PASSPHRASE`**: Passphrase for the GPG signing key. * Workflows: `java-publish-maven.yml` -* **`JAVA_RELEASE_TOKEN`**: GitHub token with **push** permission on the repository. Used by the release workflow for `actions/checkout`, pushing release commits and tags to `main`, and running `mvn release:prepare -DpushChanges=true`. - * Workflows: `java-publish-maven.yml` - * **`JAVA_RELEASE_GITHUB_TOKEN`**: GitHub token with **workflow dispatch** (actions:write) permission on `github/copilot-sdk-java`. Used to trigger the documentation site deployment after a release is published. - * Workflows: `java-publish-maven.yml` + * Workflows: `publish.yml` ## Rust publishing secret diff --git a/java/README.md b/java/README.md index 9b8b243247..be1cdc315d 100644 --- a/java/README.md +++ b/java/README.md @@ -661,6 +661,32 @@ mvn clean package -pl copilot-native -DskipTests -Dcopilot.native.libc=glibc -Dc Each classifier JAR includes `runtime.node`, `platform.properties`, and `copilot-runtime` (or `copilot-runtime.exe`) under its `native/` directory. It does not contain the legacy `copilot` SEA. The placeholder JAR remains OS-neutral and contains no native binaries. Unsupported hosts retain the placeholder-only behavior. +### Versioning and releases + +The Java SDK uses [Maven CI-friendly versions](https://maven.apache.org/maven-ci-friendly.html). Every module declares `${revision}`, and the single source of truth is the `` property in `java/pom.xml`. The committed value stays a `-SNAPSHOT` (for example `1.0.14-SNAPSHOT`) and is only used for local development and the daily snapshot publish. + +Releasing is intentionally a **read-only** operation that never mutates the repository: + +- The release version is computed by the shared release pipeline (`.github/workflows/publish.yml`) — the same version used by every other language SDK — and injected at build time with `-Drevision=X.Y.Z`. The POM is **not** edited or committed. +- `.github/workflows/java-publish-maven.yml` builds every native classifier and the primary artifact from a single immutable source commit and publishes to Maven Central. It creates no commits, no branch-protection bypass, and requires no elevated repository token. +- The `java/vX.Y.Z` traceability tag and the cross-language `vX.Y.Z` GitHub Release are created by `publish.yml` **after** publication succeeds, pointing at the original release commit. + +For an independent Java publication retry, dispatch `java-publish-maven.yml` from `main` with the original `releaseVersion` and full `sourceSha`. The source must be a commit already in `main`'s history. Unmerged commits, branch names, and tag names are rejected before builds run. + +Because there is no `maven-release-plugin` and no `release:prepare` ceremony, the POM deliberately does not track the "next" release version. To validate a build with an explicit version locally, without publishing: + +```bash +# Build and verify with an explicit version, without touching the POM +mvn clean verify -Drevision=1.2.3 + +# Inspect the generated flattened POMs for the literal version (no ${revision}) +cat sdk/.flattened-pom.xml copilot-native/.flattened-pom.xml +``` + +These commands do not upload artifacts. Do not use `deploy` for local validation: the Central publishing plugin is configured with `autoPublish=true`. + +`flatten-maven-plugin` (ossrh mode) resolves `${revision}` into the installed and published POMs, so downstream consumers never see the unresolved property. Documentation version references are updated through a normal reviewed pull request (see `scripts/update-documentation-versions.sh`), not as a side effect of publishing. + ## License MIT — see [LICENSE](sdk/LICENSE) for details. diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index 7624deb4fe..c4d244365b 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.14-SNAPSHOT + ${revision} ../pom.xml @@ -884,30 +884,6 @@ - - - local-publication-validation - - - copilot.native.test.local.publication - true - - - - - - org.sonatype.central - central-publishing-maven-plugin - - true - - - - - /g, ""); + if (pom.includes("${revision}")) { + throw new Error( + `Published POM contains unresolved \${revision}: ${pomPath}`, + ); + } + if (/)/.test(pom)) { + throw new Error(`Published POM must not depend on a parent: ${pomPath}`); + } + + // Maven writes the flattened project's coordinates before nested elements. + // Match that header so dependency coordinates cannot satisfy this check. + const coordinates = pom.match( + /]*>\s*[^<]+<\/modelVersion>\s*([^<]+)<\/groupId>\s*([^<]+)<\/artifactId>\s*([^<]+)<\/version>/, + ); + if (!coordinates) { + throw new Error( + `Published POM is missing flattened project coordinates: ${pomPath}`, + ); + } + const [, groupId, publishedArtifactId, publishedVersion] = coordinates.map( + (value) => value.trim(), + ); + if ( + groupId !== "com.github" || + publishedArtifactId !== artifactId || + publishedVersion !== version + ) { + throw new Error( + `Unexpected Maven coordinates in ${pomPath}: ${groupId}:${publishedArtifactId}:${publishedVersion} (expected com.github:${artifactId}:${version})`, + ); + } +} + function main() { const [repositoryPath, artifactId, version, repoRoot, signatures] = process.argv.slice(2); diff --git a/java/copilot-native/scripts/validate-native-artifact.test.mjs b/java/copilot-native/scripts/validate-native-artifact.test.mjs index c07695a484..2986d709d8 100644 --- a/java/copilot-native/scripts/validate-native-artifact.test.mjs +++ b/java/copilot-native/scripts/validate-native-artifact.test.mjs @@ -341,122 +341,101 @@ test("accepts a matching SHA-256 manifest", (t) => { ); }); -test("validates one complete signed local publication", (t) => { - const fixture = createFixture(t); - const artifactId = "copilot-sdk-java-runtime"; - const version = "1.2.3"; - const publicationDirectory = path.join( - fixture.root, - "repository", - "com", - "github", - artifactId, - version, - ); - fs.mkdirSync(publicationDirectory, { recursive: true }); - const primaryJar = `${artifactId}-${version}.jar`; - const linuxJar = `${artifactId}-${version}-linux-x64.jar`; - const linuxArm64Jar = `${artifactId}-${version}-linux-arm64.jar`; - const windowsJar = `${artifactId}-${version}-win32-x64.jar`; - const windowsArm64Jar = `${artifactId}-${version}-win32-arm64.jar`; - const darwinJar = `${artifactId}-${version}-darwin-arm64.jar`; - writeStoredZip(path.join(publicationDirectory, primaryJar), [ - ["META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n"], - ]); - writeStoredZip( - path.join(publicationDirectory, `${artifactId}-${version}-sources.jar`), - [], - ); - writeStoredZip( - path.join(publicationDirectory, `${artifactId}-${version}-javadoc.jar`), - [], - ); - createNativeClassifierTestFixture({ - classifier: "linux-x64", - outputPath: path.join(publicationDirectory, linuxJar), - repoRoot: fixture.repoRoot, - }); - createNativeClassifierTestFixture({ - classifier: "linux-arm64", - outputPath: path.join(publicationDirectory, linuxArm64Jar), - repoRoot: fixture.repoRoot, - }); - createNativeClassifierTestFixture({ - classifier, - outputPath: path.join(publicationDirectory, windowsJar), - repoRoot: fixture.repoRoot, - }); - createNativeClassifierTestFixture({ - classifier: "win32-arm64", - outputPath: path.join(publicationDirectory, windowsArm64Jar), - repoRoot: fixture.repoRoot, - }); - createNativeClassifierTestFixture({ - classifier: "darwin-arm64", - outputPath: path.join(publicationDirectory, darwinJar), - repoRoot: fixture.repoRoot, - }); - fs.writeFileSync( - path.join(publicationDirectory, `${artifactId}-${version}.pom`), - "", - ); - for (const artifact of [ - primaryJar, - `${artifactId}-${version}.pom`, - `${artifactId}-${version}-sources.jar`, - `${artifactId}-${version}-javadoc.jar`, - linuxJar, - linuxArm64Jar, - windowsJar, - windowsArm64Jar, - darwinJar, +for (const artifactId of ["copilot-sdk-java-runtime", "copilot-sdk-java"]) { + for (const version of ["0.0.0-ci", "1.2.3-SNAPSHOT"]) { + test(`validates signed ${artifactId} publication at ${version}`, (t) => { + const fixture = createPublicationFixture(t, { artifactId, version }); + + assert.equal( + validateLocalPublication({ ...fixture, requireSignatures: true }), + fixture.publicationDirectory, + ); + }); + } + + for (const { name, from, to, error } of [ + { + name: "unresolved revision", + from: "1.2.3", + to: "${revision}", + error: /unresolved.*revision/, + }, + { + name: "the committed snapshot instead of the release version", + from: "1.2.3", + to: "1.2.3-SNAPSHOT", + error: /Unexpected Maven coordinates/, + }, + { + name: "an incorrect group", + from: "com.github", + to: "org.example", + error: /Unexpected Maven coordinates/, + }, + { + name: "an incorrect artifact", + from: `${artifactId}`, + to: "different-artifact", + error: /Unexpected Maven coordinates/, + }, + { + name: "a dependency version in place of the project version", + from: "1.2.3", + to: "1.2.3", + error: /missing flattened project coordinates/, + }, + { + name: "a commented-out project version", + from: "1.2.3", + to: "", + error: /missing flattened project coordinates/, + }, + { + name: "an unpublished parent reference", + from: "4.0.0", + to: `4.0.0 + + com.github + copilot-sdk-java-parent + 1.2.3 + `, + error: /must not depend on a parent/, + }, + { + name: "unresolved revision in a dependency", + from: "", + to: "${revision}", + error: /unresolved.*revision/, + }, ]) { - fs.writeFileSync( - path.join(publicationDirectory, `${artifact}.asc`), - "signature", - ); + test(`${artifactId} publication rejects ${name}`, (t) => { + const fixture = createPublicationFixture(t, { artifactId }); + const pom = fs.readFileSync(fixture.pomPath, "utf8"); + assert.ok(pom.includes(from)); + fs.writeFileSync(fixture.pomPath, pom.replace(from, to)); + + assert.throws(() => validateLocalPublication(fixture), error); + }); } +} - assert.equal( - validateLocalPublication({ - artifactId, - repositoryPath: path.join(fixture.root, "repository"), - repoRoot: fixture.repoRoot, - requireSignatures: true, - version, - }), - publicationDirectory, +test("local release publication requires a POM signature", (t) => { + const fixture = createPublicationFixture(t); + fs.rmSync(`${fixture.pomPath}.asc`); + + assert.throws( + () => validateLocalPublication({ ...fixture, requireSignatures: true }), + /missing signature.*\.pom\.asc/, ); }); test("local publication validation rejects cross-classifier contamination", (t) => { - const fixture = createFixture(t); - const artifactId = "copilot-sdk-java-runtime"; - const version = "1.2.3"; - const publicationDirectory = path.join( - fixture.root, - "repository", - "com", - "github", - artifactId, - version, - ); - fs.mkdirSync(publicationDirectory, { recursive: true }); - - writeStoredZip( - path.join(publicationDirectory, `${artifactId}-${version}.jar`), - [["META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n"]], - ); + const fixture = createPublicationFixture(t); writeStoredZip( - path.join(publicationDirectory, `${artifactId}-${version}-sources.jar`), - [], - ); - writeStoredZip( - path.join(publicationDirectory, `${artifactId}-${version}-javadoc.jar`), - [], - ); - writeStoredZip( - path.join(publicationDirectory, `${artifactId}-${version}-linux-x64.jar`), + path.join( + fixture.publicationDirectory, + `${fixture.artifactId}-${fixture.version}-linux-x64.jar`, + ), [ ["native/linux-x64/runtime.node", "runtime"], ["native/linux-x64/copilot-runtime", "runtime wrapper"], @@ -467,55 +446,78 @@ test("local publication validation rejects cross-classifier contamination", (t) ["native/win32-x64/runtime.node", "wrong platform"], ], ); - createNativeClassifierTestFixture({ - classifier: "linux-arm64", - outputPath: path.join( - publicationDirectory, - `${artifactId}-${version}-linux-arm64.jar`, - ), - repoRoot: fixture.repoRoot, - }); - createNativeClassifierTestFixture({ - classifier: "win32-x64", - outputPath: path.join( - publicationDirectory, - `${artifactId}-${version}-win32-x64.jar`, - ), - repoRoot: fixture.repoRoot, - }); - createNativeClassifierTestFixture({ - classifier: "win32-arm64", - outputPath: path.join( - publicationDirectory, - `${artifactId}-${version}-win32-arm64.jar`, - ), - repoRoot: fixture.repoRoot, - }); - createNativeClassifierTestFixture({ - classifier: "darwin-arm64", - outputPath: path.join( - publicationDirectory, - `${artifactId}-${version}-darwin-arm64.jar`, - ), - repoRoot: fixture.repoRoot, - }); + + assert.throws(() => validateLocalPublication(fixture), /must not contain/); +}); + +function createPublicationFixture( + t, + { artifactId = "copilot-sdk-java-runtime", version = "1.2.3" } = {}, +) { + const fixture = createFixture(t); + const repositoryPath = path.join(fixture.root, "repository"); + const publicationDirectory = path.join( + repositoryPath, + "com", + "github", + artifactId, + version, + ); + fs.mkdirSync(publicationDirectory, { recursive: true }); + const pomPath = path.join( + publicationDirectory, + `${artifactId}-${version}.pom`, + ); fs.writeFileSync( - path.join(publicationDirectory, `${artifactId}-${version}.pom`), - "", + pomPath, + ` + + 4.0.0 + com.github + ${artifactId} + ${version} +`, ); - - assert.throws( - () => - validateLocalPublication({ - artifactId, - repositoryPath: path.join(fixture.root, "repository"), + for (const suffix of ["", "-sources", "-javadoc"]) { + writeStoredZip( + path.join(publicationDirectory, `${artifactId}-${version}${suffix}.jar`), + [["META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n"]], + ); + } + if (artifactId === "copilot-sdk-java-runtime") { + for (const nativeClassifier of [ + "linux-x64", + "linux-arm64", + "win32-x64", + "win32-arm64", + "darwin-arm64", + ]) { + createNativeClassifierTestFixture({ + classifier: nativeClassifier, + outputPath: path.join( + publicationDirectory, + `${artifactId}-${version}-${nativeClassifier}.jar`, + ), repoRoot: fixture.repoRoot, - requireSignatures: false, - version, - }), - /must not contain/, - ); -}); + }); + } + } + for (const artifact of fs.readdirSync(publicationDirectory)) { + fs.writeFileSync( + path.join(publicationDirectory, `${artifact}.asc`), + "signature", + ); + } + + return { + ...fixture, + artifactId, + version, + repositoryPath, + publicationDirectory, + pomPath, + }; +} function createFixture(t) { const fixtureParent = path.join( diff --git a/java/pom.xml b/java/pom.xml index b17cdbde7d..2fd5b562f2 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -7,7 +7,7 @@ com.github copilot-sdk-java-parent - 1.0.14-SNAPSHOT + ${revision} pom GitHub Copilot SDK :: Java :: Parent @@ -47,6 +47,17 @@ + + 1.0.14-SNAPSHOT 17 UTF-8