From 78de617c06dc8fb31327ec4682649a613860b348 Mon Sep 17 00:00:00 2001 From: Tomer Aberbach Date: Thu, 11 Jun 2026 14:33:10 -0400 Subject: [PATCH 1/3] build: speed up local and CI builds (#62) - Enable the Gradle daemon and give the Kotlin daemon its own JVM args with full tiered JIT compilation (the old -XX:TieredStopAtLevel=1 crippled the long-lived, CPU-bound Kotlin compiler; cold compile 84s -> 62s) - Audit the daemon JVM flags against measurements: drop no-op or unhelpful flags (-XX:+OptimizeStringConcat is default-on, -XX:+UseStringDeduplication had no measurable effect, -Xms2g/-XX:GCTimeRatio/-XX:CICompilerCount tuned the old no-daemon setup) and document why each kept flag earns its place - Cap daemon heaps so the Gradle daemon, Kotlin daemon, and test workers fit in a 16 GB GitHub Actions runner; uncapped daemons got CI jobs killed - Fix CI caching: setup-java's 'cache: gradle' restored ~/.gradle first, which made setup-gradle skip both restoring and saving its own cache (including the build cache), so CI recompiled everything every run - Unify on gradle/actions/setup-gradle v4 (the test and release workflows used the deprecated gradle-build-action v2) and wire cache-encryption-key so configuration-cache data can be saved/restored on CI - Generate the anthropic-java javadoc JAR from aggregated source sets on its own dokkaJavadoc task instead of the root dokkaJavadocCollector task, whose outputs overlap with the per-module dokka tasks' outputs, disabling build caching and re-documenting every module on every CI build (~17 min cold); warm CI publish step is now ~13s with dokka fully cached - Raise the build job timeout to 30 minutes so the first cache-seeding run (full compile + full dokka) can complete; cached runs are much faster - Make anthropic-java-proguard-test configuration-cache compatible (pass the shadow JAR path instead of the task to ProGuard's injars) and declare inputs/outputs on the ProGuard/R8 tasks so warm test runs skip ~15s of re-verification - Remove forkEvery=100 from test tasks and fix the underlying flakiness: SLF4J prints a 'no providers' warning to stderr on first use, corrupting tests that assert on exact stderr contents; binding slf4j-nop on the test runtime classpath keeps SLF4J silent (also ~4s faster test execution) - Raise the Gradle daemon heap to 6g: Dokka generates documentation in-process and a cold build OOMs the daemon at 3g (locally: 10.5 min of GC thrash, then OOM; at 6g the same cold publish completes in 1.5 min) - Run the anthropic-java dokkaJavadoc task after every other module's: two large in-process Dokka generations running concurrently can exhaust the daemon heap regardless of machine size (a CI run hung for 25 minutes this way); serializing them costs ~6s on a cold build - Move CI jobs to ubuntu-latest-16-core runners (16 vCPU / 64 GB vs 4 vCPU / 16 GB), cutting the cold-cache build job to ~10 min and lint to ~1 min --- .github/workflows/ci.yml | 30 ++++++++---- .github/workflows/create-releases.yml | 5 +- .github/workflows/publish-sonatype.yml | 7 ++- anthropic-java-proguard-test/build.gradle.kts | 31 +++++++++--- anthropic-java/build.gradle.kts | 48 ++++++++++++------- build.gradle.kts | 7 --- .../src/main/kotlin/anthropic.java.gradle.kts | 10 +++- gradle.properties | 34 ++++++++----- 8 files changed, 110 insertions(+), 62 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47bcad9b5..046299f05 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: '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: '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: '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 == 'stainless-sdks/anthropic-java' && 'depot-ubuntu-24.04' || '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/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..f83b8cf20 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 From 19882bbada3e1ccbd1cb4fc10b011801cd0865fa Mon Sep 17 00:00:00 2001 From: Tomer Aberbach Date: Thu, 11 Jun 2026 16:52:47 -0400 Subject: [PATCH 2/3] build: fix runners (#66) --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 046299f05..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-16-core' + 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: @@ -49,7 +49,7 @@ jobs: permissions: contents: read id-token: write - runs-on: 'ubuntu-latest-16-core' + 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: @@ -100,7 +100,7 @@ jobs: test: timeout-minutes: 30 name: test - runs-on: 'ubuntu-latest-16-core' + 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 @@ -125,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-16-core' }} + 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--')) || From 213d307ea764fb9a0455752c6432d30eb7ad4d35 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 21:06:40 +0000 Subject: [PATCH 3/3] release: 2.41.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 9 +++++++++ README.md | 4 ++-- build.gradle.kts | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) 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/build.gradle.kts b/build.gradle.kts index f83b8cf20..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 {