Merge pull request #8531 from Kilo-Org/fix/opencode-annotation-ci-path

feat(cli): re-apply kilocode_change annotation CI check (lost in force push)
This commit is contained in:
Kirill Kalishev
2026-04-07 16:16:23 -04:00
committed by GitHub
3 changed files with 201 additions and 0 deletions
@@ -0,0 +1,32 @@
name: Check opencode annotations
on:
pull_request:
paths:
- "packages/opencode/**"
- "script/check-opencode-annotations.ts"
- ".github/workflows/check-opencode-annotations.yml"
workflow_dispatch:
jobs:
check-annotations:
name: Check kilocode_change annotations
if: github.repository == 'Kilo-Org/kilocode'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
- name: Check kilocode_change annotations in shared opencode files
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
if [ -n "$BASE_SHA" ]; then
bun run script/check-opencode-annotations.ts --base "$BASE_SHA"
else
echo "No PR base SHA available (workflow_dispatch without PR context) — skipping."
fi
+1
View File
@@ -18,6 +18,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.
## Products
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env bun
/**
* Verifies that every Kilo-specific change in shared packages/opencode/ files
* 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>
*
* A line is "covered" if it:
* - contains // kilocode_change (inline annotation)
* - falls inside a // kilocode_change start/end block (block annotation)
* - is in a file whose first non-empty line is (whole-file annotation)
* // kilocode_change - new file
* - is empty / whitespace-only (skipped)
* - is itself a marker line (auto-covered)
*
* Exempt paths (no markers needed — entirely Kilo-specific):
* - packages/opencode/src/kilocode/**
* - packages/opencode/test/kilocode/**
* - Any path containing "kilocode" in directory or filename
*/
import { spawnSync } from "node:child_process"
import { readFileSync } from "node:fs"
import path from "node:path"
const ROOT = path.resolve(import.meta.dir, "..")
const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx"])
const args = process.argv.slice(2)
const baseIdx = args.indexOf("--base")
const base = baseIdx !== -1 ? args[baseIdx + 1] : "origin/main"
function run(cmd: string, args: string[]) {
const result = spawnSync(cmd, args, { cwd: ROOT, encoding: "utf8" })
return result.stdout?.trim() ?? ""
}
function changedFiles() {
const out = run("git", ["diff", "--name-only", "--diff-filter=AMRT", `${base}...HEAD`, "--", "packages/opencode"])
return out ? out.split("\n").filter(Boolean) : []
}
function isExempt(file: string) {
const norm = file.replaceAll("\\", "/").toLowerCase()
return norm.split("/").some((part) => part.includes("kilocode"))
}
function isSource(file: string) {
return SOURCE_EXTS.has(path.extname(file))
}
function addedLines(file: string): Set<number> {
const diff = run("git", ["diff", "--unified=0", "--diff-filter=AMRT", `${base}...HEAD`, "--", file])
const out = new Set<number>()
for (const line of diff.split("\n")) {
const m = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/)
if (!m) continue
const start = Number(m[1])
const count = m[2] !== undefined ? Number(m[2]) : 1
for (let i = 0; i < count; i++) out.add(start + i)
}
return out
}
function coveredLines(text: string): { lines: string[]; covered: Set<number> } {
const lines = text.split(/\r?\n/)
const covered = new Set<number>()
// Whole-file annotation: first non-empty line is "// kilocode_change - new file"
const first = lines.find((x) => x.trim() !== "")
if (first?.match(/\/\/\s*kilocode_change\s*-\s*new\s*file\b/)) {
for (let i = 1; i <= lines.length; i++) covered.add(i)
return { lines, covered }
}
let block = false
for (let i = 0; i < lines.length; i++) {
const n = i + 1
const line = lines[i] ?? ""
if (line.match(/\/\/\s*kilocode_change\s+start\b/)) {
block = true
covered.add(n)
continue
}
if (line.match(/\/\/\s*kilocode_change\s+end\b/)) {
covered.add(n)
block = false
continue
}
if (block) {
covered.add(n)
continue
}
if (line.match(/\/\/\s*kilocode_change\b/)) covered.add(n)
}
return { lines, covered }
}
// --- main ---
const files = changedFiles().filter((f) => !isExempt(f) && isSource(f))
if (files.length === 0) {
console.log("No shared opencode source files changed — nothing to check.")
process.exit(0)
}
const violations: string[] = []
for (const file of files) {
const nums = addedLines(file)
if (nums.size === 0) continue
const abs = path.join(ROOT, file)
const text = readFileSync(abs, "utf8")
const { lines, covered } = coveredLines(text)
for (const n of nums) {
const line = lines[n - 1] ?? ""
const trim = line.trim()
if (!trim) continue
if (trim.match(/\/\/\s*kilocode_change\b/)) continue
if (!covered.has(n)) violations.push(` ${file}:${n}: ${trim}`)
}
}
if (violations.length === 0) {
console.log("All shared opencode changes are annotated with kilocode_change markers.")
process.exit(0)
}
console.error(
[
"Unannotated Kilo changes found in shared opencode files:",
"",
...violations,
"",
"Every Kilo-specific change in packages/opencode/ must be annotated.",
"",
"Inline (single line):",
" const url = Flag.KILO_MODELS_URL || 'https://models.dev' // kilocode_change",
"",
"Block (multiple lines):",
" // kilocode_change start",
" ...",
" // kilocode_change end",
"",
"New file:",
" // kilocode_change - new file",
"",
"Exempt paths (no markers needed):",
" - packages/opencode/src/kilocode/**",
" - packages/opencode/test/kilocode/**",
" - Any path containing 'kilocode' in the directory or filename",
"",
"See AGENTS.md for details.",
].join("\n"),
)
process.exit(1)