diff --git a/.kilo/skills/release-jetbrains/SKILL.md b/.kilo/skills/release-jetbrains/SKILL.md index f2d20c4fc5..4eb73e0f07 100644 --- a/.kilo/skills/release-jetbrains/SKILL.md +++ b/.kilo/skills/release-jetbrains/SKILL.md @@ -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 --failed`, then `gh run watch --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` using the reviewed changelog. diff --git a/.kilo/skills/release-jetbrains/script/check-pin.ts b/.kilo/skills/release-jetbrains/script/check-pin.ts new file mode 100644 index 0000000000..adb531b948 --- /dev/null +++ b/.kilo/skills/release-jetbrains/script/check-pin.ts @@ -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" +} diff --git a/.kilo/skills/release-jetbrains/script/set-pin.ts b/.kilo/skills/release-jetbrains/script/set-pin.ts new file mode 100644 index 0000000000..371cf3ddf4 --- /dev/null +++ b/.kilo/skills/release-jetbrains/script/set-pin.ts @@ -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 ) [--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() +} diff --git a/packages/kilo-jetbrains/AGENTS.md b/packages/kilo-jetbrains/AGENTS.md index dd24d8beb6..e7e83f3f4d 100644 --- a/packages/kilo-jetbrains/AGENTS.md +++ b/packages/kilo-jetbrains/AGENTS.md @@ -16,6 +16,7 @@ - Service classes ↔ ``/`` 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--/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--/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 `` / latest and test locally | `bun .kilo/skills/release-jetbrains/script/set-pin.ts --version ` 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 --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. diff --git a/packages/kilo-jetbrains/RELEASING.md b/packages/kilo-jetbrains/RELEASING.md index edb22c417a..a485813629 100644 --- a/packages/kilo-jetbrains/RELEASING.md +++ b/packages/kilo-jetbrains/RELEASING.md @@ -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: