mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-24 23:13:35 +08:00
Merge pull request #12047 from Kilo-Org/dark-shield
feat(jetbrains): add repo CLI dev mode
This commit is contained in:
@@ -35,7 +35,9 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inpu
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
issues: write # kilocode_change - label automated JetBrains CLI pin bump PRs
|
||||
packages: write
|
||||
pull-requests: write # kilocode_change - create automated JetBrains CLI pin bump PRs
|
||||
|
||||
jobs:
|
||||
version:
|
||||
|
||||
@@ -38,36 +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.
|
||||
|
||||
Read the pinned CLI version:
|
||||
Run the pin preflight:
|
||||
|
||||
```bash
|
||||
bun -e 'const p=require("./packages/kilo-jetbrains/package.json"); console.log(p.version)'
|
||||
bun .kilo/skills/release-jetbrains/script/check-pin.ts
|
||||
```
|
||||
|
||||
Verify the matching GitHub Release exists and includes every runtime asset the backend may download:
|
||||
The script prints:
|
||||
|
||||
| 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
|
||||
cli_version="7.4.1"
|
||||
gh release view "v${cli_version}" --repo Kilo-Org/kilocode --json assets \
|
||||
--jq '.assets[].name' | sort
|
||||
bun .kilo/skills/release-jetbrains/script/set-pin.ts --latest
|
||||
# or
|
||||
bun .kilo/skills/release-jetbrains/script/set-pin.ts --version 7.4.1
|
||||
```
|
||||
|
||||
Expected assets:
|
||||
Then test from `packages/kilo-jetbrains/`:
|
||||
|
||||
```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
|
||||
./gradlew typecheck
|
||||
./gradlew test
|
||||
```
|
||||
|
||||
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.
|
||||
If the user confirms the tested pin should be released, open or update a pin bump PR to `main`:
|
||||
|
||||
Show the resolved JetBrains plugin version, release kind, default `fromTagDefault`, pinned CLI version, and CLI release asset status to the user, then ask for confirmation before continuing.
|
||||
```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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
@@ -200,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,89 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import semver from "semver"
|
||||
import { parseArgs } from "util"
|
||||
import { latest, missing } from "./pin-common"
|
||||
|
||||
const repo = process.env.GH_REPO ?? process.env.GITHUB_REPOSITORY ?? "Kilo-Org/kilocode"
|
||||
|
||||
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(repo)
|
||||
const prevJetbrainsCli = await previous()
|
||||
const missingAssets = await missing(repo, 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 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
|
||||
}
|
||||
|
||||
function pinned(text: string) {
|
||||
const value = text.split(/\r?\n/).flatMap((line) => {
|
||||
const [key, value] = line.split("=", 2)
|
||||
if (key.trim() !== "kilo.cli.pinned") return []
|
||||
return [value?.trim().toLowerCase()]
|
||||
})[0]
|
||||
return value == null || value === "true"
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { $ } from "bun"
|
||||
import semver from "semver"
|
||||
|
||||
export const assets = [
|
||||
"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",
|
||||
]
|
||||
|
||||
export async function latest(repo: string) {
|
||||
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
|
||||
}
|
||||
|
||||
export async function missing(repo: string, version: string) {
|
||||
const res = await $`gh release view ${`v${version}`} --repo ${repo} --json assets --jq ${".assets[].name"}`.quiet().nothrow()
|
||||
if (res.exitCode !== 0) return assets
|
||||
const names = res.stdout.toString().split(/\r?\n/).map((item) => item.trim()).filter(Boolean)
|
||||
return assets.filter((item) => !names.includes(item))
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import semver from "semver"
|
||||
import { parseArgs } from "util"
|
||||
import { latest, missing } from "./pin-common"
|
||||
|
||||
const repo = process.env.GH_REPO ?? process.env.GITHUB_REPOSITORY ?? "Kilo-Org/kilocode"
|
||||
const file = "packages/kilo-jetbrains/package.json"
|
||||
const label = "jetbrains-cli-pin-bump"
|
||||
|
||||
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(repo) : 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(repo, 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}`
|
||||
await tag(branch)
|
||||
console.log(view.stdout.toString().trim())
|
||||
return
|
||||
}
|
||||
const url = await $`gh pr create --repo ${repo} --base main --head ${branch} --title ${title} --body ${desc}`.text()
|
||||
await tag(branch)
|
||||
console.log(url.trim())
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
async function tag(branch: string) {
|
||||
await $`gh label create ${label} --repo ${repo} --color 1D76DB --description ${"JetBrains pinned CLI version bump"}`.quiet().nothrow()
|
||||
const result = await $`gh pr edit ${branch} --repo ${repo} --add-label ${label}`.quiet().nothrow()
|
||||
if (result.exitCode !== 0) {
|
||||
console.warn(`Warning: failed to add ${label} label to ${branch}`)
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@
|
||||
- `plugin.xml` `<content>` entries ↔ module XML descriptors (`kilo.jetbrains.{shared,frontend,backend}.xml`)
|
||||
- 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
|
||||
|
||||
@@ -154,8 +156,8 @@ For blocking I/O in coroutines, move the dispatcher switch inside the callee usi
|
||||
## CLI Integration
|
||||
|
||||
- CLI process spawning, download, extraction, and lifecycle belong in `backend`.
|
||||
- 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` for split-mode RPC and runtime use.
|
||||
- The generated API client is produced from the pinned release binary by running `kilo generate` during the Gradle OpenAPI generation task.
|
||||
- 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.
|
||||
- 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`.
|
||||
@@ -163,6 +165,33 @@ 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.
|
||||
|
||||
Stable CLI releases also attempt this PR automatically after publishing and label it `jetbrains-cli-pin-bump`. The CLI release workflow logs the PR URL when creation succeeds and logs a warning without failing the release if PR creation fails.
|
||||
|
||||
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.
|
||||
@@ -191,7 +220,8 @@ For blocking I/O in coroutines, move the dispatcher switch inside the callee usi
|
||||
|
||||
- **Marketplace version build**: Use `script/build-version.sh <version>` from `packages/kilo-jetbrains/` to clean, build, sign, and verify the JetBrains Marketplace plugin ZIP. Pass `--skip-verification` only when explicitly needed.
|
||||
- **Test version build**: If the user asks for a JetBrains test build, still require a version and use `script/build-version.sh <version> --skip-signing --skip-verification` from `packages/kilo-jetbrains/` so no signing secrets are needed. Add `--skip-clean` only when the user wants a faster incremental test build.
|
||||
- **Typecheck**: `bun run typecheck` or `./gradlew typecheck` from `packages/kilo-jetbrains/` — compiles all Kotlin sources including the generated API client. A cold build downloads the pinned CLI release via `generateOpenApiSpec` and needs network access; Gradle-cached incremental runs skip the download. It does not bundle per-platform CLI binaries.
|
||||
- **Typecheck**: `bun run typecheck` or `./gradlew typecheck` from `packages/kilo-jetbrains/` — compiles all Kotlin sources including the generated API client. A cold pinned build downloads the pinned CLI release via `generateOpenApiSpec` and needs network access; Gradle-cached incremental runs skip the download. Repo CLI mode (`-Pkilo.cli.pinned=false`) generates the spec from local source and bundles the staged local CLI binary.
|
||||
- **Build local repo CLI for JetBrains dev**: `./gradlew :backend:buildRepoCli` from `packages/kilo-jetbrains/` builds `packages/opencode/dist/@kilocode/cli-<os>-<arch>/bin/`. `stageRepoCli` intentionally does not depend on this task; missing binaries fail with instructions instead of silently starting a slow CLI build.
|
||||
- **Full build**: `bun run build` from `packages/kilo-jetbrains/` (runs Gradle `buildPlugin`).
|
||||
- **Gradle only**: `./gradlew buildPlugin` from `packages/kilo-jetbrains/`.
|
||||
- **Java checks**: Do not run `java -version` as a routine preflight. Gradle commands already fail clearly when Java is missing or incompatible; check Java only when diagnosing that failure mode.
|
||||
@@ -202,7 +232,7 @@ For blocking I/O in coroutines, move the dispatcher switch inside the callee usi
|
||||
|
||||
### CLI/SDK Change Awareness
|
||||
|
||||
- JetBrains runtime behavior depends on the downloaded CLI release pinned by `packages/kilo-jetbrains/package.json`; local `packages/opencode/` changes are not used unless published and pinned.
|
||||
- JetBrains runtime behavior normally depends on the downloaded CLI release pinned by `packages/kilo-jetbrains/package.json`; local `packages/opencode/` changes are used only with `kilo.cli.pinned=false` repo CLI mode.
|
||||
- If there are relevant server/API changes outside `packages/kilo-jetbrains/`, warn the user that JetBrains may need a newly published/pinned CLI release and regenerated SDK artifacts.
|
||||
|
||||
## UI Guidelines
|
||||
|
||||
@@ -10,8 +10,46 @@ Maintainers can use the Kilo `release-jetbrains` skill to drive this process fro
|
||||
|
||||
JetBrains plugin builds and runtime downloads use the Kilo Core version pinned in `packages/kilo-jetbrains/package.json`, so verify that pin points at a published `v<version>` release before creating the release tag.
|
||||
|
||||
`kilo.cli.pinned=false` in `packages/kilo-jetbrains/gradle.properties` is local development mode only. It generates the client from `packages/opencode/` and bundles a locally built CLI into the plugin; production Gradle builds and release scripts fail until the property is restored to `true`.
|
||||
|
||||
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.
|
||||
|
||||
Stable CLI releases also try to open this pin bump PR automatically after publishing. The PR is labeled `jetbrains-cli-pin-bump`. Release publishing does not fail if creating the PR fails; inspect the publish log for either the PR URL or the warning with manual follow-up instructions.
|
||||
|
||||
## Create Release Tag And PR
|
||||
|
||||
1. Open the GitHub Actions workflow:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import normalization.NormalizeOpenApiSpecTask
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.tasks.Exec
|
||||
import org.gradle.api.tasks.WriteProperties
|
||||
|
||||
plugins {
|
||||
@@ -17,6 +19,10 @@ val generatedApi = layout.buildDirectory.dir("generated/openapi/src/main/kotlin"
|
||||
val rawSpec = layout.buildDirectory.file("generated/openapi-spec/openapi.raw.json")
|
||||
val generatedSpec = layout.buildDirectory.file("generated/openapi-spec/openapi.json")
|
||||
val generatedProps = layout.buildDirectory.dir("generated/kilo-props")
|
||||
val generatedCli = layout.buildDirectory.dir("generated/kilo-cli-res")
|
||||
val pinned = providers.gradleProperty("kilo.cli.pinned").map { it.trim().toBoolean() }.orElse(true)
|
||||
val repoCli = pinned.map { !it }
|
||||
val repoRootDir = rootProject.layout.projectDirectory.dir("../opencode")
|
||||
|
||||
val pinnedCliVersion = providers.fileContents(rootProject.layout.projectDirectory.file("package.json")).asText.map { text ->
|
||||
Regex("\"version\"\\s*:\\s*\"([^\"]+)\"").find(text)?.groupValues?.get(1)
|
||||
@@ -26,6 +32,7 @@ val pinnedCliVersion = providers.fileContents(rootProject.layout.projectDirector
|
||||
sourceSets {
|
||||
main {
|
||||
resources.srcDir(generatedProps)
|
||||
if (repoCli.get()) resources.srcDir(generatedCli)
|
||||
kotlin.srcDir(generatedApi)
|
||||
}
|
||||
}
|
||||
@@ -35,11 +42,14 @@ val writeKiloProperties by tasks.registering(WriteProperties::class) {
|
||||
val out = generatedProps.map { it.file("kilo.properties") }
|
||||
destinationFile.set(out)
|
||||
property("cli.version", pinnedCliVersion)
|
||||
property("cli.pinned", pinned.map { it.toString() })
|
||||
}
|
||||
|
||||
val generateOpenApiSpec by tasks.registering(GenerateOpenApiSpecTask::class) {
|
||||
description = "Generate CLI OpenAPI spec into the build directory"
|
||||
cliVersion.set(pinnedCliVersion)
|
||||
repo.set(repoCli)
|
||||
repoRoot.set(repoRootDir)
|
||||
token.set(
|
||||
providers.environmentVariable("GH_TOKEN")
|
||||
.orElse(providers.environmentVariable("GITHUB_TOKEN"))
|
||||
@@ -48,6 +58,36 @@ val generateOpenApiSpec by tasks.registering(GenerateOpenApiSpecTask::class) {
|
||||
spec.set(rawSpec)
|
||||
}
|
||||
|
||||
val buildRepoCli by tasks.registering(Exec::class) {
|
||||
description = "Build the local repo CLI for the current platform"
|
||||
workingDir = repoRootDir.asFile
|
||||
commandLine("bun", "run", "script/build.ts", "--single", "--skip-install")
|
||||
}
|
||||
|
||||
fun platform(): String {
|
||||
val os = System.getProperty("os.name").lowercase()
|
||||
val name = when {
|
||||
os.contains("mac") || os.contains("darwin") -> "darwin"
|
||||
os.contains("linux") -> "linux"
|
||||
os.contains("windows") -> "windows"
|
||||
else -> throw GradleException("Unsupported OS: ${System.getProperty("os.name")}")
|
||||
}
|
||||
val arch = when (System.getProperty("os.arch").lowercase()) {
|
||||
"aarch64", "arm64" -> "arm64"
|
||||
"x86_64", "amd64" -> "x64"
|
||||
else -> throw GradleException("Unsupported architecture: ${System.getProperty("os.arch")}")
|
||||
}
|
||||
return "$name-$arch"
|
||||
}
|
||||
|
||||
val stageRepoCli by tasks.registering(StageRepoCliTask::class) {
|
||||
description = "Stage the local repo CLI into backend resources"
|
||||
val bin = repoRootDir.dir("dist/@kilocode/cli-${platform()}/bin")
|
||||
this.bin.set(bin)
|
||||
archive.set(generatedCli.map { it.file("kilo-cli.zip") })
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
val normalizeOpenApiSpec by tasks.registering(NormalizeOpenApiSpecTask::class) {
|
||||
description = "Normalize upstream CLI OpenAPI metadata before Kotlin client generation"
|
||||
dependsOn(generateOpenApiSpec)
|
||||
@@ -102,11 +142,13 @@ val fixGeneratedApi by tasks.registering(FixGeneratedApiTask::class) {
|
||||
|
||||
tasks.named("compileKotlin") {
|
||||
dependsOn(fixGeneratedApi, writeKiloProperties)
|
||||
if (repoCli.get()) dependsOn(stageRepoCli)
|
||||
inputs.dir(generatedApi)
|
||||
}
|
||||
|
||||
tasks.named("processResources") {
|
||||
dependsOn(writeKiloProperties)
|
||||
if (repoCli.get()) dependsOn(stageRepoCli)
|
||||
}
|
||||
|
||||
tasks.named("compileTestKotlin") {
|
||||
|
||||
+6
@@ -87,6 +87,12 @@ class KiloBackendCliManager(
|
||||
private suspend fun resolveCli(onProgress: (CliDownload) -> Unit): File {
|
||||
val force = forceExtract
|
||||
forceExtract = false
|
||||
if (!KiloProps.pinned()) {
|
||||
if (force) log.info("Force re-extracting local repo CLI ${KiloProps.cliVersion()}")
|
||||
val cli = KiloRepoCli.extract(force)
|
||||
onProgress(CliDownload(100, KiloProps.cliVersion(), KiloCliPlatform.current()))
|
||||
return cli
|
||||
}
|
||||
if (force) log.info("Force re-downloading CLI ${KiloProps.cliVersion()}")
|
||||
return KiloCliDownloader(log = log).resolve(KiloProps.cliVersion(), force, onProgress)
|
||||
}
|
||||
|
||||
@@ -13,4 +13,8 @@ object KiloProps {
|
||||
|
||||
fun cliVersion(): String = props.getProperty("cli.version")
|
||||
?: throw IllegalStateException("cli.version missing from kilo.properties")
|
||||
|
||||
fun pinned(): Boolean = pinned(props)
|
||||
|
||||
internal fun pinned(props: Properties): Boolean = props.getProperty("cli.pinned")?.toBoolean() ?: true
|
||||
}
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package ai.kilocode.backend.cli
|
||||
|
||||
import com.intellij.openapi.application.PathManager
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
object KiloRepoCli {
|
||||
suspend fun extract(force: Boolean): File = extract(
|
||||
force = force,
|
||||
root = File(PathManager.getSystemPath(), "kilo/repo-cli"),
|
||||
source = {
|
||||
KiloRepoCli::class.java.classLoader.getResourceAsStream("kilo-cli.zip")
|
||||
?: throw IllegalStateException("kilo-cli.zip resource not found; rebuild with kilo.cli.pinned=false")
|
||||
},
|
||||
)
|
||||
|
||||
internal suspend fun extract(force: Boolean, root: File, source: () -> InputStream): File = withContext(Dispatchers.IO) {
|
||||
val exe = File(root, "bin/${KiloCliPlatform.exe()}")
|
||||
val done = File(root, ".complete")
|
||||
if (!force && done.isFile && exe.isFile) {
|
||||
if (!SystemInfo.isWindows) exe.setExecutable(true)
|
||||
return@withContext exe
|
||||
}
|
||||
|
||||
if (root.exists() && !root.deleteRecursively()) {
|
||||
throw IllegalStateException("Failed to delete local repo CLI under ${root.absolutePath}")
|
||||
}
|
||||
if (!root.isDirectory && !root.mkdirs()) {
|
||||
throw IllegalStateException("Failed to create local repo CLI directory ${root.absolutePath}")
|
||||
}
|
||||
|
||||
source().use { input ->
|
||||
ZipInputStream(input.buffered()).use { zip ->
|
||||
while (true) {
|
||||
val entry = zip.nextEntry ?: break
|
||||
write(root, entry.name, entry.isDirectory) { out -> zip.copyTo(out) }
|
||||
zip.closeEntry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!exe.isFile) throw IllegalStateException("Local repo CLI archive did not contain bin/${KiloCliPlatform.exe()}")
|
||||
if (!SystemInfo.isWindows) exe.setExecutable(true)
|
||||
done.writeText("ok\n")
|
||||
return@withContext exe
|
||||
}
|
||||
|
||||
private fun write(dir: File, name: String, directory: Boolean, copy: (OutputStream) -> Unit) {
|
||||
val path = if (name.startsWith("bin/")) name else "bin/$name"
|
||||
val target = File(dir, path).canonicalFile
|
||||
val base = dir.canonicalFile
|
||||
if (target != base && !target.path.startsWith(base.path + File.separator)) {
|
||||
throw IllegalStateException("Archive entry escapes target directory: $name")
|
||||
}
|
||||
if (directory) {
|
||||
target.mkdirs()
|
||||
return
|
||||
}
|
||||
target.parentFile.mkdirs()
|
||||
target.outputStream().use(copy)
|
||||
if (!SystemInfo.isWindows && (target.name == "kilo" || target.name == "bwrap")) {
|
||||
target.setExecutable(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package ai.kilocode.backend.cli
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.util.Properties
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContains
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class KiloRepoCliTest {
|
||||
@TempDir
|
||||
lateinit var dir: File
|
||||
|
||||
@Test
|
||||
fun `extracts cached repo cli and force re-extracts`() = runBlocking {
|
||||
val first = archive("#!/bin/old\n")
|
||||
val next = archive("#!/bin/new\n")
|
||||
val cli = KiloRepoCli.extract(false, dir) { ByteArrayInputStream(first) }
|
||||
|
||||
assertTrue(cli.isFile)
|
||||
assertEquals("#!/bin/old\n", cli.readText())
|
||||
assertTrue(File(cli.parentFile, "kilo-sandbox-mutation-worker.js").isFile)
|
||||
assertTrue(File(dir, ".complete").isFile)
|
||||
|
||||
val cached = KiloRepoCli.extract(false, dir) { ByteArrayInputStream(next) }
|
||||
assertEquals(cli.absolutePath, cached.absolutePath)
|
||||
assertEquals("#!/bin/old\n", cached.readText())
|
||||
|
||||
val forced = KiloRepoCli.extract(true, dir) { ByteArrayInputStream(next) }
|
||||
assertEquals(cli.absolutePath, forced.absolutePath)
|
||||
assertEquals("#!/bin/new\n", forced.readText())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rejects archive entries that escape root`() = runBlocking {
|
||||
val ex = assertFailsWith<IllegalStateException> {
|
||||
KiloRepoCli.extract(false, dir) { ByteArrayInputStream(archive(entry = "../../../bad")) }
|
||||
}
|
||||
|
||||
assertContains(ex.message.orEmpty(), "escapes target directory")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pinned defaults true unless explicitly false`() {
|
||||
assertEquals(true, KiloProps.pinned(Properties()))
|
||||
assertEquals(true, KiloProps.pinned(Properties().apply { setProperty("cli.pinned", "true") }))
|
||||
assertEquals(false, KiloProps.pinned(Properties().apply { setProperty("cli.pinned", "false") }))
|
||||
}
|
||||
|
||||
private fun archive(script: String = "#!/bin/sh\n", entry: String = "bin/${KiloCliPlatform.exe()}"): ByteArray {
|
||||
val out = ByteArrayOutputStream()
|
||||
ZipOutputStream(out).use { zip ->
|
||||
zip.putNextEntry(ZipEntry(entry))
|
||||
zip.write(script.toByteArray())
|
||||
zip.closeEntry()
|
||||
zip.putNextEntry(ZipEntry("bin/kilo-sandbox-mutation-worker.js"))
|
||||
zip.write("worker".toByteArray())
|
||||
zip.closeEntry()
|
||||
}
|
||||
return out.toByteArray()
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,12 @@ abstract class GenerateOpenApiSpecTask : DefaultTask() {
|
||||
@get:Input
|
||||
abstract val cliVersion: Property<String>
|
||||
|
||||
@get:Input
|
||||
abstract val repo: Property<Boolean>
|
||||
|
||||
@get:Internal
|
||||
abstract val repoRoot: DirectoryProperty
|
||||
|
||||
@get:Internal
|
||||
abstract val token: Property<String>
|
||||
|
||||
@@ -49,20 +55,51 @@ abstract class GenerateOpenApiSpecTask : DefaultTask() {
|
||||
@get:Inject
|
||||
abstract val exec: ExecOperations
|
||||
|
||||
init {
|
||||
repo.convention(false)
|
||||
outputs.upToDateWhen { !repo.getOrElse(false) }
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
fun run() {
|
||||
if (repo.getOrElse(false)) {
|
||||
generateFromRepo()
|
||||
return
|
||||
}
|
||||
val kilo = resolve()
|
||||
generate(kilo.absolutePath)
|
||||
}
|
||||
|
||||
private fun generateFromRepo() {
|
||||
val root = repoRoot.asFile.get()
|
||||
val out = ByteArrayOutputStream()
|
||||
val err = ByteArrayOutputStream()
|
||||
val result = exec.exec {
|
||||
commandLine(kilo.absolutePath, "generate")
|
||||
workingDir = root
|
||||
commandLine("bun", "run", "--conditions=browser", "./src/index.ts", "generate")
|
||||
standardOutput = out
|
||||
errorOutput = err
|
||||
isIgnoreExitValue = true
|
||||
}
|
||||
if (result.exitValue != 0) {
|
||||
writeSpec(result.exitValue, out, err)
|
||||
}
|
||||
|
||||
private fun generate(kilo: String) {
|
||||
val out = ByteArrayOutputStream()
|
||||
val err = ByteArrayOutputStream()
|
||||
val result = exec.exec {
|
||||
commandLine(kilo, "generate")
|
||||
standardOutput = out
|
||||
errorOutput = err
|
||||
isIgnoreExitValue = true
|
||||
}
|
||||
writeSpec(result.exitValue, out, err)
|
||||
}
|
||||
|
||||
private fun writeSpec(code: Int, out: ByteArrayOutputStream, err: ByteArrayOutputStream) {
|
||||
if (code != 0) {
|
||||
throw GradleException(
|
||||
"kilo generate failed with exit code ${result.exitValue}.\n" +
|
||||
"kilo generate failed with exit code $code.\n" +
|
||||
err.toString(Charsets.UTF_8).take(2000)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.file.DirectoryProperty
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.tasks.Internal
|
||||
import org.gradle.api.tasks.OutputFile
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import java.io.File
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
abstract class StageRepoCliTask : DefaultTask() {
|
||||
@get:Internal
|
||||
abstract val bin: DirectoryProperty
|
||||
|
||||
@get:OutputFile
|
||||
abstract val archive: RegularFileProperty
|
||||
|
||||
@TaskAction
|
||||
fun run() {
|
||||
val dir = bin.asFile.get()
|
||||
val exe = File(dir, exe())
|
||||
if (!exe.isFile) {
|
||||
throw GradleException(
|
||||
"Repo CLI binary not found at ${exe.absolutePath}. Run ./gradlew :backend:buildRepoCli " +
|
||||
"(or bun run script/build.ts --single --skip-install in packages/opencode) first."
|
||||
)
|
||||
}
|
||||
|
||||
val out = archive.get().asFile
|
||||
out.parentFile.mkdirs()
|
||||
ZipOutputStream(out.outputStream().buffered()).use { zip ->
|
||||
dir.walkTopDown()
|
||||
.filter { it.isFile }
|
||||
.forEach { file ->
|
||||
val name = "bin/${file.relativeTo(dir).invariantSeparatorsPath}"
|
||||
zip.putNextEntry(ZipEntry(name))
|
||||
file.inputStream().use { it.copyTo(zip) }
|
||||
zip.closeEntry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun exe() = if (System.getProperty("os.name").lowercase().contains("windows")) "kilo.exe" else "kilo"
|
||||
}
|
||||
@@ -82,6 +82,7 @@ fun gitTag(): String? {
|
||||
}
|
||||
|
||||
val release = providers.gradleProperty("production").map { it.toBoolean() }.orElse(false).get()
|
||||
val pinned = providers.gradleProperty("kilo.cli.pinned").map { it.trim().toBoolean() }.orElse(true).get()
|
||||
val override = providers.gradleProperty("kilo.version").orNull?.trim()?.takeIf { it.isNotEmpty() }
|
||||
val prop = providers.gradleProperty("kilo.jetbrains.version").orNull?.trim()?.takeIf { it.isNotEmpty() }
|
||||
val tag = gitTag()?.removePrefix("jetbrains/v")
|
||||
@@ -89,6 +90,10 @@ val ver = override?.let(::checked) ?: prop?.let(::checked) ?: if (release) check
|
||||
tag ?: error("Missing JetBrains plugin version. Publish builds must set kilo.jetbrains.version or run from a jetbrains/v<version> tag."),
|
||||
) else checked(tag ?: "0.0.0-dev")
|
||||
|
||||
if (release && !pinned) error(
|
||||
"kilo.cli.pinned=false is a dev-only mode and cannot be released. Set kilo.cli.pinned=true before a production/publish build."
|
||||
)
|
||||
|
||||
val channel = providers.gradleProperty("kilo.channel").map { it.trim() }.orElse("default")
|
||||
val splitPort = providers.gradleProperty("kilo.splitModeServerPort").map(::port).orElse(0)
|
||||
val isolated = providers.gradleProperty("kilo.dev.storage.isolated").map { it.toBoolean() }.orElse(false)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
kotlin.stdlib.default.dependency=false
|
||||
kilo.jetbrains.version=7.0.2
|
||||
# When true (default) the JetBrains plugin uses the pinned CLI release from package.json.
|
||||
# Set to false ONLY for local dev: generate the client from local source + bundle the local binary.
|
||||
# false is NOT releasable -- production builds fail unless this is true.
|
||||
kilo.cli.pinned=true
|
||||
org.gradle.configuration-cache=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m
|
||||
|
||||
@@ -86,6 +86,11 @@ if [[ ! -d "$plugin" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -Eq '^[[:space:]]*kilo\.cli\.pinned[[:space:]]*=[[:space:]]*false[[:space:]]*$' "$plugin/gradle.properties"; then
|
||||
echo "kilo.cli.pinned=false is a dev-only mode and cannot be released. Set kilo.cli.pinned=true before building a version." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$sign" == "1" ]]; then
|
||||
for file in "$chain" "$key" "$pass"; do
|
||||
if [[ ! -s "$file" ]]; then
|
||||
|
||||
@@ -42,6 +42,9 @@ if (kind === "stable" && !/^\d+\.\d+\.\d+$/.test(ver)) throw new Error("Stable v
|
||||
if (!semver.valid(ver)) throw new Error(`Invalid semver: ${ver}`)
|
||||
|
||||
await $`git fetch origin main --tags`
|
||||
if (!(await pinned())) {
|
||||
throw new Error("packages/kilo-jetbrains/gradle.properties has kilo.cli.pinned=false; JetBrains releases require kilo.cli.pinned=true")
|
||||
}
|
||||
|
||||
const tag = `jetbrains/v${ver}`
|
||||
const branch = `jetbrains/release/v${ver}`
|
||||
@@ -161,7 +164,7 @@ async function release(from: string, tag: string, sha: string) {
|
||||
}
|
||||
|
||||
async function label(name: string, color: string, desc: string) {
|
||||
const labels = (await $`gh label list --repo ${repo} --json name --limit 1000`.json()) as { name: string }[]
|
||||
const labels: { name: string }[] = await $`gh label list --repo ${repo} --json name --limit 1000`.json()
|
||||
if (labels.some((item) => item.name === name)) return
|
||||
await $`gh label create ${name} --repo ${repo} --color ${color} --description ${desc}`
|
||||
}
|
||||
@@ -214,6 +217,16 @@ async function writeprops(ver: string) {
|
||||
await Bun.write(props, next.endsWith("\n") ? next : `${next}\n`)
|
||||
}
|
||||
|
||||
async function pinned() {
|
||||
const text = await Bun.file(props).text()
|
||||
const value = text.split(/\r?\n/).flatMap((line) => {
|
||||
const [key, raw] = line.split("=", 2)
|
||||
if (key.trim() !== "kilo.cli.pinned") return []
|
||||
return [raw?.trim().toLowerCase()]
|
||||
})[0]
|
||||
return value == null || value === "true"
|
||||
}
|
||||
|
||||
async function writelog(ver: string, entry: string) {
|
||||
const current = await Bun.file(log)
|
||||
.text()
|
||||
|
||||
@@ -39,8 +39,8 @@ type Pull = {
|
||||
state: string
|
||||
}
|
||||
|
||||
const data =
|
||||
(await $`gh pr view ${pr} --repo ${repo} --json body,headRefName,isCrossRepository,labels,mergedAt,mergeCommit,state`.json()) as Pull
|
||||
const data: Pull =
|
||||
await $`gh pr view ${pr} --repo ${repo} --json body,headRefName,isCrossRepository,labels,mergedAt,mergeCommit,state`.json()
|
||||
const labels = new Set(data.labels.map((item) => item.name))
|
||||
if (!labels.has("jetbrains-release")) throw new Error("PR is missing jetbrains-release label")
|
||||
if (data.isCrossRepository) throw new Error("JetBrains release PR must come from this repository")
|
||||
@@ -73,6 +73,9 @@ if (sha !== commit) throw new Error(`${tag} points at ${sha}, expected ${commit}
|
||||
const prop = await props()
|
||||
if (prop !== ver)
|
||||
throw new Error(`packages/kilo-jetbrains/gradle.properties kilo.jetbrains.version is ${prop}, expected ${ver}`)
|
||||
if (!(await pinned())) {
|
||||
throw new Error("packages/kilo-jetbrains/gradle.properties has kilo.cli.pinned=false; JetBrains releases require kilo.cli.pinned=true")
|
||||
}
|
||||
|
||||
const changelog = await Bun.file("packages/kilo-jetbrains/CHANGELOG.md").text()
|
||||
if (!changelog.includes(`## [${ver}]`)) throw new Error(`CHANGELOG.md is missing section for ${ver}`)
|
||||
@@ -117,3 +120,13 @@ async function props() {
|
||||
if (!value) throw new Error("packages/kilo-jetbrains/gradle.properties is missing kilo.jetbrains.version")
|
||||
return value
|
||||
}
|
||||
|
||||
async function pinned() {
|
||||
const text = await Bun.file("packages/kilo-jetbrains/gradle.properties").text()
|
||||
const value = text.split(/\r?\n/).flatMap((line) => {
|
||||
const [key, raw] = line.split("=", 2)
|
||||
if (key.trim() !== "kilo.cli.pinned") return []
|
||||
return [raw?.trim().toLowerCase()]
|
||||
})[0]
|
||||
return value == null || value === "true"
|
||||
}
|
||||
|
||||
@@ -6,6 +6,11 @@ import { fileURLToPath } from "url"
|
||||
|
||||
console.log("=== publishing ===\n")
|
||||
|
||||
// kilocode_change start - keep JetBrains CLI pin reviewable outside CLI release commits
|
||||
const jetbrainsPkg = fileURLToPath(new URL("../packages/kilo-jetbrains/package.json", import.meta.url))
|
||||
const jetbrainsPin = await Bun.file(jetbrainsPkg).text()
|
||||
// kilocode_change end
|
||||
|
||||
// kilocode_change start - consume changesets on the publish runner so changelog
|
||||
// changes are included in the release commit. Previously this ran in the
|
||||
// version job on a separate runner whose workspace was discarded.
|
||||
@@ -42,6 +47,13 @@ const pkgjsons = await Array.fromAsync(
|
||||
).then((arr) => arr.filter((x) => !x.includes("node_modules") && !x.includes("dist")))
|
||||
|
||||
for (const file of pkgjsons) {
|
||||
// kilocode_change start - create a follow-up PR for JetBrains CLI pin bumps
|
||||
if (file === jetbrainsPkg) {
|
||||
console.log("preserved JetBrains CLI pin:", file)
|
||||
await Bun.file(file).write(jetbrainsPin)
|
||||
continue
|
||||
}
|
||||
// kilocode_change end
|
||||
let pkg = await Bun.file(file).text()
|
||||
pkg = pkg.replaceAll(/"version": "[^"]+"/g, `"version": "${Script.version}"`)
|
||||
console.log("updated:", file)
|
||||
@@ -122,3 +134,34 @@ await import(`../packages/kilo-vscode/script/publish.ts`)
|
||||
|
||||
const dir = fileURLToPath(new URL("..", import.meta.url))
|
||||
process.chdir(dir)
|
||||
|
||||
// kilocode_change start - non-blocking JetBrains CLI pin bump PR after stable CLI release
|
||||
await createJetbrainsPinPr()
|
||||
// kilocode_change end
|
||||
|
||||
// kilocode_change start
|
||||
async function createJetbrainsPinPr() {
|
||||
console.log("\n=== jetbrains cli pin bump pr ===\n")
|
||||
if (!Script.release) {
|
||||
console.log("Skipping JetBrains CLI pin bump PR: not a release build")
|
||||
return
|
||||
}
|
||||
if (Script.preview) {
|
||||
console.log(`Skipping JetBrains CLI pin bump PR for pre-release v${Script.version}`)
|
||||
return
|
||||
}
|
||||
const result = await $`bun .kilo/skills/release-jetbrains/script/set-pin.ts --version ${Script.version} --pr`.nothrow()
|
||||
const out = result.stdout.toString().trim()
|
||||
const err = result.stderr.toString().trim()
|
||||
if (result.exitCode === 0) {
|
||||
if (out) console.log(out)
|
||||
const url = out.match(/https:\/\/github\.com\/\S+\/pull\/\d+/)?.[0]
|
||||
if (url) console.log(`::notice title=JetBrains CLI pin bump PR::${url}`)
|
||||
return
|
||||
}
|
||||
console.warn("JetBrains CLI pin bump PR creation failed; release will continue.")
|
||||
if (out) console.warn(out)
|
||||
if (err) console.warn(err)
|
||||
console.warn("::warning title=JetBrains CLI pin bump PR failed::Release completed, but the JetBrains CLI pin bump PR was not created. Check the logs above and create it manually if needed.")
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
Reference in New Issue
Block a user