diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47bcad9b5..a1637e754 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: lint: timeout-minutes: 15 name: lint - runs-on: 'ubuntu-latest' + runs-on: ${{ github.repository == 'anthropics/anthropic-sdk-java' && 'ubuntu-latest-16core' || 'ubuntu-latest-16-core' }} if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) steps: @@ -31,21 +31,25 @@ jobs: java-version: | 8 21 - cache: gradle - name: Set up Gradle uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3 + with: + # Required for configuration-cache data to be saved/restored across CI runs. + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Run lints run: ./scripts/lint build: - timeout-minutes: 15 + # The first run on a fresh cache compiles everything and generates all documentation, which + # does not fit in 15 minutes; cached runs are much faster. + timeout-minutes: 30 name: build permissions: contents: read id-token: write - runs-on: 'ubuntu-latest' + runs-on: ${{ github.repository == 'anthropics/anthropic-sdk-java' && 'ubuntu-latest-16core' || 'ubuntu-latest-16-core' }} if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) steps: @@ -58,10 +62,12 @@ jobs: java-version: | 8 21 - cache: gradle - name: Set up Gradle uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3 + with: + # Required for configuration-cache data to be saved/restored across CI runs. + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Build SDK run: ./scripts/build @@ -94,7 +100,7 @@ jobs: test: timeout-minutes: 30 name: test - runs-on: 'ubuntu-latest' + runs-on: ${{ github.repository == 'anthropics/anthropic-sdk-java' && 'ubuntu-latest-16core' || 'ubuntu-latest-16-core' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -106,10 +112,12 @@ jobs: java-version: | 8 21 - cache: gradle - name: Set up Gradle - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3 + with: + # Required for configuration-cache data to be saved/restored across CI runs. + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Run tests run: ./scripts/test @@ -117,7 +125,7 @@ jobs: detect_breaking_changes_vs_main: timeout-minutes: 15 name: detect-breaking-changes-vs-main - runs-on: ${{ github.repository == 'stainless-sdks/anthropic-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: ${{ github.repository == 'anthropics/anthropic-sdk-java' && 'ubuntu-latest-16core' || 'ubuntu-latest-16-core' }} if: |- (github.event_name == 'push' && !startsWith(github.ref, 'refs/heads/release-please--')) || @@ -140,10 +148,12 @@ jobs: java-version: | 8 21 - cache: gradle - name: Set up Gradle uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3 + with: + # Required for configuration-cache data to be saved/restored across CI runs. + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Determine base SHA run: | diff --git a/.github/workflows/create-releases.yml b/.github/workflows/create-releases.yml index 935219780..0cfc1d2dd 100644 --- a/.github/workflows/create-releases.yml +++ b/.github/workflows/create-releases.yml @@ -10,7 +10,7 @@ jobs: release: name: release if: github.ref == 'refs/heads/main' && github.repository == 'anthropics/anthropic-sdk-java' - runs-on: ubuntu-latest + runs-on: ubuntu-latest-16-core environment: production-release steps: @@ -30,11 +30,10 @@ jobs: java-version: | 8 21 - cache: gradle - name: Set up Gradle if: ${{ steps.release.outputs.releases_created }} - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3 - name: Publish to Sonatype if: ${{ steps.release.outputs.releases_created }} diff --git a/.github/workflows/publish-sonatype.yml b/.github/workflows/publish-sonatype.yml index 565e4fd0f..85d7b88cb 100644 --- a/.github/workflows/publish-sonatype.yml +++ b/.github/workflows/publish-sonatype.yml @@ -7,7 +7,7 @@ on: jobs: publish: name: publish - runs-on: ubuntu-latest + runs-on: ubuntu-latest-16-core steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -19,10 +19,9 @@ jobs: java-version: | 8 21 - cache: gradle - name: Set up Gradle - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3 - name: Publish to Sonatype run: |- @@ -34,4 +33,4 @@ jobs: SONATYPE_USERNAME: ${{ secrets.ANTHROPIC_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }} SONATYPE_PASSWORD: ${{ secrets.ANTHROPIC_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }} GPG_SIGNING_KEY: ${{ secrets.ANTHROPIC_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }} - GPG_SIGNING_PASSWORD: ${{ secrets.ANTHROPIC_SONATYPE_GPG_SIGNING_PASSWORD || secrets.GPG_SIGNING_PASSWORD }} \ No newline at end of file + GPG_SIGNING_PASSWORD: ${{ secrets.ANTHROPIC_SONATYPE_GPG_SIGNING_PASSWORD || secrets.GPG_SIGNING_PASSWORD }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8d572e9c9..29ad411ba 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.40.1" + ".": "2.41.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1005cef25..414acc07e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 2.41.0 (2026-06-11) + +Full Changelog: [v2.40.1...v2.41.0](https://github.com/anthropics/anthropic-sdk-java/compare/v2.40.1...v2.41.0) + +### Build System + +* fix runners ([#66](https://github.com/anthropics/anthropic-sdk-java/issues/66)) ([19882bb](https://github.com/anthropics/anthropic-sdk-java/commit/19882bbada3e1ccbd1cb4fc10b011801cd0865fa)) +* speed up local and CI builds ([#62](https://github.com/anthropics/anthropic-sdk-java/issues/62)) ([78de617](https://github.com/anthropics/anthropic-sdk-java/commit/78de617c06dc8fb31327ec4682649a613860b348)) + ## 2.40.1 (2026-06-09) Full Changelog: [v2.40.0...v2.40.1](https://github.com/anthropics/anthropic-sdk-java/compare/v2.40.0...v2.40.1) diff --git a/README.md b/README.md index 1d1082a75..30d58e81f 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Full documentation is available at **[platform.claude.com/docs/en/api/sdks/java] ### Gradle ```kotlin -implementation("com.anthropic:anthropic-java:2.40.1") +implementation("com.anthropic:anthropic-java:2.41.0") ``` ### Maven @@ -24,7 +24,7 @@ implementation("com.anthropic:anthropic-java:2.40.1") com.anthropic anthropic-java - 2.40.1 + 2.41.0 ``` diff --git a/anthropic-java-proguard-test/build.gradle.kts b/anthropic-java-proguard-test/build.gradle.kts index 1038c2368..7ec112802 100644 --- a/anthropic-java-proguard-test/build.gradle.kts +++ b/anthropic-java-proguard-test/build.gradle.kts @@ -31,9 +31,10 @@ val proguardJarPath = "${layout.buildDirectory.get()}/libs/${project.name}-${pro val proguardJar by tasks.registering(proguard.gradle.ProGuardTask::class) { group = "verification" dependsOn(tasks.shadowJar) - notCompatibleWithConfigurationCache("ProGuard") - injars(tasks.shadowJar) + // Pass the archive path rather than the task itself: `Task` objects cannot + // be serialized to the configuration cache. + injars(tasks.shadowJar.get().archiveFile.get().asFile.absolutePath) outjars(proguardJarPath) printmapping("${layout.buildDirectory.get()}/proguard-mapping.txt") @@ -57,40 +58,56 @@ val proguardJar by tasks.registering(proguard.gradle.ProGuardTask::class) { val testProGuard by tasks.registering(JavaExec::class) { group = "verification" dependsOn(proguardJar) - notCompatibleWithConfigurationCache("ProGuard") mainClass.set("com.anthropic.proguard.ProGuardCompatibilityTest") classpath = files(proguardJarPath) + + // This is a verification task with no file outputs, so rerun it only when + // the JAR changes. + outputs.upToDateWhen { true } } val r8JarPath = "${layout.buildDirectory.get()}/libs/${project.name}-${project.version}-r8.jar" val r8Jar by tasks.registering(JavaExec::class) { group = "verification" dependsOn(tasks.shadowJar) - notCompatibleWithConfigurationCache("R8") mainClass.set("com.android.tools.r8.R8") classpath = buildscript.configurations["classpath"] + val proguardConfigs = listOf( + "./test.pro", + "../anthropic-java-core/src/main/resources/META-INF/proguard/anthropic-java-core.pro", + ) + args = listOf( "--release", "--classfile", "--output", r8JarPath, "--lib", System.getProperty("java.home"), - "--pg-conf", "./test.pro", - "--pg-conf", "../anthropic-java-core/src/main/resources/META-INF/proguard/anthropic-java-core.pro", + "--pg-conf", proguardConfigs[0], + "--pg-conf", proguardConfigs[1], "--pg-map-output", "${layout.buildDirectory.get()}/r8-mapping.txt", tasks.shadowJar.get().archiveFile.get().asFile.absolutePath, ) + + // `args` are not tracked as task inputs, so declare them explicitly for + // up-to-date checking. + inputs.files(tasks.shadowJar.map { it.archiveFile }) + inputs.files(proguardConfigs) + outputs.file(r8JarPath) } val testR8 by tasks.registering(JavaExec::class) { group = "verification" dependsOn(r8Jar) - notCompatibleWithConfigurationCache("R8") mainClass.set("com.anthropic.proguard.ProGuardCompatibilityTest") classpath = files(r8JarPath) + + // This is a verification task with no file outputs, so rerun it only when + // the JAR changes. + outputs.upToDateWhen { true } } tasks.test { diff --git a/anthropic-java/build.gradle.kts b/anthropic-java/build.gradle.kts index 62c8f1d88..0c96e0772 100644 --- a/anthropic-java/build.gradle.kts +++ b/anthropic-java/build.gradle.kts @@ -7,23 +7,37 @@ dependencies { api(project(":anthropic-java-client-okhttp")) } -// Redefine `dokkaJavadoc` to: -// - Depend on the root project's task for merging the docs of all the projects -// - Forward that task's output to this task's output -tasks.named("dokkaJavadoc").configure { - actions.clear() +// This module's javadoc JAR must document the API of every module it +// re-exports, so add each module's main sources to this module's `dokkaJavadoc` +// task as extra source sets. +tasks.named("dokkaJavadoc").configure { + // Run after every other module's `dokkaJavadoc`: this task's documentation generation is by + // far the largest, and Dokka generates in-process, so running it concurrently with another + // large generation can exhaust the Gradle daemon's heap. + rootProject.subprojects + .filter { it.name != project.name } + .forEach { subproject -> mustRunAfter(subproject.tasks.matching { it.name == "dokkaJavadoc" }) } - val dokkaJavadocCollector = rootProject.tasks["dokkaJavadocCollector"] - dependsOn(dokkaJavadocCollector) - - val outputDirectory = project.layout.buildDirectory.dir("dokka/javadoc") - doLast { - copy { - from(dokkaJavadocCollector.outputs.files) - into(outputDirectory) - duplicatesStrategy = DuplicatesStrategy.INCLUDE - } + dokkaSourceSets { + rootProject.subprojects + .filter { it.file("src/main/kotlin").exists() } + .sortedBy { it.name } + .forEach { subproject -> + register(subproject.name) { + sourceRoots.from( + listOf("src/main/kotlin", "src/main/java") + .map(subproject::file) + .filter { it.exists() } + ) + // Resolve lazily: sibling projects may not be configured + // yet when this runs. + classpath.from( + project.provider { + subproject.configurations.getByName("compileClasspath") + } + ) + jdkVersion.set(8) + } + } } - - outputs.dir(outputDirectory) } diff --git a/build.gradle.kts b/build.gradle.kts index 07b458e32..55a823e49 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ repositories { allprojects { group = "com.anthropic" - version = "2.40.1" // x-release-please-version + version = "2.41.0" // x-release-please-version } subprojects { @@ -26,10 +26,3 @@ subprojects { subprojects { apply(plugin = "org.jetbrains.dokka") } - -// Avoid race conditions between `dokkaJavadocCollector` and `dokkaJavadocJar` tasks -tasks.named("dokkaJavadocCollector").configure { - subprojects.flatMap { it.tasks } - .filter { it.project.name != "anthropic-java" && it.name == "dokkaJavadocJar" } - .forEach { mustRunAfter(it) } -} diff --git a/buildSrc/src/main/kotlin/anthropic.java.gradle.kts b/buildSrc/src/main/kotlin/anthropic.java.gradle.kts index 0239e2013..76f8009a7 100644 --- a/buildSrc/src/main/kotlin/anthropic.java.gradle.kts +++ b/buildSrc/src/main/kotlin/anthropic.java.gradle.kts @@ -36,7 +36,6 @@ tasks.withType().configureEach { // Run tests in parallel to some degree. maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) - forkEvery = 100 // Mockito's ByteBuddy agent emits a JVM warning when loaded dynamically. Tests that capture // stderr (e.g. LoggingHttpClientTest) see this warning interleaved with their expected output @@ -49,6 +48,15 @@ tasks.withType().configureEach { } } +dependencies { + // SLF4J lazily initializes on the first `LoggerFactory.getLogger` call in + // the JVM and, without a provider, prints a warning to stderr. That warning + // corrupts tests that capture and assert on exact stderr contents when + // another test races the initialization. Binding a no-op provider keeps + // SLF4J silent. + "testRuntimeOnly"("org.slf4j:slf4j-nop:2.0.16") +} + val palantir by configurations.creating dependencies { palantir("com.palantir.javaformat:palantir-java-format:2.89.0") diff --git a/gradle.properties b/gradle.properties index 6680f9ce9..f4c160ba6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,18 +1,26 @@ org.gradle.caching=true org.gradle.configuration-cache=true org.gradle.parallel=true -org.gradle.daemon=false -# These options improve our compilation and test performance. They are inherited by the Kotlin daemon. +org.gradle.daemon=true +# JVM flags for the Gradle daemon, chosen from measurements: +# - Heaps are capped so that the Gradle daemon, Kotlin daemon, and test workers +# fit together in a 16 GB GitHub Actions runner; exceeding the runner's memory +# gets the job killed mid-run. The Gradle daemon needs the most headroom +# because Dokka generates documentation in-process, and a cold build can run +# two large generations concurrently (a 3 GB heap OOMs). +# - ParallelGC gives a measured ~2% faster clean build than the default G1. +# - Both daemons peak at ~200 MB of JIT-compiled code, which overflows the +# default 240 MB reserved code cache once it is split into segments, so +# reserve more (reserved != committed, so this costs nothing up front). +# - The metaspace cap (default: unlimited) is a guard against runaway memory on +# CI, not a speedup; the daemons use 100-260 MB. org.gradle.jvmargs=\ - -Xms2g \ - -Xmx8g \ + -Xmx6g \ -XX:+UseParallelGC \ - -XX:InitialCodeCacheSize=256m \ - -XX:ReservedCodeCacheSize=1G \ - -XX:MetaspaceSize=512m \ - -XX:MaxMetaspaceSize=2G \ - -XX:TieredStopAtLevel=1 \ - -XX:GCTimeRatio=4 \ - -XX:CICompilerCount=4 \ - -XX:+OptimizeStringConcat \ - -XX:+UseStringDeduplication + -XX:ReservedCodeCacheSize=512m \ + -XX:MaxMetaspaceSize=1G +kotlin.daemon.jvmargs=\ + -Xmx4g \ + -XX:+UseParallelGC \ + -XX:ReservedCodeCacheSize=512m \ + -XX:MaxMetaspaceSize=1G