mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-29 18:52:45 +08:00
fix(script): add worktree mode to annotation check (#11842)
* fix(script): add worktree mode to annotation check * fix(script): limit worktree annotation check to local changes
This commit is contained in:
@@ -97,10 +97,10 @@ registerKiloFeature(app)
|
||||
After editing shared files or marker comments, run:
|
||||
|
||||
```bash
|
||||
bun run script/check-opencode-annotations.ts
|
||||
bun run script/check-opencode-annotations.ts --worktree
|
||||
```
|
||||
|
||||
If the PR uses a non-default comparison base, pass the correct base ref:
|
||||
If checking committed PR changes against a non-default comparison base, pass the correct base ref without `--worktree`:
|
||||
|
||||
```bash
|
||||
bun run script/check-opencode-annotations.ts --base <base-ref>
|
||||
|
||||
@@ -20,7 +20,7 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang
|
||||
- **Knip** (unused exports): `bun run knip` from `packages/kilo-vscode/`. CI runs this — all exported types/functions must be imported somewhere. Remove or unexport unused exports before pushing.
|
||||
- **Source links**: After adding or changing URLs in `packages/kilo-vscode/`, `packages/kilo-vscode/webview-ui/`, or `packages/opencode/src/`, run `bun run script/extract-source-links.ts` from the repo root and commit the updated `packages/kilo-docs/source-links.md`. CI runs this check — the build fails if the file is stale.
|
||||
- **kilocode_change check**: `bun run check-kilocode-change` from `packages/kilo-vscode/`. CI runs this — `kilocode_change` is a marker for upstream merge conflicts and must not appear in `packages/kilo-vscode/` or `packages/kilo-ui/` (these are entirely Kilo Code additions). Remove the markers before pushing.
|
||||
- **opencode annotation check**: `bun run script/check-opencode-annotations.ts` from repo root. CI runs this on PRs touching `packages/opencode/` — every Kilo-specific change in shared opencode files must be annotated with `kilocode_change` markers. Exempt paths (no markers needed): `packages/opencode/src/kilocode/`, `packages/opencode/test/kilocode/`, and any path containing `kilocode` in the name.
|
||||
- **opencode annotation check**: `bun run script/check-opencode-annotations.ts --worktree` from repo root when verifying local agent changes. CI runs `bun run script/check-opencode-annotations.ts` on PRs touching `packages/opencode/` — every Kilo-specific change in shared opencode files must be annotated with `kilocode_change` markers. Exempt paths (no markers needed): `packages/opencode/src/kilocode/`, `packages/opencode/test/kilocode/`, and any path containing `kilocode` in the name.
|
||||
- **Effect facade ratchet**: Do not add runtime-backed Promise facades to shared `packages/opencode/src` Effect services; use service dependencies, `AppRuntime`, or Kilo-owned boundaries. Run `bun run script/check-opencode-promise-facades.ts` when touching service adapters.
|
||||
- **workflow allowlist**: `bun run script/check-workflows.ts` from repo root. CI runs this as part of the annotations workflow — any `.yml` / `.yaml` file added to or removed from `.github/workflows/` must be reflected in the hardcoded list in `script/check-workflows.ts`. Prevents upstream-merged workflows from silently starting to run in our CI.
|
||||
- **Backend/SDK programmatic testing**: see [TESTING.md](./TESTING.md) for spawning the local main-branch backend (`bun dev serve`) and driving it via `curl` — use this instead of `kilo serve` (prod binary) when testing backend fixes.
|
||||
@@ -36,7 +36,7 @@ Before saying an implementation is ready, run the smallest relevant checks that
|
||||
| VS Code extension | From `packages/kilo-vscode/`: `bun run typecheck`, `bun run lint`, `bun run test:unit` or `bun run test` |
|
||||
| Extension build/package | From `packages/kilo-vscode/`: `bun run compile` or `bun run package` when touching build, packaging, SDK, or webview integration paths |
|
||||
| JetBrains plugin | From `packages/kilo-jetbrains/`: `./gradlew typecheck`, `./gradlew test`. Requires Java 21; do not run `java -version` as a routine preflight. Check Java only after a Java-version or missing-Java failure. |
|
||||
| CI-only guards | Run affected guards documented above, such as `bun run knip`, `bun run check-kilocode-change`, `bun run script/check-opencode-annotations.ts`, or source link extraction |
|
||||
| CI/local guards | Run affected guards documented above, such as `bun run knip`, `bun run check-kilocode-change`, `bun run script/check-opencode-annotations.ts --worktree`, or source link extraction |
|
||||
|
||||
Never run root `bun test`; the root script prints `do not run tests from root` and exits with code 1. Use package-level tests instead.
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { spawnSync } from "node:child_process"
|
||||
import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
|
||||
const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".yml", ".yaml", ".toml", ".sh", ".bash", ".zsh"])
|
||||
@@ -83,6 +86,137 @@ function coveredLines(text: string): Set<number> {
|
||||
return covered
|
||||
}
|
||||
|
||||
const SCRIPT = path.resolve(import.meta.dir, "../../../script/check-opencode-annotations.ts")
|
||||
|
||||
function exec(root: string, args: string[]) {
|
||||
const out = spawnSync("git", args, { cwd: root, encoding: "utf8" })
|
||||
if (out.status === 0) return
|
||||
throw new Error(out.stderr || out.stdout || `git ${args.join(" ")} failed`)
|
||||
}
|
||||
|
||||
function repo() {
|
||||
const root = mkdtempSync(path.join(os.tmpdir(), "kilo-annotations-"))
|
||||
mkdirSync(path.join(root, "script"), { recursive: true })
|
||||
mkdirSync(path.join(root, "packages/opencode/src"), { recursive: true })
|
||||
copyFileSync(SCRIPT, path.join(root, "script/check-opencode-annotations.ts"))
|
||||
writeFileSync(path.join(root, "packages/opencode/src/shared.ts"), "export const value = 1\n")
|
||||
exec(root, ["init"])
|
||||
exec(root, ["checkout", "-B", "main"])
|
||||
exec(root, ["add", "."])
|
||||
exec(root, ["-c", "user.name=Kilo", "-c", "user.email=kilo@example.com", "commit", "-m", "init"])
|
||||
exec(root, ["update-ref", "refs/remotes/origin/main", "HEAD"])
|
||||
return root
|
||||
}
|
||||
|
||||
function check(root: string, args: string[] = []) {
|
||||
return spawnSync(process.execPath, ["run", "script/check-opencode-annotations.ts", ...args], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
})
|
||||
}
|
||||
|
||||
// ─── CLI worktree mode ───────────────────────────────────────────────────────
|
||||
|
||||
describe("CLI worktree mode", () => {
|
||||
test("default mode ignores local edits, worktree mode reports them", () => {
|
||||
const root = repo()
|
||||
try {
|
||||
writeFileSync(path.join(root, "packages/opencode/src/shared.ts"), "export const value = 2\n")
|
||||
|
||||
const head = check(root)
|
||||
expect(head.status).toBe(0)
|
||||
expect(head.stdout).toContain("No shared upstream source files changed")
|
||||
|
||||
const local = check(root, ["--worktree"])
|
||||
expect(local.status).toBe(1)
|
||||
expect(local.stderr).toContain("packages/opencode/src/shared.ts:1")
|
||||
expect(local.stderr).toContain("export const value = 2")
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test("worktree mode reports untracked shared source files", () => {
|
||||
const root = repo()
|
||||
try {
|
||||
writeFileSync(path.join(root, "packages/opencode/src/new.ts"), "export const value = 1\n")
|
||||
|
||||
const local = check(root, ["--worktree"])
|
||||
expect(local.status).toBe(1)
|
||||
expect(local.stderr).toContain("packages/opencode/src/new.ts:1")
|
||||
expect(local.stderr).toContain("export const value = 1")
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test("worktree mode reports staged shared source edits", () => {
|
||||
const root = repo()
|
||||
try {
|
||||
writeFileSync(path.join(root, "packages/opencode/src/shared.ts"), "export const value = 4\n")
|
||||
exec(root, ["add", "packages/opencode/src/shared.ts"])
|
||||
|
||||
const local = check(root, ["--worktree"])
|
||||
expect(local.status).toBe(1)
|
||||
expect(local.stderr).toContain("packages/opencode/src/shared.ts:1")
|
||||
expect(local.stderr).toContain("export const value = 4")
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test("worktree mode checks local edits on upstream merge branches", () => {
|
||||
const root = repo()
|
||||
try {
|
||||
exec(root, ["checkout", "-B", "upstream"])
|
||||
writeFileSync(path.join(root, "packages/opencode/src/shared.ts"), "export const value = 2\n")
|
||||
exec(root, ["add", "."])
|
||||
exec(root, ["-c", "user.name=Kilo", "-c", "user.email=kilo@example.com", "commit", "-m", "upstream"])
|
||||
exec(root, ["checkout", "main"])
|
||||
exec(root, ["merge", "--no-ff", "-m", "Merge: upstream opencode", "upstream"])
|
||||
|
||||
const head = check(root)
|
||||
expect(head.status).toBe(0)
|
||||
expect(head.stdout).toContain("Skipping shared upstream annotation check")
|
||||
|
||||
const upstream = check(root, ["--worktree"])
|
||||
expect(upstream.status).toBe(0)
|
||||
expect(upstream.stdout).toContain("No shared upstream source files changed")
|
||||
|
||||
writeFileSync(path.join(root, "packages/opencode/src/shared.ts"), "export const value = 3\n")
|
||||
|
||||
const local = check(root, ["--worktree"])
|
||||
expect(local.status).toBe(1)
|
||||
expect(local.stderr).toContain("packages/opencode/src/shared.ts:1")
|
||||
expect(local.stderr).toContain("export const value = 3")
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test("worktree mode rejects base refs", () => {
|
||||
const root = repo()
|
||||
try {
|
||||
const local = check(root, ["--worktree", "--base", "origin/main"])
|
||||
expect(local.status).toBe(1)
|
||||
expect(local.stderr).toContain("--base cannot be used with --worktree")
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test("unknown arguments fail instead of falling back to default mode", () => {
|
||||
const root = repo()
|
||||
try {
|
||||
const local = check(root, ["--worktre"])
|
||||
expect(local.status).toBe(1)
|
||||
expect(local.stderr).toContain("Unknown argument: --worktre")
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ─── hasMarker tests ──────────────────────────────────────────────────────────
|
||||
|
||||
describe("hasMarker", () => {
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
* is annotated with a kilocode_change marker.
|
||||
*
|
||||
* Usage:
|
||||
* bun run script/check-opencode-annotations.ts # diff against origin/main
|
||||
* bun run script/check-opencode-annotations.ts --base <ref> # diff against <ref>
|
||||
* bun run script/check-opencode-annotations.ts # diff origin/main...HEAD
|
||||
* bun run script/check-opencode-annotations.ts --base <ref> # diff <ref>...HEAD
|
||||
* bun run script/check-opencode-annotations.ts --worktree # diff HEAD..worktree plus untracked files
|
||||
*
|
||||
* A line is "covered" if it:
|
||||
* - contains a kilocode_change marker comment (inline annotation)
|
||||
@@ -54,8 +55,24 @@ const EXEMPT_SCOPES = [
|
||||
]
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const unknown = args.find((arg, i) => arg !== "--base" && arg !== "--worktree" && args[i - 1] !== "--base")
|
||||
if (unknown) {
|
||||
console.error(`Unknown argument: ${unknown}`)
|
||||
process.exit(1)
|
||||
}
|
||||
const baseIdx = args.indexOf("--base")
|
||||
const base = baseIdx !== -1 ? args[baseIdx + 1] : "origin/main"
|
||||
const worktree = args.includes("--worktree")
|
||||
if (worktree && baseIdx !== -1) {
|
||||
console.error("--base cannot be used with --worktree")
|
||||
process.exit(1)
|
||||
}
|
||||
const base = (() => {
|
||||
if (baseIdx === -1) return "origin/main"
|
||||
const ref = args[baseIdx + 1]
|
||||
if (ref && !ref.startsWith("--")) return ref
|
||||
console.error("Missing value for --base")
|
||||
process.exit(1)
|
||||
})()
|
||||
|
||||
function run(cmd: string, args: string[]) {
|
||||
const result = spawnSync(cmd, args, { cwd: ROOT, encoding: "utf8" })
|
||||
@@ -67,11 +84,23 @@ function run(cmd: string, args: string[]) {
|
||||
return result.stdout?.trim() ?? ""
|
||||
}
|
||||
|
||||
function changedFiles() {
|
||||
const out = run("git", ["diff", "--name-only", "--diff-filter=AMRT", `${base}...HEAD`, "--", ...SCOPES])
|
||||
const ref = worktree ? "HEAD" : `${base}...HEAD`
|
||||
|
||||
function lines(out: string) {
|
||||
return out ? out.split("\n").filter(Boolean) : []
|
||||
}
|
||||
|
||||
function untracked(file?: string) {
|
||||
if (!worktree) return []
|
||||
const pathspec = file ? [file] : SCOPES
|
||||
return lines(run("git", ["ls-files", "--others", "--exclude-standard", "--", ...pathspec]))
|
||||
}
|
||||
|
||||
function changedFiles() {
|
||||
const out = run("git", ["diff", "--name-only", "--diff-filter=AMRT", ref, "--", ...SCOPES])
|
||||
return [...new Set([...lines(out), ...untracked()])]
|
||||
}
|
||||
|
||||
function isUpstreamMerge() {
|
||||
const out = run("git", ["log", "--format=%P%x09%s", `${base}..HEAD`])
|
||||
return out.split("\n").some((line) => {
|
||||
@@ -102,8 +131,8 @@ function isSource(file: string) {
|
||||
return content(file).startsWith("#!") // kilocode_change
|
||||
}
|
||||
|
||||
// Parses the unified=0 diff for `file` against `base` and returns:
|
||||
// - added: every added line number on HEAD
|
||||
// Parses the unified=0 diff for `file` against the selected target and returns:
|
||||
// - added: every added line number on the checked version
|
||||
// - revert: true when the file's diff removes any kilocode_change marker.
|
||||
// In that case the changes are reverting Kilo modifications back to the
|
||||
// upstream baseline, so newly added lines (which are restoring upstream
|
||||
@@ -112,8 +141,15 @@ function isSource(file: string) {
|
||||
// hunks than the marker itself, so we use file-level detection rather
|
||||
// than hunk-level to avoid false positives on legitimate reverts.
|
||||
function addedLines(file: string): { added: Set<number>; revert: boolean } {
|
||||
const diff = run("git", ["diff", "--unified=0", "--diff-filter=AMRT", `${base}...HEAD`, "--", file])
|
||||
const added = new Set<number>()
|
||||
if (untracked(file).includes(file)) {
|
||||
const text = content(file)
|
||||
const count = text.split(/\r?\n/).length
|
||||
for (const n of Array.from({ length: count }, (_, i) => i + 1)) added.add(n)
|
||||
return { added, revert: false }
|
||||
}
|
||||
|
||||
const diff = run("git", ["diff", "--unified=0", "--diff-filter=AMRT", ref, "--", file])
|
||||
let revert = false
|
||||
const all = diff.split("\n")
|
||||
|
||||
@@ -208,7 +244,7 @@ function coveredLines(text: string): { lines: string[]; covered: Set<number> } {
|
||||
|
||||
// --- main ---
|
||||
|
||||
if (isUpstreamMerge()) {
|
||||
if (!worktree && isUpstreamMerge()) {
|
||||
console.log("Skipping shared upstream annotation check — upstream merge detected.")
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user