From 32652b017a850ab9a153496eb9f339b152a120f7 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 24 Jul 2026 14:10:51 -0400 Subject: [PATCH] fix(jetbrains): prune bundled CLI cache --- .../workflows/publish-jetbrains-bundled.yml | 15 +++++------- .github/workflows/publish-jetbrains.yml | 1 + .../ai/kilocode/backend/cli/KiloRepoCli.kt | 24 ++++++++++++++++++- .../kilocode/backend/cli/KiloRepoCliTest.kt | 18 ++++++++++++++ 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/.github/workflows/publish-jetbrains-bundled.yml b/.github/workflows/publish-jetbrains-bundled.yml index 97dde98765..56b331680e 100644 --- a/.github/workflows/publish-jetbrains-bundled.yml +++ b/.github/workflows/publish-jetbrains-bundled.yml @@ -187,17 +187,14 @@ jobs: - name: Resolve bundled asset URL id: asset run: | - url="$(python3 - <<'PY' - import os - import urllib.parse - repo = os.environ["GITHUB_REPOSITORY"] - tag = urllib.parse.quote(os.environ["TAG"], safe="") - asset = urllib.parse.quote(os.environ["ASSET"], safe="") - print(f"https://github.com/{repo}/releases/download/{tag}/{asset}") - PY - )" + url="$(gh release view "$TAG" --json assets --jq '.assets[] | select(.name == env.ASSET) | .url' --repo "$GITHUB_REPOSITORY")" + if [[ -z "$url" ]]; then + echo "Could not resolve GitHub Release URL for $ASSET" >&2 + exit 1 + fi echo "url=$url" >> "$GITHUB_OUTPUT" env: + GH_TOKEN: ${{ github.token }} TAG: ${{ needs.validate.outputs.tag }} ASSET: ${{ steps.archive.outputs.asset }} diff --git a/.github/workflows/publish-jetbrains.yml b/.github/workflows/publish-jetbrains.yml index 665d4780b3..cb976193ac 100644 --- a/.github/workflows/publish-jetbrains.yml +++ b/.github/workflows/publish-jetbrains.yml @@ -201,6 +201,7 @@ jobs: NOTES: packages/kilo-jetbrains/build/release-notes.md - name: Dispatch bundled GitHub release build + continue-on-error: true run: | gh workflow run publish-jetbrains-bundled.yml \ --repo "$GITHUB_REPOSITORY" \ diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloRepoCli.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloRepoCli.kt index 42dac82365..8138ae4dae 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloRepoCli.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloRepoCli.kt @@ -1,5 +1,6 @@ package ai.kilocode.backend.cli +import ai.kilocode.log.KiloLog import com.intellij.openapi.application.PathManager import com.intellij.openapi.util.SystemInfo import kotlinx.coroutines.Dispatchers @@ -11,24 +12,32 @@ import java.util.zip.ZipInputStream object KiloRepoCli { private const val ARCHIVE = "kilo-cli.zip" + private val log = KiloLog.create(KiloRepoCli::class.java) fun available(): Boolean = KiloRepoCli::class.java.classLoader.getResource(ARCHIVE) != null suspend fun extract(force: Boolean): File = extract( force = force, root = File(PathManager.getSystemPath(), "kilo/repo-cli/${KiloProps.cliVersion()}"), + cleanup = true, source = { KiloRepoCli::class.java.classLoader.getResourceAsStream(ARCHIVE) ?: throw IllegalStateException("kilo-cli.zip resource not found; rebuild with bundled CLI resources") }, ) - internal suspend fun extract(force: Boolean, root: File, source: () -> InputStream): File = withContext(Dispatchers.IO) { + internal suspend fun extract( + force: Boolean, + root: File, + cleanup: Boolean = false, + source: () -> InputStream, + ): File = withContext(Dispatchers.IO) { val platform = KiloCliPlatform.current() val exe = File(root, "$platform/bin/${KiloCliPlatform.exe()}") val done = File(root, ".complete") if (!force && done.isFile && exe.isFile) { if (!SystemInfo.isWindows) exe.setExecutable(true) + if (cleanup) prune(root) return@withContext exe } @@ -53,9 +62,22 @@ object KiloRepoCli { if (!exe.isFile) throw IllegalStateException("Bundled CLI archive did not contain $platform/bin/${KiloCliPlatform.exe()}") if (!SystemInfo.isWindows) exe.setExecutable(true) done.writeText("ok\n") + if (cleanup) prune(root) return@withContext exe } + private fun prune(root: File) { + val parent = root.parentFile ?: return + val entries = parent.listFiles() ?: return + for (entry in entries) { + if (!entry.isDirectory || entry.name == root.name || entry.name.startsWith(".")) continue + log.info("Removing stale bundled Kilo CLI version ${entry.absolutePath}") + if (!entry.deleteRecursively()) { + log.warn("Failed to remove stale bundled Kilo CLI version ${entry.absolutePath}") + } + } + } + private fun select(dir: File, name: String, platform: String): String? { check(dir, name) val path = name.replace('\\', '/') diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloRepoCliTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloRepoCliTest.kt index 28f22a420c..b5a2adf93f 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloRepoCliTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloRepoCliTest.kt @@ -53,6 +53,24 @@ class KiloRepoCliTest { assertFalse(File(dir, "$other/bin/kilo").exists()) } + @Test + fun `prunes stale bundled cli versions after resolve`() = runBlocking { + val root = File(dir, "7.4.11") + val stale = File(dir, "7.4.10") + File(stale, "old").apply { + parentFile.mkdirs() + writeText("old") + } + + val cli = KiloRepoCli.extract(false, root, cleanup = true) { + ByteArrayInputStream(archive("current")) + } + + assertTrue(cli.isFile) + assertFalse(stale.exists()) + assertTrue(root.isDirectory) + } + @Test fun `rejects archive entries that escape root`() = runBlocking { val ex = assertFailsWith {