Skip to content

Commit 34e0d7f

Browse files
ci: avoid duplicate API compatibility compilation (#1015)
The compatibility detector compiles baseline and proposed fixtures separately even when their inputs are identical. Compare the prepared source trees, public fixtures, and dependency manifests and omit only the duplicate proposed compilation when they match. Keep the baseline compilation uncached and always executing; differing inputs, comparison errors, or additional default source directories retain both compilations. Both policy validations and public API signature checks still run. In a [hosted Linux comparison](https://github.com/openai/openai-java/actions/runs/35261823386/job/105339667724), detector time fell from **244.81s to 152.43s**: **92.38s (38%) saved**. Both API manifest checks passed and the selected compatibility compilation executed uncached. This was one sequential old/new pair on the same runner and SDK; the second trial also benefits from a warm process/environment, so this is not an end-to-end queue estimate. Local JDK 21 measurements were **130.01s to 99.80s**. This improves identical-fixture runs; changed fixtures still require both compilations. Validation: 50 build-logic tests passed (one existing OS-specific skip), Kotlin/Java lint, shell syntax, real compiler/signature checks, and two consecutive clean independent adversarial review rounds including compatibility/security review. Focused tests cover changed and untracked sources, fixture/dependency changes, default source directories, invalid exclusions, and proposed-manifest rejection even when compilation is shared. No dependencies, SDK APIs, workflow policy, or test-sharding changes. Temporary benchmarking instrumentation is excluded. CI intentionally runs both the trusted base detector and proposed detector on this PR; after merge, normal unchanged-detector runs use the optimized detector once. <!-- codex-thread: 01a0b068-1c07-70a1-b846-2ed907c8c512 -->
1 parent 15d4081 commit 34e0d7f

2 files changed

Lines changed: 168 additions & 2 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package com.openai.gradle
2+
3+
import java.nio.file.Path
4+
import javax.tools.ToolProvider
5+
import kotlin.io.path.createDirectories
6+
import kotlin.io.path.readText
7+
import kotlin.io.path.writeText
8+
import kotlin.test.Test
9+
import kotlin.test.assertEquals
10+
import kotlin.test.assertFalse
11+
import kotlin.test.assertNotEquals
12+
import kotlin.test.assertTrue
13+
import org.junit.jupiter.api.condition.EnabledOnOs
14+
import org.junit.jupiter.api.condition.OS
15+
import org.junit.jupiter.api.io.TempDir
16+
17+
@EnabledOnOs(OS.LINUX, OS.MAC)
18+
class ApiCompatibilityDetectorTest {
19+
@TempDir lateinit var directory: Path
20+
21+
@Test
22+
fun `identical inputs compile once and still check both public API manifests`() {
23+
val root = prepare("identical")
24+
assertEquals(0, detect(root).exitCode)
25+
assertEquals(listOf(BASELINE_TASK), compilationTasks(root))
26+
27+
write(root, "$POLICY/structured-output-public-api.txt", "com.openai.Example#missing ()V\n")
28+
val invalidManifest = detect(root)
29+
assertNotEquals(0, invalidManifest.exitCode)
30+
assertTrue(invalidManifest.output.contains("proposed structured-output API is missing"))
31+
assertEquals(listOf(BASELINE_TASK), compilationTasks(root))
32+
}
33+
34+
@Test
35+
fun `different compiler inputs retain both compilations`() {
36+
val changedInputs =
37+
listOf(
38+
"$TEST_ROOT/models/Added.kt",
39+
"$TEST_ROOT/services/Service.kt",
40+
"$TEST_ROOT/TestServerExtension.kt",
41+
"$POLICY/kotlin/Fixture.kt",
42+
"$POLICY/dependencies.txt",
43+
"openai-java-core/src/externalApiCompatibility/kotlin/Extra.kt",
44+
"openai-java-core/src/proposedApiCompatibility/kotlin/Extra.kt",
45+
)
46+
changedInputs.forEachIndexed { index, path ->
47+
val root = prepare("changed-$index", stopAtCompilation = true)
48+
write(root, path, "// Changed compiler input\n")
49+
val result = detect(root)
50+
assertEquals(COMPILATION_REACHED, result.exitCode, "$path: ${result.output}")
51+
assertEquals(listOf(BASELINE_TASK, PROPOSED_TASK), compilationTasks(root), path)
52+
}
53+
}
54+
55+
@Test
56+
fun `invalid proposed exclusions fail before compilation`() {
57+
val root = prepare("invalid-policy")
58+
write(root, "$POLICY/excluded-generated-tests.txt", "$TEST_ROOT/models/Missing.kt\n")
59+
val result = detect(root)
60+
assertNotEquals(0, result.exitCode)
61+
assertTrue(result.output.contains("proposed compatibility exclusion does not exist"))
62+
assertFalse(root.resolve("gradle-arguments.txt").toFile().exists())
63+
}
64+
65+
private fun prepare(name: String, stopAtCompilation: Boolean = false): Path {
66+
val root = directory.resolve(name).createDirectories()
67+
write(
68+
root,
69+
"scripts/detect-breaking-changes",
70+
Path.of("../scripts/detect-breaking-changes").readText(),
71+
)
72+
// Task-selection cases stop here; the manifest test also exercises real javap checks.
73+
val compilationExit = if (stopAtCompilation) COMPILATION_REACHED else 0
74+
val gradle =
75+
write(
76+
root,
77+
"scripts/gradle",
78+
"#!/usr/bin/env bash\nprintf '%s\\n' \"${'$'}@\" > gradle-arguments.txt\nexit $compilationExit\n",
79+
)
80+
assertTrue(gradle.toFile().setExecutable(true))
81+
write(root, "$TEST_ROOT/models/Model.kt", "class Model\n")
82+
write(root, "$TEST_ROOT/services/Service.kt", "class Service\n")
83+
write(root, "$TEST_ROOT/TestServerExtension.kt", "class TestServerExtension\n")
84+
write(root, "$POLICY/kotlin/Fixture.kt", "class Fixture\n")
85+
write(root, "$POLICY/dependencies.txt", "org.jetbrains.kotlin:kotlin-stdlib:1.9.20\n")
86+
write(root, "$POLICY/excluded-generated-tests.txt", "# No exclusions\n")
87+
write(root, "$POLICY/structured-output-public-api.txt", "com.openai.Example#call ()V\n")
88+
val java =
89+
write(
90+
root,
91+
"Example.java",
92+
"package com.openai; public class Example { public static void call() {} }\n",
93+
)
94+
val classes = root.resolve("openai-java-core/build/classes/kotlin/main").createDirectories()
95+
assertEquals(
96+
0,
97+
ToolProvider.getSystemJavaCompiler()
98+
.run(null, null, null, "-d", classes.toString(), java.toString()),
99+
)
100+
assertEquals(0, run(root, "git", "init", "--quiet").exitCode)
101+
assertEquals(0, run(root, "git", "add", "scripts", "openai-java-core/src").exitCode)
102+
assertEquals(
103+
0,
104+
run(
105+
root,
106+
"git",
107+
"-c",
108+
"user.name=Test",
109+
"-c",
110+
"user.email=test@example.invalid",
111+
"-c",
112+
"commit.gpgsign=false",
113+
"commit",
114+
"--quiet",
115+
"-m",
116+
"Baseline",
117+
)
118+
.exitCode,
119+
)
120+
return root
121+
}
122+
123+
private fun write(root: Path, path: String, text: String): Path =
124+
root.resolve(path).also {
125+
it.parent.createDirectories()
126+
it.writeText(text)
127+
}
128+
129+
private fun detect(root: Path): Result =
130+
run(root, "bash", "scripts/detect-breaking-changes", "HEAD")
131+
132+
private fun compilationTasks(root: Path): List<String> =
133+
root.resolve("gradle-arguments.txt").readText().lines().filter { it.startsWith(":") }
134+
135+
private fun run(root: Path, vararg command: String): Result {
136+
val process =
137+
ProcessBuilder(*command).directory(root.toFile()).redirectErrorStream(true).start()
138+
val output = process.inputStream.bufferedReader().use { it.readText() }
139+
return Result(process.waitFor(), output)
140+
}
141+
142+
private data class Result(val exitCode: Int, val output: String)
143+
144+
private companion object {
145+
const val COMPILATION_REACHED = 42
146+
const val TEST_ROOT = "openai-java-core/src/test/kotlin/com/openai"
147+
const val POLICY = "openai-java-core/src/apiCompatibility"
148+
const val BASELINE_TASK = ":openai-java-core:compileExternalApiCompatibilityKotlin"
149+
const val PROPOSED_TASK = ":openai-java-core:compileProposedApiCompatibilityKotlin"
150+
}
151+
}

‎scripts/detect-breaking-changes‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,22 @@ BASELINE_FIXTURE_SOURCE_ROOT="$(pwd)/${BASELINE_FIXTURE_PATH}"
234234
BASELINE_DEPENDENCIES_FILE="$(pwd)/${BASELINE_DEPENDENCIES_PATH}"
235235
PROPOSED_FIXTURE_SOURCE_ROOT="$(pwd)/${PROPOSED_FIXTURE_PATH}"
236236
PROPOSED_DEPENDENCIES_FILE="$(pwd)/${PROPOSED_DEPENDENCIES_PATH}"
237+
238+
# One fresh compilation covers both policies when their complete compiler inputs match. Keep
239+
# separate compilations for any difference (including untracked local fixtures), comparison error,
240+
# or additional sources in Gradle's default source-set directories. Signature checks below still
241+
# validate both manifests independently.
242+
COMPATIBILITY_TASKS=(:openai-java-core:compileExternalApiCompatibilityKotlin)
243+
if [ ! -e openai-java-core/src/externalApiCompatibility ] &&
244+
[ ! -e openai-java-core/src/proposedApiCompatibility ] &&
245+
diff -qr "${GENERATED_TEST_SOURCE_ROOT}" "${PROPOSED_GENERATED_TEST_SOURCE_ROOT}" >/dev/null &&
246+
diff -qr "${BASELINE_FIXTURE_SOURCE_ROOT}" "${PROPOSED_FIXTURE_SOURCE_ROOT}" >/dev/null &&
247+
cmp -s "${BASELINE_DEPENDENCIES_FILE}" "${PROPOSED_DEPENDENCIES_FILE}"; then
248+
echo "Compatibility compiler inputs are identical; compiling once against the proposed SDK."
249+
else
250+
COMPATIBILITY_TASKS+=(:openai-java-core:compileProposedApiCompatibilityKotlin)
251+
fi
252+
237253
COMPATIBILITY_INIT_SCRIPT="$(mktemp)"
238254
trap 'rm -f "${COMPATIBILITY_INIT_SCRIPT}"' EXIT
239255

@@ -316,8 +332,7 @@ GENERATED_TEST_SOURCE_ROOT="${GENERATED_TEST_SOURCE_ROOT}" \
316332
PROPOSED_DEPENDENCIES_FILE="${PROPOSED_DEPENDENCIES_FILE}" \
317333
./scripts/gradle \
318334
--init-script "${COMPATIBILITY_INIT_SCRIPT}" \
319-
:openai-java-core:compileExternalApiCompatibilityKotlin \
320-
:openai-java-core:compileProposedApiCompatibilityKotlin
335+
"${COMPATIBILITY_TASKS[@]}"
321336

322337
validate_public_api_signatures() {
323338
local manifest_path="$1"

0 commit comments

Comments
 (0)