Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
464 changes: 122 additions & 342 deletions .github/workflows/java-publish-maven.yml

Large diffs are not rendered by default.

34 changes: 22 additions & 12 deletions .github/workflows/java-sdk-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -360,14 +361,15 @@ 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 }}"
test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-windows.outputs.source_sha }}"
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 }}"
Expand Down Expand Up @@ -407,17 +409,21 @@ jobs:
--quick-generate-key 'Copilot SDK local validation <local-validation@example.invalid>' 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" \
"-Dcopilot.native.external.darwin.classifier.path=$DARWIN_JAR" \
"-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 \

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add -DskipITs here and to the actual Java release publication command, which also intends to skip tests. The CI merge includes main's Failsafe 3.6.0 upgrade; that version no longer honors -DskipTests for integration tests. This new SDK installation step therefore runs integration tests while CLI preparation is skipped, causing the current publication assembly failure and consequently Java required. I reproduced Failsafe 3.6.0 running an integration test despite -DskipTests and confirmed that adding -DskipITs skips it. This is a deterministic interaction with main, not a failure of ${revision} versioning or a transient CI issue.

"-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 }})"
Expand Down Expand Up @@ -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

Expand Down
59 changes: 57 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move Java tagging into its own downstream job and make deploy-java-site depend on it, as was already done for the site dispatch itself. This fetch/push still runs after the non-idempotent gh release create steps in the same github-release job. If fetching or pushing the Java tag fails after the release has been created, rerunning the failed job stops at the already-existing release before reaching this step. The tag's existing-target check is correct, but it cannot help if the retry never reaches it; Java tagging and the dependent site deployment remain incomplete.

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 }}
7 changes: 2 additions & 5 deletions docs/developer-docs/secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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

Expand Down
26 changes: 26 additions & 0 deletions java/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<classifier>` 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 `<version>${revision}</version>`, and the single source of truth is the `<revision>` 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.
26 changes: 1 addition & 25 deletions java/copilot-native/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<parent>
<groupId>com.github</groupId>
<artifactId>copilot-sdk-java-parent</artifactId>
<version>1.0.14-SNAPSHOT</version>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down Expand Up @@ -884,30 +884,6 @@
</plugins>
</build>
</profile>
<!--
Keep the end-to-end local deployment check entirely local while
exercising the same deploy lifecycle used by releases.
-->
<profile>
<id>local-publication-validation</id>
<activation>
<property>
<name>copilot.native.test.local.publication</name>
<value>true</value>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<configuration>
<skipPublishing>true</skipPublishing>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<!--
Skip the npm download when building offline or when only the
placeholder JAR is needed: mvn -Dcopilot.native.skip.download=true
Expand Down
66 changes: 54 additions & 12 deletions java/copilot-native/scripts/validate-local-publication.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ export function validateLocalPublication({
requireSignatures,
version,
}) {
if (
artifactId !== "copilot-sdk-java" &&
artifactId !== "copilot-sdk-java-runtime"
) {
throw new Error(`Unsupported Java SDK artifact: ${artifactId}`);
}
const classifiers =
artifactId === "copilot-sdk-java-runtime"
? ["linux-x64", "linux-arm64", "win32-x64", "win32-arm64", "darwin-arm64"]
: [];
const artifactDirectory = path.join(
repositoryPath,
"com",
Expand All @@ -30,11 +40,9 @@ export function validateLocalPublication({
`${artifactId}-${version}.pom`,
`${artifactId}-${version}-sources.jar`,
`${artifactId}-${version}-javadoc.jar`,
`${artifactId}-${version}-linux-x64.jar`,
`${artifactId}-${version}-linux-arm64.jar`,
`${artifactId}-${version}-win32-x64.jar`,
`${artifactId}-${version}-win32-arm64.jar`,
`${artifactId}-${version}-darwin-arm64.jar`,
...classifiers.map(
(classifier) => `${artifactId}-${version}-${classifier}.jar`,
),
];
const files = new Set(fs.readdirSync(artifactDirectory));

Expand All @@ -61,16 +69,15 @@ export function validateLocalPublication({
);
}

validatePublishedPom({
artifactId,
pomPath: path.join(artifactDirectory, `${artifactId}-${version}.pom`),
version,
});
validatePlaceholderJar(
path.join(artifactDirectory, `${artifactId}-${version}.jar`),
);
for (const classifier of [
"linux-x64",
"linux-arm64",
"win32-x64",
"win32-arm64",
"darwin-arm64",
]) {
for (const classifier of classifiers) {
const filename = `${artifactId}-${version}-${classifier}.jar`;
validateNativeClassifierJar({
classifier,
Expand All @@ -83,6 +90,41 @@ export function validateLocalPublication({
return artifactDirectory;
}

function validatePublishedPom({ artifactId, pomPath, version }) {
const pom = fs.readFileSync(pomPath, "utf8").replace(/<!--[\s\S]*?-->/g, "");
if (pom.includes("${revision}")) {
throw new Error(
`Published POM contains unresolved \${revision}: ${pomPath}`,
);
}
if (/<parent(?:\s|\/?>)/.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(
/<project\b[^>]*>\s*<modelVersion>[^<]+<\/modelVersion>\s*<groupId>([^<]+)<\/groupId>\s*<artifactId>([^<]+)<\/artifactId>\s*<version>([^<]+)<\/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);
Expand Down
Loading
Loading