ci: fix JetBrains typecheck/test CI race conditions

- Add compile-only Gradle typecheck task so JetBrains CI no longer
  requires CLI binaries or buildPlugin to verify Kotlin compiles
- Remove PrepareLocalCliTask from Gradle; CLI binary prep is now an
  explicit step via bun run build, not triggered implicitly from
  processResources
- Remove @kilocode/cli#build dependency from JetBrains typecheck in
  Turbo so root typecheck and test:ci don't pull CLI bundling as a
  prerequisite for JetBrains compile
- Add @kilocode/cli#build -> @kilocode/sdk#build ordering in Turbo to
  prevent CLI bundling from racing with SDK generated-source cleanup
- Fix test-ci.ts: Windows gradlew.bat support, strip nested XML
  declarations from JUnit reports, exit with Gradle exit code
This commit is contained in:
kirillk
2026-05-15 09:04:17 -04:00
parent c79785ea90
commit d06a10dbb8
8 changed files with 38 additions and 108 deletions
+3 -2
View File
@@ -67,8 +67,9 @@
## Build
- **Full build**: `bun run build` from `packages/kilo-jetbrains/` (builds CLI + Gradle plugin).
- **Gradle only**: `./gradlew buildPlugin` from `packages/kilo-jetbrains/` (requires CLI binaries already present).
- **Typecheck**: `bun run typecheck` or `./gradlew typecheck` from `packages/kilo-jetbrains/` — compiles all Kotlin sources including the generated API client. Does NOT require CLI binaries.
- **Full build**: `bun run build` from `packages/kilo-jetbrains/` (prepares CLI binaries + runs Gradle `buildPlugin`).
- **Gradle only**: `./gradlew buildPlugin` from `packages/kilo-jetbrains/` (requires CLI binaries already present in `backend/build/generated/cli/`; run `bun run build --prepare-cli` first).
- **Via Turbo**: `bun turbo build --filter=@kilocode/kilo-jetbrains` from repo root.
- **Run in sandbox**: `./gradlew runIde` — launches sandboxed IntelliJ with the plugin. Does NOT build CLI binaries.
@@ -88,37 +88,12 @@ val requiredPlatforms = listOf(
"windows-arm64",
)
val localCli by tasks.registering(PrepareLocalCliTask::class) {
description = "Prepare local CLI binary for JetBrains dev"
val os = providers.systemProperty("os.name").map {
val name = it.lowercase()
if (name.contains("mac")) return@map "darwin"
if (name.contains("win")) return@map "windows"
if (name.contains("linux")) return@map "linux"
throw GradleException("Unsupported host OS: $it")
}
val arch = providers.systemProperty("os.arch").map {
val name = it.lowercase()
if (name == "aarch64" || name == "arm64") return@map "arm64"
if (name == "x86_64" || name == "amd64") return@map "x64"
throw GradleException("Unsupported host arch: $it")
}
script.set(rootProject.layout.projectDirectory.file("script/build.ts"))
root.set(rootProject.layout.projectDirectory)
out.set(cliDir)
platform.set(os.zip(arch) { a, b -> "$a-$b" })
exe.set(platform.map { if (it.startsWith("windows")) "kilo.exe" else "kilo" })
}
val prod = production
val checkCli by tasks.registering(CheckCliTask::class) {
description = "Verify CLI binaries exist before building"
description = "Verify CLI binaries exist before packaging"
dir.set(cliDir)
this.production.set(prod)
platforms.set(requiredPlatforms)
if (!prod.get()) {
dependsOn(localCli)
}
}
tasks.processResources {
@@ -5,6 +5,7 @@ import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
@@ -14,9 +15,14 @@ import java.io.File
* Verify that CLI binaries exist before packaging the plugin.
* In production mode, all platform binaries must be present.
* In dev mode, only the current platform binary is required.
*
* CLI binaries must be prepared separately before packaging:
* Local: bun run build (from packages/kilo-jetbrains/)
* Production: bun run build:production
*/
abstract class CheckCliTask : DefaultTask() {
@get:InputDirectory
@get:Optional
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val dir: DirectoryProperty
@@ -1,74 +0,0 @@
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
import java.io.File
import javax.inject.Inject
abstract class PrepareLocalCliTask : DefaultTask() {
@get:InputFile
abstract val script: RegularFileProperty
@get:Internal
abstract val root: DirectoryProperty
@get:OutputDirectory
abstract val out: DirectoryProperty
@get:Input
abstract val platform: Property<String>
@get:Input
abstract val exe: Property<String>
@get:Inject
abstract val exec: ExecOperations
@TaskAction
fun run() {
val bin = out.file("${platform.get()}/${exe.get()}").get().asFile
if (bin.exists()) return
exec.exec {
workingDir = root.get().asFile
commandLine(findBun(), "script/build.ts", "--prepare-cli")
}
}
/**
* Resolve the absolute path to `bun`. The Gradle daemon's PATH is often
* stripped down and doesn't include Homebrew or user-local bin dirs.
* Probe common install locations so the build works without manual PATH setup.
*/
private fun findBun(): String {
// 1. Already on PATH?
val which = runCatching {
ProcessBuilder("which", "bun")
.redirectErrorStream(true)
.start()
.inputStream.bufferedReader().readLine()?.trim()
}.getOrNull()
if (which != null && File(which).isFile) return which
// 2. Common install locations
val home = System.getProperty("user.home")
val candidates = listOf(
"$home/.bun/bin/bun",
"/opt/homebrew/bin/bun",
"/usr/local/bin/bun",
"$home/.nvm/current/bin/bun",
)
for (path in candidates) {
val f = File(path)
if (f.isFile && f.canExecute()) return f.absolutePath
}
// 3. Fall back — let the OS resolve it (will fail with a clear message)
return "bun"
}
}
+12
View File
@@ -127,6 +127,18 @@ tasks {
}
}
// Compile-only typecheck: verifies Kotlin compiles (including generated API client)
// without running processResources, CLI binary prep, or buildPlugin.
tasks.register("typecheck") {
dependsOn(
":shared:compileKotlin",
":frontend:compileKotlin",
":backend:compileKotlin",
":frontend:compileTestKotlin",
":backend:compileTestKotlin",
)
}
tasks.named<JavaExec>("runIde") {
dependsOn(":backend:processResources")
jvmArgumentProviders += CommandLineArgumentProvider {
+1 -1
View File
@@ -4,7 +4,7 @@
"scripts": {
"build": "bun script/build.ts",
"build:production": "bun script/build.ts --production",
"typecheck": "bun run build",
"typecheck": "./gradlew typecheck",
"test": "./gradlew test",
"test:ci": "bun script/test-ci.ts"
}
+11 -4
View File
@@ -7,8 +7,8 @@
* then collects per-module JUnit XML results into .artifacts/unit/junit.xml
* so mikepenz/action-junit-report can find them at the standard path.
*
* Always exits 0 — test failures are reported via the JUnit uploader,
* not by failing the CI job itself.
* Exits with the Gradle exit code after writing the aggregate report so that
* test failures fail the CI job.
*/
import { $ } from "bun"
@@ -16,8 +16,9 @@ import { join } from "node:path"
import { mkdirSync, readdirSync, readFileSync, writeFileSync, existsSync } from "node:fs"
const root = join(import.meta.dir, "..")
const gradlew = process.platform === "win32" ? "gradlew.bat" : "./gradlew"
await $`./gradlew test --continue`.cwd(root).nothrow()
const result = await $`${gradlew} test --continue`.cwd(root).nothrow()
const modules = [".", "shared", "frontend", "backend"]
const suites: string[] = []
@@ -27,7 +28,11 @@ for (const mod of modules) {
if (!existsSync(dir)) continue
for (const f of readdirSync(dir)) {
if (!f.endsWith(".xml")) continue
suites.push(readFileSync(join(dir, f), "utf8"))
// Strip leading XML declaration so it does not appear as a nested
// declaration inside the <testsuites> wrapper, which would produce
// malformed XML and fail the JUnit report uploader.
const xml = readFileSync(join(dir, f), "utf8").replace(/^\s*<\?xml[^>]*\?>\s*/u, "")
suites.push(xml)
}
}
@@ -36,3 +41,5 @@ mkdirSync(join(root, ".artifacts", "unit"), { recursive: true })
writeFileSync(out, `<?xml version="1.0" encoding="UTF-8"?>\n<testsuites>\n${suites.join("\n")}\n</testsuites>\n`)
console.log(`[jetbrains-test] collected ${suites.length} suite(s) -> ${out}`)
process.exit(result.exitCode)
+4 -1
View File
@@ -10,6 +10,10 @@
"dependsOn": [],
"outputs": ["dist/**"]
},
"@kilocode/cli#build": {
"dependsOn": ["@kilocode/sdk#build"],
"outputs": ["dist/**"]
},
"@kilocode/cli#test": {
"dependsOn": ["^build"],
"outputs": [],
@@ -30,7 +34,6 @@
"outputs": ["build/distributions/**"]
},
"@kilocode/kilo-jetbrains#typecheck": {
"dependsOn": ["@kilocode/cli#build"],
"outputs": []
},
"@kilocode/kilo-jetbrains#test:ci": {