docs(jetbrains): add CLI pin release helpers

This commit is contained in:
kirillk
2026-07-08 13:51:54 -04:00
parent 98d8d22ae8
commit b88175f6ec
5 changed files with 358 additions and 24 deletions
+45 -21
View File
@@ -38,44 +38,67 @@ Show the resolved `version`, `kind`, and default `fromTagDefault` to the user.
## CLI Pin Verification
Before dispatching prepare, verify the JetBrains plugin is pinned to the intended Kilo Core release. The plugin downloads the CLI version from `packages/kilo-jetbrains/package.json`, not from the JetBrains plugin version.
Before dispatching prepare, verify the JetBrains plugin is pinned to the intended Kilo Core release. The plugin downloads the CLI version from `packages/kilo-jetbrains/package.json`, not from the JetBrains plugin version. Prepare tags `origin/main`, so the authoritative pin is the value on `origin/main`, not a local edit.
Verify repo CLI dev mode is disabled on `main` before creating the immutable tag:
Run the pin preflight:
```bash
git show origin/main:packages/kilo-jetbrains/gradle.properties | grep '^kilo.cli.pinned=' || true
bun .kilo/skills/release-jetbrains/script/check-pin.ts
```
If `kilo.cli.pinned` is present and is not `true`, stop and ask the user to reset it to `true` on `main` before dispatching prepare. `kilo.cli.pinned=false` generates from and bundles the local repo CLI, so it is dev-only and non-releasable.
The script prints:
Read the pinned CLI version:
| Field | Meaning |
|---|---|
| `pinMain` | CLI version that `origin/main` will lock into the release tag. |
| `pinLocal` | CLI version in the current worktree, useful for catching stale local checkouts. |
| `latestCli` | Latest stable `v*` Kilo CLI GitHub release. |
| `prevJetbrainsCli` | CLI pin used by the latest `jetbrains/v*` release tag, for reviewing the jump. |
| `pinnedMain` / `pinnedLocal` | Whether `kilo.cli.pinned=true`; `false` means repo CLI dev mode. |
| `assetsOk` / `missingAssets` | Whether the pinned CLI release has every runtime asset. |
| `drift` | `up-to-date`, `behind`, `worktree-behind-main`, `repo-mode-on-main`, `repo-mode-local`, or `assets-missing`. |
Interpretation:
| Drift | Action |
|---|---|
| `up-to-date` | Continue after user confirmation. |
| `behind` | Stop and show `pinMain`, `latestCli`, and `prevJetbrainsCli`; ask whether to cancel, bump + test, or proceed anyway. |
| `worktree-behind-main` | Explain that prepare tags `origin/main`; refresh the worktree or rely on `pinMain` in the confirmation. |
| `repo-mode-on-main` | Stop. `kilo.cli.pinned=false` is dev-only and must be reset to `true` on `main` before release. |
| `repo-mode-local` | Stop or reset local `kilo.cli.pinned=true`; release checks should run from a releasable local state. |
| `assets-missing` | Stop. The pinned CLI release is incomplete and would fail runtime download. |
Show the resolved JetBrains plugin version, release kind, default `fromTagDefault`, `pinMain`, `latestCli`, `prevJetbrainsCli`, and `assetsOk` to the user, then ask for confirmation before continuing. If the user wants a different CLI pin, use the bump workflow below and do not dispatch prepare until the bump is merged to `main`.
## Bump the CLI Pin
Use this only when the user wants to test or release with a different CLI than `origin/main` currently pins. The helper refuses versions whose GitHub release or runtime assets are missing.
Local test edit only:
```bash
bun -e 'const p=require("./packages/kilo-jetbrains/package.json"); console.log(p.version)'
bun .kilo/skills/release-jetbrains/script/set-pin.ts --latest
# or
bun .kilo/skills/release-jetbrains/script/set-pin.ts --version 7.4.1
```
Verify the matching GitHub Release exists and includes every runtime asset the backend may download:
Then test from `packages/kilo-jetbrains/`:
```bash
cli_version="7.4.1"
gh release view "v${cli_version}" --repo Kilo-Org/kilocode --json assets \
--jq '.assets[].name' | sort
./gradlew typecheck
./gradlew test
```
Expected assets:
If the user confirms the tested pin should be released, open or update a pin bump PR to `main`:
```text
kilo-darwin-arm64.zip
kilo-darwin-x64.zip
kilo-linux-arm64.tar.gz
kilo-linux-x64.tar.gz
kilo-windows-arm64.zip
kilo-windows-x64.zip
```bash
bun .kilo/skills/release-jetbrains/script/set-pin.ts --latest --pr
# or
bun .kilo/skills/release-jetbrains/script/set-pin.ts --version 7.4.1 --pr
```
If the pin is stale or the release assets are missing, stop and ask the user to update `packages/kilo-jetbrains/package.json` on `main` before dispatching prepare. The prepare workflow tags `origin/main`, so the pin must already be reviewed and merged before the release tag is created.
Show the resolved JetBrains plugin version, release kind, default `fromTagDefault`, `kilo.cli.pinned` status, pinned CLI version, and CLI release asset status to the user, then ask for confirmation before continuing.
After that PR merges to `main`, re-run `resolve-version.ts`, re-run `check-pin.ts`, confirm `drift=up-to-date`, then dispatch prepare. Do not dispatch prepare from a local-only pin edit; the prepare workflow tags `origin/main`.
## Prepare Workflow
@@ -208,6 +231,7 @@ Report the Marketplace channel and GitHub Release URL. RC versions publish to th
- If prepare created the tag but failed before creating a PR, rerun prepare for the same version. The existing workflow reuses the tag if it points to the same commit.
- If a tag points to an unexpected SHA, stop and inspect manually. Do not move or delete release tags casually.
- If prepare tagged an unintended CLI pin, do not move the tag. Land the intended pin on `main`, resolve the next JetBrains version, and create a new release tag.
- If release PR checks fail from an apparent flake, use `gh run rerun <run-id> --failed`, then `gh run watch <run-id> --exit-status` before publishing.
- If publish fails after merge, rerun the failed workflow only if Marketplace did not already accept the version.
- If Marketplace succeeds but GitHub Release upload fails, manually create or edit the GitHub Release for `jetbrains/v<version>` using the reviewed changelog.
@@ -0,0 +1,112 @@
#!/usr/bin/env bun
import { $ } from "bun"
import semver from "semver"
import { parseArgs } from "util"
const repo = process.env.GH_REPO ?? process.env.GITHUB_REPOSITORY ?? "Kilo-Org/kilocode"
const asset = [
"kilo-darwin-arm64.zip",
"kilo-darwin-x64.zip",
"kilo-linux-arm64.tar.gz",
"kilo-linux-x64.tar.gz",
"kilo-windows-arm64.zip",
"kilo-windows-x64.zip",
]
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
help: { type: "boolean", short: "h", default: false },
},
})
if (values.help) {
console.log(`
Usage: bun .kilo/skills/release-jetbrains/script/check-pin.ts
Checks the CLI pin that a JetBrains release would lock. Prepare tags origin/main,
so this reads packages/kilo-jetbrains/package.json from origin/main and compares
it with the latest published Kilo CLI release plus the local worktree pin.
Exit codes:
0 Pin is release-ready.
2 Pin drift, repo CLI mode, or missing CLI assets require maintainer review.
`)
process.exit(0)
}
await $`git fetch origin main --tags`.quiet()
const pinMain = JSON.parse(await $`git show origin/main:packages/kilo-jetbrains/package.json`.text()).version as string
const pinLocal = (await Bun.file("packages/kilo-jetbrains/package.json").json()).version as string
const propsMain = await $`git show origin/main:packages/kilo-jetbrains/gradle.properties`.text()
const propsLocal = await Bun.file("packages/kilo-jetbrains/gradle.properties").text()
const pinnedMain = pinned(propsMain)
const pinnedLocal = pinned(propsLocal)
const latestCli = await latest()
const prevJetbrainsCli = await previous()
const missingAssets = await missing(pinMain)
const assetsOk = missingAssets.length === 0
const drift = (() => {
if (!pinnedMain) return "repo-mode-on-main"
if (!pinnedLocal) return "repo-mode-local"
if (pinLocal !== pinMain) return "worktree-behind-main"
if (!assetsOk) return "assets-missing"
if (latestCli && semver.lt(pinMain, latestCli)) return "behind"
return "up-to-date"
})()
console.log(JSON.stringify({
pinMain,
pinLocal,
latestCli,
prevJetbrainsCli,
pinnedMain,
pinnedLocal,
assetsOk,
missingAssets,
drift,
}, null, 2))
if (drift !== "up-to-date") process.exit(2)
async function latest() {
const list = (await $`gh release list --repo ${repo} --limit 100 --json tagName,isDraft,isPrerelease`.json()) as {
tagName: string
isDraft: boolean
isPrerelease: boolean
}[]
return list
.filter((item) => /^v\d+\.\d+\.\d+$/.test(item.tagName) && !item.isDraft && !item.isPrerelease)
.map((item) => item.tagName.slice(1))
.sort(semver.rcompare)[0] ?? null
}
async function previous() {
const text = await $`git tag --list ${"jetbrains/v*"}`.text()
const tag = text
.split(/\r?\n/)
.map((item) => item.trim())
.filter(Boolean)
.map((tag) => ({ tag, version: tag.replace(/^jetbrains\/v/, "") }))
.filter((item) => semver.valid(item.version))
.sort((a, b) => semver.rcompare(a.version, b.version))[0]?.tag
if (!tag) return null
const res = await $`git show ${tag}:packages/kilo-jetbrains/package.json`.nothrow().text()
if (!res.trim()) return null
return JSON.parse(res).version as string
}
async function missing(version: string) {
const res = await $`gh release view ${`v${version}`} --repo ${repo} --json assets --jq ${".assets[].name"}`.quiet().nothrow()
if (res.exitCode !== 0) return asset
const names = res.stdout.toString().split(/\r?\n/).map((item) => item.trim()).filter(Boolean)
return asset.filter((item) => !names.includes(item))
}
function pinned(text: string) {
const line = text.split(/\r?\n/).find((item) => item.startsWith("kilo.cli.pinned="))
const value = line?.split("=", 2)[1]?.trim().toLowerCase()
return value == null || value === "true"
}
@@ -0,0 +1,140 @@
#!/usr/bin/env bun
import { $ } from "bun"
import semver from "semver"
import { parseArgs } from "util"
const repo = process.env.GH_REPO ?? process.env.GITHUB_REPOSITORY ?? "Kilo-Org/kilocode"
const file = "packages/kilo-jetbrains/package.json"
const asset = [
"kilo-darwin-arm64.zip",
"kilo-darwin-x64.zip",
"kilo-linux-arm64.tar.gz",
"kilo-linux-x64.tar.gz",
"kilo-windows-arm64.zip",
"kilo-windows-x64.zip",
]
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
version: { type: "string" },
latest: { type: "boolean", default: false },
pr: { type: "boolean", default: false },
help: { type: "boolean", short: "h", default: false },
},
})
if (values.help) {
console.log(`
Usage: bun .kilo/skills/release-jetbrains/script/set-pin.ts (--latest | --version <x.y.z>) [--pr]
Without --pr, rewrites ${file} in the local worktree so you can test a CLI pin.
With --pr, opens or updates a PR against main using the GitHub API; prepare tags
origin/main, so the pin bump must merge there before a JetBrains release starts.
Examples:
bun .kilo/skills/release-jetbrains/script/set-pin.ts --latest
bun .kilo/skills/release-jetbrains/script/set-pin.ts --version 7.4.1
bun .kilo/skills/release-jetbrains/script/set-pin.ts --latest --pr
`)
process.exit(0)
}
if (values.latest && values.version) throw new Error("Pass either --latest or --version, not both")
const version = values.latest ? await latest() : values.version?.replace(/^v/, "")
if (!version || !semver.valid(version) || semver.prerelease(version)) {
throw new Error("Pass a stable CLI version with --version x.y.z or use --latest")
}
const miss = await missing(version)
if (miss.length > 0) {
throw new Error(`CLI release v${version} is missing required assets: ${miss.join(", ")}`)
}
if (values.pr) {
await pr(version)
process.exit(0)
}
const pkg = await Bun.file(file).json()
const previous = pkg.version as string
pkg.version = version
await Bun.write(file, `${JSON.stringify(pkg, null, 2)}\n`)
if (previous === version) {
console.log(`${file} already pins CLI v${version}`)
} else {
console.log(`Pinned JetBrains CLI ${previous} -> ${version} in ${file}`)
}
console.log("Test locally with: cd packages/kilo-jetbrains && ./gradlew typecheck && ./gradlew test")
console.log("When satisfied, run this script again with --pr so the bump lands on main before prepare tags it.")
async function pr(version: string) {
await $`git fetch origin main`.quiet()
const branch = `chore/jetbrains-cli-pin-v${version}`
const main = (await $`git rev-parse origin/main`.text()).trim()
const text = await $`git show origin/main:${file}`.text()
const pkg = JSON.parse(text)
const previous = pkg.version as string
if (previous === version) {
console.log(`origin/main already pins CLI v${version}; no PR needed.`)
return
}
pkg.version = version
const body = `${JSON.stringify(pkg, null, 2)}\n`
await ensure(branch, main)
const current = (await $`gh api ${`repos/${repo}/contents/${file}?ref=${branch}`}`.json()) as { sha: string }
await $`gh api --method PUT ${`repos/${repo}/contents/${file}`} -f message=${`chore(jetbrains): bump CLI pin to v${version}`} -f content=${Buffer.from(body).toString("base64")} -f branch=${branch} -f sha=${current.sha}`.quiet()
const title = `chore(jetbrains): bump CLI pin to v${version}`
const desc = [
`Bumps the JetBrains CLI pin from v${previous} to v${version}.`,
"",
"Prepare tags origin/main, so this PR must merge before dispatching a JetBrains release that should lock this CLI.",
"",
"After merging, re-run:",
"",
"```bash",
"bun .kilo/skills/release-jetbrains/script/check-pin.ts",
"```",
].join("\n")
const view = await $`gh pr view ${branch} --repo ${repo} --json url --jq .url`.quiet().nothrow()
if (view.exitCode === 0 && view.stdout.toString().trim()) {
await $`gh pr edit ${branch} --repo ${repo} --title ${title} --body ${desc}`
console.log(view.stdout.toString().trim())
return
}
const url = await $`gh pr create --repo ${repo} --base main --head ${branch} --title ${title} --body ${desc}`.text()
console.log(url.trim())
}
async function latest() {
const list = (await $`gh release list --repo ${repo} --limit 100 --json tagName,isDraft,isPrerelease`.json()) as {
tagName: string
isDraft: boolean
isPrerelease: boolean
}[]
const version = list
.filter((item) => /^v\d+\.\d+\.\d+$/.test(item.tagName) && !item.isDraft && !item.isPrerelease)
.map((item) => item.tagName.slice(1))
.sort(semver.rcompare)[0]
if (!version) throw new Error(`No stable CLI release found in ${repo}`)
return version
}
async function missing(version: string) {
const res = await $`gh release view ${`v${version}`} --repo ${repo} --json assets --jq ${".assets[].name"}`.quiet().nothrow()
if (res.exitCode !== 0) return asset
const names = res.stdout.toString().split(/\r?\n/).map((item) => item.trim()).filter(Boolean)
return asset.filter((item) => !names.includes(item))
}
async function ensure(branch: string, sha: string) {
const ref = `repos/${repo}/git/refs/heads/${branch}`
const exists = await $`gh api ${ref}`.nothrow().quiet()
if (exists.exitCode === 0) {
await $`gh api --method PATCH ${ref} -f sha=${sha} -F force=true`.quiet()
return
}
await $`gh api --method POST ${`repos/${repo}/git/refs`} -f ref=${`refs/heads/${branch}`} -f sha=${sha}`.quiet()
}
+27 -3
View File
@@ -16,6 +16,7 @@
- Service classes ↔ `<applicationService>`/`<projectService>` entries in the corresponding module XML
- `packages/kilo-jetbrains/package.json` version ↔ GitHub CLI release tag consumed by the backend downloader
- `packages/kilo-jetbrains/gradle.properties` `kilo.cli.pinned` ↔ Gradle and release-script gates
- `.kilo/skills/release-jetbrains/script/check-pin.ts` / `set-pin.ts` ↔ release skill and CLI pin documentation
## IntelliJ Platform Source Lookup
@@ -156,9 +157,7 @@ For blocking I/O in coroutines, move the dispatcher switch inside the callee usi
- CLI process spawning, download, extraction, and lifecycle belong in `backend`.
- By default, the plugin does not bundle CLI binaries. At connect time the backend downloads the GitHub Release asset for the version pinned in `packages/kilo-jetbrains/package.json`; `backend` resources include `kilo.properties` with `cli.version` and `cli.pinned` for split-mode RPC and runtime use.
- `kilo.cli.pinned=false` in `gradle.properties` is dev-only repo CLI mode: OpenAPI generation runs `bun run --conditions=browser ./src/index.ts generate` from `packages/opencode/`, and runtime extracts a staged local CLI resource instead of downloading.
- Repo CLI mode requires a local CLI build. Run `./gradlew :backend:buildRepoCli` from `packages/kilo-jetbrains/` or `bun run script/build.ts --single --skip-install` from `packages/opencode/`, then let `:backend:stageRepoCli` bundle the full `dist/@kilocode/cli-<os>-<arch>/bin/` directory.
- Production builds must keep `kilo.cli.pinned=true`; Gradle release mode, release scripts, and `script/build-version.sh` reject repo CLI mode.
- For release questions, use the `release-jetbrains` skill and reference `.kilo/skills/release-jetbrains/SKILL.md`; it verifies the CLI pin before creating immutable `jetbrains/v*` tags.
- For OS and environment checks, prefer IntelliJ Platform classes over raw JVM APIs such as `System.getProperty(...)` or `System.getenv(...)`.
- Detect architecture with `com.intellij.util.system.CpuArch.CURRENT`, not `System.getProperty("os.arch")`.
- Detect OS with `com.intellij.openapi.util.SystemInfo.isMac` / `isLinux` / `isWindows`.
@@ -166,6 +165,31 @@ For blocking I/O in coroutines, move the dispatcher switch inside the callee usi
- Resolve IDE paths with `com.intellij.openapi.application.PathManager` rather than inferring paths from process working directories.
- For packaging/build plumbing, see `script/build.ts` and `backend/build.gradle.kts`.
### CLI Pinning, Unpinning, and Bumping
The JetBrains plugin has two independent CLI controls. Use the commands below directly when asked to change either one; do not hand-edit versions by guesswork.
**Pin mode** (`kilo.cli.pinned` in `packages/kilo-jetbrains/gradle.properties`) controls release CLI vs local repo CLI.
| Ask | Do |
|---|---|
| Unpin / use local repo CLI | Set `kilo.cli.pinned=false`, then run `./gradlew :backend:buildRepoCli` from `packages/kilo-jetbrains/`. `:backend:stageRepoCli` bundles `packages/opencode/dist/@kilocode/cli-<os>-<arch>/bin/`; runtime extracts it instead of downloading. |
| Re-pin / use release CLI | Set `kilo.cli.pinned=true`. This is the default and the only releasable state. |
`kilo.cli.pinned=false` is dev-only: OpenAPI generation runs from local `packages/opencode/` source and the local binary is bundled. Production Gradle builds, `script/build-version.sh`, and the release scripts hard-fail on `false`, so restore `true` before releasing.
**Pinned CLI version** (`packages/kilo-jetbrains/package.json` `version`) controls which GitHub CLI release the plugin downloads and generates the client from. The JetBrains release locks the value already merged to `origin/main`.
| Ask | Do |
|---|---|
| Check whether the CLI pin is current | `bun .kilo/skills/release-jetbrains/script/check-pin.ts` |
| Bump the pin to `<version>` / latest and test locally | `bun .kilo/skills/release-jetbrains/script/set-pin.ts --version <x.y.z>` or `bun .kilo/skills/release-jetbrains/script/set-pin.ts --latest`, then run `./gradlew typecheck && ./gradlew test` from `packages/kilo-jetbrains/`. |
| Land a tested pin bump for release | `bun .kilo/skills/release-jetbrains/script/set-pin.ts --version <x.y.z> --pr` or `bun .kilo/skills/release-jetbrains/script/set-pin.ts --latest --pr`; merge the PR to `main`, then re-run `check-pin.ts` before dispatching prepare. |
`set-pin.ts` refuses versions whose CLI release or runtime assets do not exist, so it cannot create a pin that would 404 during runtime download.
For the full release process (resolve version, pin verification, prepare, changelog, publish), load the `release-jetbrains` skill: `.kilo/skills/release-jetbrains/SKILL.md`.
### Server Protocol
- The plugin spawns `kilo serve --port 0` (OS assigns random port) and reads stdout for `listening on http://...:(\d+)` to discover the port.
+34
View File
@@ -14,6 +14,40 @@ JetBrains plugin builds and runtime downloads use the Kilo Core version pinned i
The skill lives at `.kilo/skills/release-jetbrains/SKILL.md`. It does not move or recreate release tags, and merge permission is only required if the user explicitly asks the skill to merge the release PR automatically.
## CLI Pin Review
The JetBrains plugin has two independent versions:
| Field | Meaning |
|---|---|
| `packages/kilo-jetbrains/package.json` `version` | The pinned Kilo CLI release used for OpenAPI generation and runtime downloads. |
| `packages/kilo-jetbrains/gradle.properties` `kilo.jetbrains.version` | The JetBrains Marketplace plugin version. |
The prepare workflow tags `origin/main`, so the CLI pin that matters is the one already merged to `main`. Before creating a release tag, run:
```bash
bun .kilo/skills/release-jetbrains/script/check-pin.ts
```
The script reports the CLI that `origin/main` will lock, the latest published stable CLI release, the CLI shipped by the latest `jetbrains/v*` tag, whether `kilo.cli.pinned=true`, and whether all runtime assets exist. Stop before tagging if the pin is behind the latest CLI and you want to test the newer CLI first.
To test a different CLI pin locally:
```bash
bun .kilo/skills/release-jetbrains/script/set-pin.ts --latest
cd packages/kilo-jetbrains
./gradlew typecheck
./gradlew test
```
To land the tested pin on `main` before releasing:
```bash
bun .kilo/skills/release-jetbrains/script/set-pin.ts --latest --pr
```
Merge the generated pin PR first, then re-run `check-pin.ts` and dispatch prepare. Do not dispatch prepare from a local-only pin edit.
## Create Release Tag And PR
1. Open the GitHub Actions workflow: