Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/stable-models-snapshot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Fail publication builds when the bundled models snapshot cannot be downloaded or validated, and load the snapshot as JSON data in compiled binaries.
238 changes: 177 additions & 61 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,182 @@ jobs:
outputs:
version: ${{ needs.version.outputs.version }}

build-vscode:
# kilocode_change start - execute supported Unix CLI binaries before packaging VSIX artifacts
validate-cli-unix:
name: Validate CLI (${{ matrix.target }})
needs: build-cli
runs-on: ${{ matrix.runner }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- target: darwin-arm64
runner: macos-15
uname: arm64
package: "@kilocode/cli-darwin-arm64"
mode: host
- target: darwin-x64
runner: macos-13
uname: x86_64
package: "@kilocode/cli-darwin-x64"
mode: host
- target: linux-arm64
runner: ubuntu-24.04-arm
uname: aarch64
package: "@kilocode/cli-linux-arm64"
mode: host
- target: linux-x64
runner: ubuntu-24.04
uname: x86_64
package: "@kilocode/cli-linux-x64"
mode: host
- target: alpine-arm64
runner: ubuntu-24.04-arm
uname: aarch64
package: "@kilocode/cli-linux-arm64-musl"
mode: alpine
platform: linux/arm64
- target: alpine-x64
runner: ubuntu-24.04
uname: x86_64
package: "@kilocode/cli-linux-x64-musl"
mode: alpine
platform: linux/amd64
steps:
- uses: actions/download-artifact@v8
with:
name: kilo-cli.tar.zst
path: /tmp
skip-decompress: true

- name: Unpack CLI dist
run: |
mkdir -p dist
tar --zstd -xf /tmp/kilo-cli.tar.zst -C dist

- name: Run CLI smoke test
run: |
test "$(uname -m)" = "${{ matrix.uname }}"

smoke_host() {
binary="$1"
"$binary" --version
root="$(mktemp -d)"
trap 'rm -rf "$root"' RETURN
(
unset KILO_MODELS_PATH KILO_MODELS_URL KILO_CONFIG KILO_CONFIG_DIR
export XDG_DATA_HOME="$root/data"
export XDG_CACHE_HOME="$root/cache"
export XDG_CONFIG_HOME="$root/config"
export XDG_STATE_HOME="$root/state"
export KILO_DISABLE_MODELS_FETCH=1
export KILO_DISABLE_PROJECT_CONFIG=1
export KILO_CONFIG_CONTENT='{"enabled_providers":["anthropic"]}'
export ANTHROPIC_API_KEY=dummy
"$binary" --pure models anthropic | grep -q '^anthropic/'
)
}

if [ "${{ matrix.mode }}" = "host" ]; then
smoke_host "./dist/${{ matrix.package }}/bin/kilo"
exit 0
fi

docker run --rm \
--platform "${{ matrix.platform }}" \
-v "$PWD/dist:/dist:ro" \
-e PACKAGE="${{ matrix.package }}" \
alpine:3.22 \
sh -c '
set -eu
binary="/dist/$PACKAGE/bin/kilo"
"$binary" --version
root="$(mktemp -d)"
trap '\''rm -rf "$root"'\'' EXIT
unset KILO_MODELS_PATH KILO_MODELS_URL KILO_CONFIG KILO_CONFIG_DIR
export XDG_DATA_HOME="$root/data"
export XDG_CACHE_HOME="$root/cache"
export XDG_CONFIG_HOME="$root/config"
export XDG_STATE_HOME="$root/state"
export KILO_DISABLE_MODELS_FETCH=1
export KILO_DISABLE_PROJECT_CONFIG=1
export KILO_CONFIG_CONTENT='\''{"enabled_providers":["anthropic"]}'\''
export ANTHROPIC_API_KEY=dummy
"$binary" --pure models anthropic | grep -q "^anthropic/"
'

validate-cli-windows:
name: Validate CLI (windows-${{ matrix.arch }})
needs: build-cli
runs-on: ${{ matrix.runner }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
runner: windows-11-arm
package: "@kilocode/cli-windows-arm64"
- arch: x64
runner: windows-2025
package: "@kilocode/cli-windows-x64"
steps:
- uses: actions/download-artifact@v8
with:
name: kilo-cli.tar.zst
path: ${{ runner.temp }}
skip-decompress: true

- name: Install zstd
shell: pwsh
run: |
if (-not (Get-Command zstd -ErrorAction SilentlyContinue)) {
choco install zstandard -y --no-progress
}

- name: Unpack CLI dist
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path dist | Out-Null
zstd -d --stdout "$env:RUNNER_TEMP\kilo-cli.tar.zst" | tar -xf - -C dist

- name: Run CLI smoke test
shell: pwsh
run: |
$package = "${{ matrix.package }}".Replace("/", "\")
$binary = ".\dist\$package\bin\kilo.exe"
& $binary --version
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

$root = Join-Path $env:RUNNER_TEMP ([guid]::NewGuid().ToString())
New-Item -ItemType Directory -Force -Path $root | Out-Null
try {
foreach ($name in "KILO_MODELS_PATH", "KILO_MODELS_URL", "KILO_CONFIG", "KILO_CONFIG_DIR") {
Remove-Item "Env:$name" -ErrorAction SilentlyContinue
}
$env:XDG_DATA_HOME = Join-Path $root "data"
$env:XDG_CACHE_HOME = Join-Path $root "cache"
$env:XDG_CONFIG_HOME = Join-Path $root "config"
$env:XDG_STATE_HOME = Join-Path $root "state"
$env:KILO_DISABLE_MODELS_FETCH = "1"
$env:KILO_DISABLE_PROJECT_CONFIG = "1"
$env:KILO_CONFIG_CONTENT = '{"enabled_providers":["anthropic"]}'
$env:ANTHROPIC_API_KEY = "dummy"
$output = & $binary --pure models anthropic
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
if (-not ($output -match "(?m)^anthropic/")) {
throw "Compiled Windows binary did not list Anthropic models from the sidecar snapshot"
}
} finally {
Remove-Item -Recurse -Force $root -ErrorAction SilentlyContinue
}
# kilocode_change end
build-vscode:
needs:
- build-cli
- validate-cli-unix
- validate-cli-windows
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'Kilo-Org/kilocode'
steps:
Expand Down Expand Up @@ -161,64 +335,6 @@ jobs:
name: kilo-vscode
path: packages/kilo-vscode/out

# kilocode_change start - execute the cross-compiled Apple Silicon binary before publishing it
validate-darwin-arm64:
name: Validate CLI (darwin-arm64)
needs: build-cli
runs-on: macos-15
timeout-minutes: 15
steps:
- uses: actions/download-artifact@v8
with:
name: kilo-cli.tar.zst
path: /tmp
skip-decompress: true

- name: Unpack CLI dist
run: |
mkdir -p dist
tar --zstd -xf /tmp/kilo-cli.tar.zst -C dist

- name: Run CLI smoke test
run: |
test "$(uname -m)" = "arm64"
./dist/@kilocode/cli-darwin-arm64/bin/kilo --version
# kilocode_change end

# kilocode_change start - execute native Linux x64 and arm64 binaries before publishing them
validate-linux:
name: Validate CLI (linux-${{ matrix.arch }})
needs: build-cli
strategy:
fail-fast: false
matrix:
include:
- arch: x64
runner: ubuntu-24.04
uname: x86_64
- arch: arm64
runner: ubuntu-24.04-arm
uname: aarch64
runs-on: ${{ matrix.runner }}
timeout-minutes: 15
steps:
- uses: actions/download-artifact@v8
with:
name: kilo-cli.tar.zst
path: /tmp
skip-decompress: true

- name: Unpack CLI dist
run: |
mkdir -p dist
tar --zstd -xf /tmp/kilo-cli.tar.zst -C dist

- name: Run CLI smoke test
run: |
test "$(uname -m)" = "${{ matrix.uname }}"
./dist/@kilocode/cli-linux-${{ matrix.arch }}/bin/kilo --version
# kilocode_change end

# kilocode_change start
# Run smoke tests against CLI assets uploaded to the draft GitHub release
# before publishing the release and package artifacts.
Expand All @@ -239,8 +355,8 @@ jobs:
- version
- build-cli
- build-vscode
- validate-darwin-arm64 # kilocode_change
- validate-linux # kilocode_change
- validate-cli-unix # kilocode_change
- validate-cli-windows # kilocode_change
- smoke-test
runs-on: ubuntu-24.04
steps:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,27 +85,36 @@ class KiloBackendCliManager(
private fun extractCli(): File {
val platform = platform()
val exe = if (SystemInfo.isWindows) "kilo.exe" else "kilo"
val resource = "cli/$platform/$exe"
val loader = javaClass.classLoader

val target = File(PathManager.getSystemPath(), "kilo/bin/$exe")
val snapshot = File(target.parentFile, "models-snapshot.json")

if (forceExtract && target.exists()) {
log.info("Force re-extracting CLI binary — deleting ${target.absolutePath}")
target.delete()
if (forceExtract) {
log.info("Force re-extracting CLI resources under ${target.parentFile.absolutePath}")
if (target.exists()) target.delete()
if (snapshot.exists()) snapshot.delete()
forceExtract = false
}

extractResource("cli/$platform/$exe", target, executable = true)
extractResource("cli/$platform/models-snapshot.json", snapshot, executable = false)
return target
}

private fun extractResource(resource: String, target: File, executable: Boolean) {
val loader = javaClass.classLoader
val url = loader.getResource(resource)
?: throw IllegalStateException("CLI binary not found in JAR resources at $resource")
?: throw IllegalStateException("CLI resource not found in JAR resources at $resource")

val size = url.openConnection().contentLengthLong
if (size >= 0 && target.exists() && target.length() == size) {
log.info("CLI binary up-to-date at ${target.absolutePath}")
return target
log.info("CLI resource up-to-date at ${target.absolutePath}")
if (executable && !SystemInfo.isWindows) {
target.setExecutable(true)
}
return
}

log.info("Extracting CLI binary to ${target.absolutePath}")
log.info("Extracting CLI resource to ${target.absolutePath}")
target.parentFile.mkdirs()

url.openStream().use { input ->
Expand All @@ -114,11 +123,9 @@ class KiloBackendCliManager(
}
}

if (!SystemInfo.isWindows) {
if (executable && !SystemInfo.isWindows) {
target.setExecutable(true)
}

return target
}

// Must be called from a background thread — devStorageEnv() performs blocking I/O (mkdirs).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ abstract class CheckCliTask : DefaultTask() {
val missing = platforms.get().filter { platform ->
val d = File(resolved, platform)
val exe = if (platform.startsWith("windows")) "kilo.exe" else "kilo"
!File(d, exe).exists()
!File(d, exe).exists() || !File(d, "models-snapshot.json").exists()
}
if (missing.isNotEmpty()) {
throw GradleException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import javax.inject.Inject
* 4. Each entry on `PATH`
* 5. Common install locations (`~/.bun/bin`, `/opt/homebrew/bin`, `/usr/local/bin`)
*
* After Bun finishes, the task verifies the expected binary exists so that
* After Bun finishes, the task verifies the expected binary and models snapshot exist so that
* Rosetta / architecture mismatches surface with a clear error message.
*/
@DisableCachingByDefault(because = "Local developer bootstrap task that shells out to Bun")
Expand All @@ -47,7 +47,8 @@ abstract class PrepareLocalCliTask : DefaultTask() {
@TaskAction
fun run() {
val expected = binary()
if (expected.exists()) {
val snapshot = snapshot()
if (expected.exists() && snapshot.exists()) {
logger.lifecycle("CLI binary already exists at ${expected.absolutePath}")
return
}
Expand All @@ -71,12 +72,19 @@ abstract class PrepareLocalCliTask : DefaultTask() {
"This can happen if Bun and Gradle are running under different architectures."
)
}
if (!snapshot.exists()) {
throw GradleException("Expected CLI models snapshot was not created at ${snapshot.absolutePath}.")
}
}

private fun binary(): File {
return File(File(dir.get().asFile, platform()), exe())
}

private fun snapshot(): File {
return File(File(dir.get().asFile, platform()), "models-snapshot.json")
}

private fun platform(): String {
return "${os()}-${arch()}"
}
Expand Down
Loading
Loading