From 34959bfaac6060fbd5d5a39f49d21b2de7a1b7bf Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 06:38:28 +0000 Subject: [PATCH 1/4] ci: guard against upstream workflow additions Hardcodes the list of active and disabled workflows and fails CI when .github/workflows/ drifts. Wired into check-opencode-annotations so we don't add a new workflow file for this. --- .../workflows/check-opencode-annotations.yml | 3 + AGENTS.md | 1 + script/check-workflows.ts | 100 ++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 script/check-workflows.ts diff --git a/.github/workflows/check-opencode-annotations.yml b/.github/workflows/check-opencode-annotations.yml index e1588c0fc44..a9992d14060 100644 --- a/.github/workflows/check-opencode-annotations.yml +++ b/.github/workflows/check-opencode-annotations.yml @@ -37,3 +37,6 @@ jobs: else echo "No PR base SHA available (workflow_dispatch without PR context) — skipping." fi + + - name: Check workflow allowlist + run: bun run script/check-workflows.ts diff --git a/AGENTS.md b/AGENTS.md index 7285909eb25..16a5ae2bdf7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,6 +21,7 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang - **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. +- **workflow allowlist**: `bun run script/check-workflows.ts` from repo root. CI runs this as part of the annotations workflow — any file added to or removed from `.github/workflows/` (or `.github/workflows/disabled/`) 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. ## Quality Checks diff --git a/script/check-workflows.ts b/script/check-workflows.ts new file mode 100644 index 00000000000..8f64eacf794 --- /dev/null +++ b/script/check-workflows.ts @@ -0,0 +1,100 @@ +#!/usr/bin/env bun + +/** + * Guards against accidentally inheriting workflows from upstream opencode. + * + * We regularly merge upstream. When upstream adds a new workflow under + * `.github/workflows/`, it silently starts running in our CI unless we + * explicitly review and accept it. This check makes that decision explicit: + * the list of allowed workflows is hardcoded below, and any drift (added or + * removed file in `.github/workflows/` or `.github/workflows/disabled/`) fails + * CI until the list is updated deliberately. + * + * To accept a new workflow: add its filename to `active` (or `disabled`). + * To drop one: remove its filename from the list. + */ + +import { readdirSync } from "node:fs" +import path from "node:path" + +const ROOT = path.resolve(import.meta.dir, "..") +const DIR = path.join(ROOT, ".github", "workflows") + +// Workflows we have deliberately accepted into CI. Sort alphabetically. +const active = new Set([ + "auto-docs.yml", + "beta.yml", + "check-md-table-padding.yml", + "check-opencode-annotations.yml", + "check-org-member.yml", + "close-issues.yml", + "close-stale-prs.yml", + "containers.yml", + "docs-build.yml", + "docs-check-links.yml", + "duplicate-issues.yml", + "generate.yml", + "nix-eval.yml", + "nix-hashes.yml", + "publish.yml", + "smoke-test.yml", + "source-check-links.yml", + "test-vscode.yml", + "test.yml", + "triage.yml", + "typecheck.yml", + "visual-regression.yml", + "watch-opencode-releases.yml", +]) + +// Workflows we have explicitly disabled. Kept here so that upstream additions +// to `.github/workflows/disabled/` also require a manual review. +const disabled = new Set([ + "compliance-close.yml.disabled", + "daily-issues-recap.yml.disabled", + "daily-pr-recap.yml.disabled", + "kilo.yml.disabled", + "nix-desktop.yml.disabled", + "notify-discord.yml.disabled", + "pr-management.yml.disabled", + "pr-standards.yml.disabled", + "publish-github-action.yml.disabled", + "release-github-action.yml.disabled", + "review.yml.disabled", + "stats.yml.disabled", + "storybook.yml.disabled", + "sync-zed-extension.yml.disabled", +]) + +function diff(expected: Set, actual: Set, label: string) { + const missing = [...expected].filter((f) => !actual.has(f)).sort() + const extra = [...actual].filter((f) => !expected.has(f)).sort() + const errs: string[] = [] + for (const f of extra) { + errs.push( + `unexpected ${label} workflow: ${f} — if this was added intentionally, add it to script/check-workflows.ts`, + ) + } + for (const f of missing) { + errs.push( + `expected ${label} workflow not found: ${f} — if this was removed intentionally, remove it from script/check-workflows.ts`, + ) + } + return errs +} + +const actualActive = new Set(readdirSync(DIR).filter((f) => f.endsWith(".yml"))) +const actualDisabled = new Set(readdirSync(path.join(DIR, "disabled")).filter((f) => f.endsWith(".disabled"))) + +const errs = [...diff(active, actualActive, "active"), ...diff(disabled, actualDisabled, "disabled")] + +if (errs.length === 0) { + console.log(`check-workflows: ok (${actualActive.size} active, ${actualDisabled.size} disabled).`) + process.exit(0) +} + +for (const e of errs) console.error(e) +console.error("") +console.error(`Found ${errs.length} workflow drift issue(s).`) +console.error("This guard prevents upstream-merged workflows from silently running in our CI.") +process.exit(1) From ed973a93c092c6f91bdbefc24decd198f6eaf2f8 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 06:40:25 +0000 Subject: [PATCH 2/4] annotate kilocode_change markers --- .github/workflows/check-opencode-annotations.yml | 2 ++ script/check-workflows.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/check-opencode-annotations.yml b/.github/workflows/check-opencode-annotations.yml index a9992d14060..8f32e264672 100644 --- a/.github/workflows/check-opencode-annotations.yml +++ b/.github/workflows/check-opencode-annotations.yml @@ -38,5 +38,7 @@ jobs: echo "No PR base SHA available (workflow_dispatch without PR context) — skipping." fi + # kilocode_change start - name: Check workflow allowlist run: bun run script/check-workflows.ts + # kilocode_change end diff --git a/script/check-workflows.ts b/script/check-workflows.ts index 8f64eacf794..05d6ad00896 100644 --- a/script/check-workflows.ts +++ b/script/check-workflows.ts @@ -1,4 +1,5 @@ #!/usr/bin/env bun +// kilocode_change - new file /** * Guards against accidentally inheriting workflows from upstream opencode. From 0c21808597e7350ee36fe478b415acb4b170e836 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 06:48:06 +0000 Subject: [PATCH 3/4] fix(check-workflows): also catch .yaml files --- script/check-workflows.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/script/check-workflows.ts b/script/check-workflows.ts index 05d6ad00896..f1e2a03686d 100644 --- a/script/check-workflows.ts +++ b/script/check-workflows.ts @@ -84,8 +84,12 @@ function diff(expected: Set, actual: Set, label: string) { return errs } -const actualActive = new Set(readdirSync(DIR).filter((f) => f.endsWith(".yml"))) -const actualDisabled = new Set(readdirSync(path.join(DIR, "disabled")).filter((f) => f.endsWith(".disabled"))) +// GitHub picks up both .yml and .yaml in .github/workflows/. We list both so +// an upstream `.yaml` addition also shows up as unexpected drift. +const isWorkflow = (f: string) => f.endsWith(".yml") || f.endsWith(".yaml") +const isDisabled = (f: string) => f.endsWith(".disabled") +const actualActive = new Set(readdirSync(DIR).filter(isWorkflow)) +const actualDisabled = new Set(readdirSync(path.join(DIR, "disabled")).filter(isDisabled)) const errs = [...diff(active, actualActive, "active"), ...diff(disabled, actualDisabled, "disabled")] From 37af1bc7516ec8cc107c32b86c7ecbcec1d82da5 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 07:11:04 +0000 Subject: [PATCH 4/4] check-workflows: drop disabled list, only check runnable workflows --- AGENTS.md | 2 +- script/check-workflows.ts | 64 ++++++++++++--------------------------- 2 files changed, 21 insertions(+), 45 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 16a5ae2bdf7..cc5eda0d054 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang - **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. -- **workflow allowlist**: `bun run script/check-workflows.ts` from repo root. CI runs this as part of the annotations workflow — any file added to or removed from `.github/workflows/` (or `.github/workflows/disabled/`) must be reflected in the hardcoded list in `script/check-workflows.ts`. Prevents upstream-merged workflows from silently starting to run in our CI. +- **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. ## Quality Checks diff --git a/script/check-workflows.ts b/script/check-workflows.ts index f1e2a03686d..a7536cd0799 100644 --- a/script/check-workflows.ts +++ b/script/check-workflows.ts @@ -8,10 +8,14 @@ * `.github/workflows/`, it silently starts running in our CI unless we * explicitly review and accept it. This check makes that decision explicit: * the list of allowed workflows is hardcoded below, and any drift (added or - * removed file in `.github/workflows/` or `.github/workflows/disabled/`) fails - * CI until the list is updated deliberately. + * removed file in `.github/workflows/`) fails CI until the list is updated + * deliberately. * - * To accept a new workflow: add its filename to `active` (or `disabled`). + * Only runnable workflows are checked (`.yml` / `.yaml`). Files under + * `.github/workflows/disabled/` are Kilo-specific and can't run, so they're + * not tracked here. + * + * To accept a new workflow: add its filename to `active`. * To drop one: remove its filename from the list. */ @@ -48,53 +52,25 @@ const active = new Set([ "watch-opencode-releases.yml", ]) -// Workflows we have explicitly disabled. Kept here so that upstream additions -// to `.github/workflows/disabled/` also require a manual review. -const disabled = new Set([ - "compliance-close.yml.disabled", - "daily-issues-recap.yml.disabled", - "daily-pr-recap.yml.disabled", - "kilo.yml.disabled", - "nix-desktop.yml.disabled", - "notify-discord.yml.disabled", - "pr-management.yml.disabled", - "pr-standards.yml.disabled", - "publish-github-action.yml.disabled", - "release-github-action.yml.disabled", - "review.yml.disabled", - "stats.yml.disabled", - "storybook.yml.disabled", - "sync-zed-extension.yml.disabled", -]) - -function diff(expected: Set, actual: Set, label: string) { - const missing = [...expected].filter((f) => !actual.has(f)).sort() - const extra = [...actual].filter((f) => !expected.has(f)).sort() - const errs: string[] = [] - for (const f of extra) { - errs.push( - `unexpected ${label} workflow: ${f} — if this was added intentionally, add it to script/check-workflows.ts`, - ) - } - for (const f of missing) { - errs.push( - `expected ${label} workflow not found: ${f} — if this was removed intentionally, remove it from script/check-workflows.ts`, - ) - } - return errs -} - -// GitHub picks up both .yml and .yaml in .github/workflows/. We list both so +// GitHub picks up both .yml and .yaml in .github/workflows/. We accept both so // an upstream `.yaml` addition also shows up as unexpected drift. const isWorkflow = (f: string) => f.endsWith(".yml") || f.endsWith(".yaml") -const isDisabled = (f: string) => f.endsWith(".disabled") const actualActive = new Set(readdirSync(DIR).filter(isWorkflow)) -const actualDisabled = new Set(readdirSync(path.join(DIR, "disabled")).filter(isDisabled)) -const errs = [...diff(active, actualActive, "active"), ...diff(disabled, actualDisabled, "disabled")] +const missing = [...active].filter((f) => !actualActive.has(f)).sort() +const extra = [...actualActive].filter((f) => !active.has(f)).sort() +const errs: string[] = [] +for (const f of extra) { + errs.push(`unexpected workflow: ${f} — if this was added intentionally, add it to script/check-workflows.ts`) +} +for (const f of missing) { + errs.push( + `expected workflow not found: ${f} — if this was removed intentionally, remove it from script/check-workflows.ts`, + ) +} if (errs.length === 0) { - console.log(`check-workflows: ok (${actualActive.size} active, ${actualDisabled.size} disabled).`) + console.log(`check-workflows: ok (${actualActive.size} workflows).`) process.exit(0) }