mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-19 01:51:21 +08:00
feat: add JetBrains release skill
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
---
|
||||
name: release-jetbrains
|
||||
description: Use when releasing the Kilo JetBrains plugin -- resolve a version ("next rc" or explicit), run the prepare workflow, edit and commit a filtered human-readable changelog on the release PR, merge it, and watch publish to completion.
|
||||
---
|
||||
|
||||
# JetBrains Release
|
||||
|
||||
Use this skill when releasing the Kilo JetBrains plugin.
|
||||
|
||||
This skill drives the existing JetBrains release workflows. It must not move, delete, or recreate JetBrains release tags. It must always confirm the resolved version with the user before dispatching the prepare workflow because the prepare workflow creates an immutable `jetbrains/v<version>` tag.
|
||||
|
||||
## Preconditions
|
||||
|
||||
- Run from the repository root.
|
||||
- `gh` must be authenticated for `Kilo-Org/kilocode` with permission to dispatch workflows, edit PRs, merge PRs, and write contents.
|
||||
- Reference `packages/kilo-jetbrains/RELEASING.md` for manual recovery rules.
|
||||
- Do not locally check out the generated release branch. The helper scripts update the release branch through GitHub to avoid disturbing the current worktree.
|
||||
|
||||
## Version Resolution
|
||||
|
||||
Resolve the user's version request:
|
||||
|
||||
```bash
|
||||
bun .kilo/skills/release-jetbrains/script/resolve-version.ts --spec "next rc"
|
||||
```
|
||||
|
||||
Accepted specs:
|
||||
|
||||
| Spec | Meaning |
|
||||
|---|---|
|
||||
| `next rc` | If the latest JetBrains tag is an RC, increment its `rc.n`; otherwise start the next patch RC at `rc.1`. |
|
||||
| `next stable` | If the latest JetBrains tag is an RC, use its base version; otherwise use the next patch stable. |
|
||||
| `x.y.z-rc.n` | Explicit RC release. |
|
||||
| `x.y.z` | Explicit stable release. |
|
||||
|
||||
Show the resolved `version`, `kind`, and default `fromTagDefault` to the user and ask for confirmation before continuing.
|
||||
|
||||
## Prepare Workflow
|
||||
|
||||
After confirmation, dispatch and watch the prepare workflow:
|
||||
|
||||
```bash
|
||||
bun .kilo/skills/release-jetbrains/script/dispatch-prepare.ts --kind rc --version 7.0.1-rc.7
|
||||
```
|
||||
|
||||
Pass a generous Bash timeout, such as `1800000` ms, because the script blocks on `gh run watch --exit-status`. If the shell times out but the workflow is still running, re-attach with:
|
||||
|
||||
```bash
|
||||
bun .kilo/skills/release-jetbrains/script/dispatch-prepare.ts --kind rc --version 7.0.1-rc.7 --run-id <run-id>
|
||||
```
|
||||
|
||||
The script prints `prNumber`, `prUrl`, `runUrl`, and `branch` on success.
|
||||
|
||||
## Changelog Draft
|
||||
|
||||
Create a changelog draft after the prepare PR exists:
|
||||
|
||||
1. Read the PR body with `gh pr view <pr> --json body`.
|
||||
2. Extract `JetBrains-From-Tag`, `JetBrains-Tag`, and `## Generated Notes`.
|
||||
3. Use the release range and path filter as the primary relevance signal:
|
||||
|
||||
```bash
|
||||
git log --oneline <from-tag>..<tag> -- packages/opencode packages/kilo-jetbrains
|
||||
```
|
||||
|
||||
Keep JetBrains and CLI/runtime changes. Drop unrelated VS Code, docs, gateway, telemetry, i18n, desktop, and webview-only changes unless they affect the CLI bundled into the JetBrains plugin.
|
||||
|
||||
Rewrite terse commit or PR titles into user-facing bullets grouped under `### Added`, `### Fixed`, and `### Changed`. Keep the exact generated header format:
|
||||
|
||||
```markdown
|
||||
## [<version>] - <date>
|
||||
```
|
||||
|
||||
Write the editable draft to:
|
||||
|
||||
```text
|
||||
packages/kilo-jetbrains/build/release/<version>-changelog.md
|
||||
```
|
||||
|
||||
Include source context in an HTML comment so it is easy to edit but not shipped:
|
||||
|
||||
```markdown
|
||||
<!-- CONTEXT - deleted automatically on commit. Source PRs in range:
|
||||
- #1234 feat(jetbrains): ... https://github.com/Kilo-Org/kilocode/pull/1234
|
||||
- #1235 fix(cli): ... https://github.com/Kilo-Org/kilocode/pull/1235
|
||||
-->
|
||||
```
|
||||
|
||||
Ask the user to edit the file and confirm when done.
|
||||
|
||||
## Commit Changelog
|
||||
|
||||
After the user confirms the draft is ready, strip the `<!-- CONTEXT ... -->` block into a temporary cleaned file, then commit the cleaned section to the release branch:
|
||||
|
||||
```bash
|
||||
bun .kilo/skills/release-jetbrains/script/update-changelog.ts --version 7.0.1-rc.7 --file /path/to/clean-section.md
|
||||
```
|
||||
|
||||
The script updates `packages/kilo-jetbrains/CHANGELOG.md` on `jetbrains/release/v<version>` through the GitHub contents API and commits with:
|
||||
|
||||
```text
|
||||
docs(jetbrains): edit changelog for v<version>
|
||||
```
|
||||
|
||||
## Approve, Merge, Publish
|
||||
|
||||
Ask the user to approve the release. After approval, merge the PR and watch the publish workflow:
|
||||
|
||||
```bash
|
||||
bun .kilo/skills/release-jetbrains/script/watch-publish.ts --pr <number> --version 7.0.1-rc.7
|
||||
```
|
||||
|
||||
Pass a generous Bash timeout, such as `1800000` ms. If the shell times out, re-attach with:
|
||||
|
||||
```bash
|
||||
bun .kilo/skills/release-jetbrains/script/watch-publish.ts --pr <number> --version 7.0.1-rc.7 --run-id <run-id>
|
||||
```
|
||||
|
||||
Report the Marketplace channel and GitHub Release URL. RC versions publish to the `eap` channel; stable versions publish to the default Marketplace channel.
|
||||
|
||||
## Recovery
|
||||
|
||||
- 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 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,73 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import { parseArgs } from "util"
|
||||
|
||||
const repo = process.env.GH_REPO ?? process.env.GITHUB_REPOSITORY ?? "Kilo-Org/kilocode"
|
||||
const workflow = "prepare-jetbrains-release.yml"
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
kind: { type: "string" },
|
||||
version: { type: "string" },
|
||||
"from-tag": { type: "string" },
|
||||
"run-id": { type: "string" },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
},
|
||||
})
|
||||
|
||||
if (values.help) {
|
||||
console.log(`Usage: bun .kilo/skills/release-jetbrains/script/dispatch-prepare.ts --kind <rc|stable> --version <version> [--from-tag <tag>] [--run-id <id>]`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const kind = values.kind
|
||||
const ver = values.version
|
||||
const branch = `jetbrains/release/v${ver}`
|
||||
|
||||
if (kind !== "rc" && kind !== "stable") throw new Error("--kind must be rc or stable")
|
||||
if (!ver) throw new Error("--version is required")
|
||||
|
||||
const id = values["run-id"] ?? (await dispatch())
|
||||
const url = `https://github.com/${repo}/actions/runs/${id}`
|
||||
|
||||
console.log(`prepareRunId=${id}`)
|
||||
console.log(`runUrl=${url}`)
|
||||
|
||||
await $`gh run watch ${id} --repo ${repo} --exit-status`
|
||||
|
||||
const pr = (await $`gh pr view ${branch} --repo ${repo} --json number,url`.json()) as { number: number; url: string }
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
prNumber: pr.number,
|
||||
prUrl: pr.url,
|
||||
runUrl: url,
|
||||
branch,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
async function dispatch() {
|
||||
const before = new Set((await runs()).map((run) => run.databaseId))
|
||||
const args = ["workflow", "run", workflow, "--repo", repo, "-f", `kind=${kind}`, "-f", `version=${ver}`]
|
||||
if (values["from-tag"]) args.push("-f", `from_tag=${values["from-tag"]}`)
|
||||
await $`gh ${args}`
|
||||
|
||||
for (const _ of Array.from({ length: 60 })) {
|
||||
const run = (await runs()).find((item) => !before.has(item.databaseId))
|
||||
if (run) return String(run.databaseId)
|
||||
await Bun.sleep(1000)
|
||||
}
|
||||
throw new Error(`No new ${workflow} run appeared after dispatch`)
|
||||
}
|
||||
|
||||
async function runs() {
|
||||
return (await $`gh run list --repo ${repo} --workflow ${workflow} --event workflow_dispatch --json databaseId,createdAt,status --limit 10`.json()) as {
|
||||
databaseId: number
|
||||
createdAt: string
|
||||
status: string
|
||||
}[]
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import semver from "semver"
|
||||
import { parseArgs } from "util"
|
||||
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
spec: { type: "string" },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
},
|
||||
})
|
||||
|
||||
if (values.help) {
|
||||
console.log(`Usage: bun .kilo/skills/release-jetbrains/script/resolve-version.ts --spec <next rc|next stable|version>`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const spec = values.spec?.trim().toLowerCase()
|
||||
if (!spec) throw new Error("--spec is required")
|
||||
|
||||
await $`git fetch origin main --tags`.quiet()
|
||||
|
||||
const tags = await list()
|
||||
const hit = explicit(spec) ?? next(spec, tags)
|
||||
const from = base(hit.version, hit.kind, tags)
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
version: hit.version,
|
||||
kind: hit.kind,
|
||||
fromTagDefault: from,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
type Kind = "rc" | "stable"
|
||||
type Tag = { tag: string; version: string }
|
||||
|
||||
function explicit(spec: string) {
|
||||
if (/^\d+\.\d+\.\d+-rc\.\d+$/.test(spec)) return { version: spec, kind: "rc" as Kind }
|
||||
if (/^\d+\.\d+\.\d+$/.test(spec)) return { version: spec, kind: "stable" as Kind }
|
||||
return undefined
|
||||
}
|
||||
|
||||
function next(spec: string, tags: Tag[]) {
|
||||
const latest = tags.sort((a, b) => semver.rcompare(a.version, b.version))[0]
|
||||
if (!latest) throw new Error("No JetBrains release tags found; pass an explicit version")
|
||||
const parsed = semver.parse(latest.version)
|
||||
if (!parsed) throw new Error(`Invalid latest JetBrains tag: ${latest.tag}`)
|
||||
|
||||
if (spec === "next rc") return rc(parsed)
|
||||
if (spec === "next stable") return stable(parsed)
|
||||
throw new Error("--spec must be 'next rc', 'next stable', x.y.z-rc.n, or x.y.z")
|
||||
}
|
||||
|
||||
function rc(ver: semver.SemVer) {
|
||||
const pre = ver.prerelease
|
||||
if (pre[0] === "rc" && typeof pre[1] === "number") {
|
||||
return { version: `${ver.major}.${ver.minor}.${ver.patch}-rc.${pre[1] + 1}`, kind: "rc" as Kind }
|
||||
}
|
||||
return { version: `${ver.major}.${ver.minor}.${ver.patch + 1}-rc.1`, kind: "rc" as Kind }
|
||||
}
|
||||
|
||||
function stable(ver: semver.SemVer) {
|
||||
if (ver.prerelease.length) return { version: `${ver.major}.${ver.minor}.${ver.patch}`, kind: "stable" as Kind }
|
||||
return { version: `${ver.major}.${ver.minor}.${ver.patch + 1}`, kind: "stable" as Kind }
|
||||
}
|
||||
|
||||
function base(ver: string, kind: Kind, tags: Tag[]) {
|
||||
const want = semver.parse(ver)
|
||||
if (!want) throw new Error(`Invalid semver: ${ver}`)
|
||||
const stable = tags
|
||||
.filter((item) => !semver.prerelease(item.version) && semver.lt(item.version, ver))
|
||||
.sort((a, b) => semver.rcompare(a.version, b.version))
|
||||
|
||||
if (kind === "stable") {
|
||||
const hit = stable[0]
|
||||
if (!hit) return null
|
||||
return hit.tag
|
||||
}
|
||||
|
||||
const prerelease = tags
|
||||
.filter((item) => {
|
||||
const parsed = semver.parse(item.version)
|
||||
if (!parsed) return false
|
||||
if (parsed.major !== want.major || parsed.minor !== want.minor || parsed.patch !== want.patch) return false
|
||||
return Boolean(semver.prerelease(item.version)) && semver.lt(item.version, ver)
|
||||
})
|
||||
.sort((a, b) => semver.rcompare(a.version, b.version))
|
||||
|
||||
return (prerelease[0] ?? stable[0])?.tag ?? null
|
||||
}
|
||||
|
||||
async function list() {
|
||||
const text = await $`git tag --list ${"jetbrains/v*"}`.text()
|
||||
return text
|
||||
.split(/\r?\n/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
.map((tag) => ({ tag, version: tag.replace(/^jetbrains\/v/, "") }))
|
||||
.filter((item) => semver.valid(item.version))
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import { parseArgs } from "util"
|
||||
|
||||
const repo = process.env.GH_REPO ?? process.env.GITHUB_REPOSITORY ?? "Kilo-Org/kilocode"
|
||||
const path = "packages/kilo-jetbrains/CHANGELOG.md"
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
version: { type: "string" },
|
||||
file: { type: "string" },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
},
|
||||
})
|
||||
|
||||
if (values.help) {
|
||||
console.log(`Usage: bun .kilo/skills/release-jetbrains/script/update-changelog.ts --version <version> --file <section.md>`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const ver = values.version
|
||||
const file = values.file
|
||||
if (!ver) throw new Error("--version is required")
|
||||
if (!file) throw new Error("--file is required")
|
||||
|
||||
const branch = `jetbrains/release/v${ver}`
|
||||
const section = strip((await Bun.file(file).text()).trim())
|
||||
validate(section, ver)
|
||||
|
||||
const current = (await $`gh api ${`repos/${repo}/contents/${path}`} -f ref=${branch}`.json()) as {
|
||||
content: string
|
||||
encoding: string
|
||||
sha: string
|
||||
}
|
||||
if (current.encoding !== "base64") throw new Error(`Unexpected content encoding: ${current.encoding}`)
|
||||
|
||||
const text = Buffer.from(current.content.replace(/\s/g, ""), "base64").toString("utf8")
|
||||
const pattern = regex(ver)
|
||||
if (!pattern.test(text)) throw new Error(`${path} is missing a section for ${ver} on ${branch}`)
|
||||
|
||||
const next = text.replace(pattern, `\n${section}\n`).replace(/\n{3,}/g, "\n\n").trimEnd() + "\n"
|
||||
if (next === text) {
|
||||
console.log("CHANGELOG.md already contains the provided section")
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const body = {
|
||||
message: `docs(jetbrains): edit changelog for v${ver}`,
|
||||
content: Buffer.from(next).toString("base64"),
|
||||
sha: current.sha,
|
||||
branch,
|
||||
}
|
||||
const tmp = await temp(body)
|
||||
await $`gh api ${`repos/${repo}/contents/${path}`} --method PUT --input ${tmp}`
|
||||
console.log(`Committed changelog update to ${branch}`)
|
||||
|
||||
function strip(text: string) {
|
||||
return text.replace(/<!--\s*CONTEXT[\s\S]*?-->/g, "").trim()
|
||||
}
|
||||
|
||||
function validate(section: string, ver: string) {
|
||||
if (!section.startsWith(`## [${ver}]`)) throw new Error(`Section must start with ## [${ver}]`)
|
||||
if (!/^### (Added|Fixed|Changed)$/m.test(section)) {
|
||||
throw new Error("Section must contain at least one Added, Fixed, or Changed heading")
|
||||
}
|
||||
}
|
||||
|
||||
function regex(ver: string) {
|
||||
const safe = ver.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
return new RegExp(`\n?## \\[${safe}\\][\\s\\S]*?(?=\n## \\[|$)`)
|
||||
}
|
||||
|
||||
async function temp(body: object) {
|
||||
const file = `/tmp/kilo-jetbrains-changelog-${Date.now()}.json`
|
||||
await Bun.write(file, JSON.stringify(body))
|
||||
return file
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import { parseArgs } from "util"
|
||||
|
||||
const repo = process.env.GH_REPO ?? process.env.GITHUB_REPOSITORY ?? "Kilo-Org/kilocode"
|
||||
const workflow = "publish-jetbrains.yml"
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
pr: { type: "string" },
|
||||
version: { type: "string" },
|
||||
"run-id": { type: "string" },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
},
|
||||
})
|
||||
|
||||
if (values.help) {
|
||||
console.log(`Usage: bun .kilo/skills/release-jetbrains/script/watch-publish.ts --pr <number> --version <version> [--run-id <id>]`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const pr = values.pr
|
||||
const ver = values.version
|
||||
if (!pr) throw new Error("--pr is required")
|
||||
if (!ver) throw new Error("--version is required")
|
||||
|
||||
const branch = `jetbrains/release/v${ver}`
|
||||
const id = values["run-id"] ?? (await merge())
|
||||
const url = `https://github.com/${repo}/actions/runs/${id}`
|
||||
|
||||
console.log(`publishRunId=${id}`)
|
||||
console.log(`runUrl=${url}`)
|
||||
|
||||
await $`gh run watch ${id} --repo ${repo} --exit-status`
|
||||
|
||||
const rel = (await $`gh release view ${`jetbrains/v${ver}`} --repo ${repo} --json url,isPrerelease`.json()) as {
|
||||
url: string
|
||||
isPrerelease: boolean
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
version: ver,
|
||||
marketplaceChannel: rel.isPrerelease ? "eap" : "default",
|
||||
releaseUrl: rel.url,
|
||||
runUrl: url,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
async function merge() {
|
||||
const before = new Set((await runs()).map((run) => run.databaseId))
|
||||
await $`gh pr merge ${pr} --repo ${repo} --merge`
|
||||
|
||||
for (const _ of Array.from({ length: 120 })) {
|
||||
const run = (await runs()).find((item) => item.headBranch === branch && !before.has(item.databaseId))
|
||||
if (run) return String(run.databaseId)
|
||||
await Bun.sleep(1000)
|
||||
}
|
||||
throw new Error(`No new ${workflow} run appeared after merging PR ${pr}`)
|
||||
}
|
||||
|
||||
async function runs() {
|
||||
return (await $`gh run list --repo ${repo} --workflow ${workflow} --event pull_request --json databaseId,createdAt,headBranch,status --limit 20`.json()) as {
|
||||
databaseId: number
|
||||
createdAt: string
|
||||
headBranch: string
|
||||
status: string
|
||||
}[]
|
||||
}
|
||||
Reference in New Issue
Block a user