mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-29 18:52:45 +08:00
Merge branch 'main' into markijbema/kilo-opencode-v1.14.30
This commit is contained in:
@@ -37,3 +37,8 @@ jobs:
|
||||
else
|
||||
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
|
||||
|
||||
@@ -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 `.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
|
||||
|
||||
@@ -358,6 +358,7 @@
|
||||
"ai-gateway-provider": "3.1.2",
|
||||
"bonjour-service": "1.3.0",
|
||||
"bun-pty": "0.4.8",
|
||||
"chardet": "2.1.1",
|
||||
"chokidar": "4.0.3",
|
||||
"cli-sound": "1.1.3",
|
||||
"clipboardy": "4.0.0",
|
||||
@@ -376,7 +377,6 @@
|
||||
"iconv-lite": "0.7.2",
|
||||
"ignore": "7.0.5",
|
||||
"immer": "11.1.4",
|
||||
"jschardet": "3.1.4",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"mime-types": "3.0.2",
|
||||
"minimatch": "10.2.5",
|
||||
@@ -3300,8 +3300,6 @@
|
||||
|
||||
"jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="],
|
||||
|
||||
"jschardet": ["jschardet@3.1.4", "", {}, "sha512-/kmVISmrwVwtyYU40iQUOp3SUPk2dhNCMsZBQX0R1/jZ8maaXJ/oZIzUOiyOqcgtLnETFKYChbJ5iDC/eWmFHg=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-1SpPwQiU4XdqJNJhw9Abf2IiBMxv22waspgDTylL2yg=",
|
||||
"aarch64-linux": "sha256-g+wFoSCCfz0lZym83gHvAPj1R9vIpF4/7jK3bUGCMP4=",
|
||||
"aarch64-darwin": "sha256-aGNmAN6RWL+4V6kxckMrOPJIgvhnomwILcNBI27VapA=",
|
||||
"x86_64-darwin": "sha256-KXPaxTzaypPSjBvmUJVICAcjg6XVnG4EKCRFCcUZ3oI="
|
||||
"x86_64-linux": "sha256-BkFDOCEvivFrxKOvMjAIhj3QZwmb9vE/KvCZv5puw6E=",
|
||||
"aarch64-linux": "sha256-sTkfQNjqCNCxmHbpQ8woA6azoiyD37e8Xy7UwB6eNsA=",
|
||||
"aarch64-darwin": "sha256-lKuagsTHqij5C764DM3d+BOe4JKs2KV8XiwSlJItGMg=",
|
||||
"x86_64-darwin": "sha256-g5gZMHLq8feSZHVaCOLGU6Tj2Qw56Zogif3pT9EIlz8="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ Kilo automatically detects the text encoding of each file it reads and preserves
|
||||
- Shift_JIS, EUC-JP, GB2312, Big5, EUC-KR
|
||||
- Windows-1251, KOI8-R
|
||||
- The ISO-8859 family
|
||||
- Other common legacy Latin and CJK encodings detected by [jschardet](https://github.com/aadsm/jschardet) and decoded by [iconv-lite](https://github.com/ashtuchkin/iconv-lite)
|
||||
- Other common legacy Latin and CJK encodings detected by [chardet](https://github.com/runk/node-chardet) and decoded by [iconv-lite](https://github.com/ashtuchkin/iconv-lite)
|
||||
|
||||
New files Kilo creates are always UTF-8 without a BOM. Encoding detection only runs when Kilo reads or overwrites an existing file.
|
||||
|
||||
|
||||
@@ -142,6 +142,7 @@
|
||||
"ai-gateway-provider": "3.1.2",
|
||||
"bonjour-service": "1.3.0",
|
||||
"bun-pty": "0.4.8",
|
||||
"chardet": "2.1.1",
|
||||
"chokidar": "4.0.3",
|
||||
"cli-sound": "1.1.3",
|
||||
"clipboardy": "4.0.0",
|
||||
@@ -160,7 +161,6 @@
|
||||
"iconv-lite": "0.7.2",
|
||||
"ignore": "7.0.5",
|
||||
"immer": "11.1.4",
|
||||
"jschardet": "3.1.4",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"mime-types": "3.0.2",
|
||||
"minimatch": "10.2.5",
|
||||
|
||||
Vendored
-14
@@ -1,14 +0,0 @@
|
||||
declare module "jschardet" {
|
||||
export interface Result {
|
||||
encoding?: string
|
||||
confidence?: number
|
||||
}
|
||||
|
||||
export function detect(input: ArrayLike<number>): Result
|
||||
|
||||
const api: {
|
||||
detect(input: ArrayLike<number>): Result
|
||||
}
|
||||
|
||||
export default api
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readFile, writeFile, mkdir } from "fs/promises"
|
||||
import { readFileSync } from "fs"
|
||||
import { dirname } from "path"
|
||||
import jschardet from "jschardet"
|
||||
import chardet from "chardet"
|
||||
import iconv from "iconv-lite"
|
||||
|
||||
/**
|
||||
@@ -9,9 +9,9 @@ import iconv from "iconv-lite"
|
||||
*
|
||||
* Supported:
|
||||
* - UTF-8 (with or without BOM)
|
||||
* - UTF-16 LE/BE with BOM (detected by jschardet)
|
||||
* - UTF-32 LE/BE with BOM (detected by jschardet)
|
||||
* - Legacy Latin and CJK encodings (detected by jschardet)
|
||||
* - UTF-16 LE/BE with BOM (detected by chardet)
|
||||
* - UTF-32 LE/BE with BOM (detected by chardet)
|
||||
* - Legacy Latin and CJK encodings (detected by chardet)
|
||||
*
|
||||
* Not supported:
|
||||
* - UTF-16 or UTF-32 without BOM (ambiguous, rare)
|
||||
@@ -19,7 +19,7 @@ import iconv from "iconv-lite"
|
||||
* Detection strategy:
|
||||
* 1. If the bytes are valid UTF-8, treat as UTF-8 (tracking the presence of a
|
||||
* BOM so it can be written back).
|
||||
* 2. Otherwise, trust jschardet. jschardet only reports the wide UTF variants
|
||||
* 2. Otherwise, trust chardet. chardet only reports the wide UTF variants
|
||||
* when a BOM is present, which aligns with the contract above.
|
||||
*
|
||||
* iconv-lite's UTF codecs strip BOMs on decode and do not emit them on encode,
|
||||
@@ -33,29 +33,47 @@ export namespace Encoding {
|
||||
* track the "with BOM" case explicitly to round-trip it faithfully.
|
||||
*/
|
||||
export const UTF8_BOM = "utf-8-bom"
|
||||
const UTF8_BOM_BYTES = Buffer.from([0xef, 0xbb, 0xbf])
|
||||
const BOMS = {
|
||||
"utf-8-bom": Buffer.from([0xef, 0xbb, 0xbf]),
|
||||
"utf-16le": Buffer.from([0xff, 0xfe]),
|
||||
"utf-16be": Buffer.from([0xfe, 0xff]),
|
||||
"utf-32le": Buffer.from([0xff, 0xfe, 0x00, 0x00]),
|
||||
"utf-32be": Buffer.from([0x00, 0x00, 0xfe, 0xff]),
|
||||
}
|
||||
|
||||
function startsWith(bytes: Buffer, bom: Buffer, limit: number): boolean {
|
||||
return limit >= bom.length && bytes.subarray(0, bom.length).equals(bom)
|
||||
}
|
||||
|
||||
function hasUtf8Bom(bytes: Buffer): boolean {
|
||||
return bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf
|
||||
return startsWith(bytes, BOMS["utf-8-bom"], bytes.length)
|
||||
}
|
||||
|
||||
/** True if `bytes[0..limit]` starts with a UTF-16 LE or BE byte-order mark. */
|
||||
export function hasUtf16Bom(bytes: Buffer, limit = bytes.length): boolean {
|
||||
if (limit < 2) return false
|
||||
// UTF-32 LE starts with FF FE 00 00, so exclude that to avoid misclassifying it as UTF-16 LE.
|
||||
// UTF-32 LE starts with FF FE 00 00, so exclude it to avoid misclassifying as UTF-16 LE.
|
||||
if (hasUtf32Bom(bytes, limit)) return false
|
||||
return (bytes[0] === 0xff && bytes[1] === 0xfe) || (bytes[0] === 0xfe && bytes[1] === 0xff)
|
||||
return startsWith(bytes, BOMS["utf-16le"], limit) || startsWith(bytes, BOMS["utf-16be"], limit)
|
||||
}
|
||||
|
||||
/** True if `bytes[0..limit]` starts with a UTF-32 LE or BE byte-order mark. */
|
||||
export function hasUtf32Bom(bytes: Buffer, limit = bytes.length): boolean {
|
||||
if (limit < 4) return false
|
||||
const le = bytes[0] === 0xff && bytes[1] === 0xfe && bytes[2] === 0x00 && bytes[3] === 0x00
|
||||
const be = bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0xfe && bytes[3] === 0xff
|
||||
return le || be
|
||||
return startsWith(bytes, BOMS["utf-32le"], limit) || startsWith(bytes, BOMS["utf-32be"], limit)
|
||||
}
|
||||
|
||||
/** Remap jschardet labels to iconv-lite compatible names. */
|
||||
/**
|
||||
* Canonicalize chardet labels to a stable lowercase-hyphenated form.
|
||||
*
|
||||
* iconv-lite already accepts every label chardet emits (e.g. "UTF-16 LE",
|
||||
* "Shift_JIS", "KOI8-R"), so this map is not required to make decode/encode
|
||||
* work. Its job is to give the rest of the codebase a consistent label —
|
||||
* callers compare against `"utf-16le"`, `"windows-1251"`, etc., and should
|
||||
* not have to account for chardet's casing or whitespace conventions.
|
||||
*
|
||||
* (ISO-2022-* is the one family iconv-lite does not support under any
|
||||
* alias; those labels fall through to the `encodingExists` guard in
|
||||
* `detect()` and are rejected to UTF-8.)
|
||||
*/
|
||||
function normalize(name: string): string {
|
||||
const lower = name.toLowerCase().replace(/[^a-z0-9]/g, "")
|
||||
const map: Record<string, string> = {
|
||||
@@ -64,7 +82,6 @@ export namespace Encoding {
|
||||
utf16be: "utf-16be",
|
||||
utf32le: "utf-32le",
|
||||
utf32be: "utf-32be",
|
||||
ascii: "utf-8",
|
||||
iso88591: "iso-8859-1",
|
||||
iso88592: "iso-8859-2",
|
||||
iso88595: "iso-8859-5",
|
||||
@@ -82,13 +99,8 @@ export namespace Encoding {
|
||||
euckr: "euc-kr",
|
||||
iso2022kr: "iso-2022-kr",
|
||||
big5: "big5",
|
||||
gb2312: "gb2312",
|
||||
gb18030: "gb18030",
|
||||
koi8r: "koi8-r",
|
||||
maccyrillic: "x-mac-cyrillic",
|
||||
ibm855: "cp855",
|
||||
ibm866: "cp866",
|
||||
tis620: "tis-620",
|
||||
}
|
||||
return map[lower] ?? name
|
||||
}
|
||||
@@ -105,9 +117,9 @@ export namespace Encoding {
|
||||
export function detect(bytes: Buffer): string {
|
||||
if (bytes.length === 0) return DEFAULT
|
||||
if (isUtf8(bytes)) return hasUtf8Bom(bytes) ? UTF8_BOM : DEFAULT
|
||||
const result = jschardet.detect(bytes)
|
||||
if (!result.encoding) return DEFAULT
|
||||
const enc = normalize(result.encoding)
|
||||
const result = chardet.detect(bytes)
|
||||
if (!result) return DEFAULT
|
||||
const enc = normalize(result)
|
||||
if (!iconv.encodingExists(enc)) return DEFAULT
|
||||
return enc
|
||||
}
|
||||
@@ -124,14 +136,9 @@ export namespace Encoding {
|
||||
// first so we never emit a double BOM when the decoded text already
|
||||
// contains one (e.g. if a tool round-trips content verbatim).
|
||||
const body = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text
|
||||
if (encoding === UTF8_BOM) return Buffer.concat([UTF8_BOM_BYTES, iconv.encode(body, "utf-8")])
|
||||
const lower = encoding.toLowerCase()
|
||||
if (lower === "utf-16le") return Buffer.concat([Buffer.from([0xff, 0xfe]), iconv.encode(body, encoding)])
|
||||
if (lower === "utf-16be") return Buffer.concat([Buffer.from([0xfe, 0xff]), iconv.encode(body, encoding)])
|
||||
if (lower === "utf-32le")
|
||||
return Buffer.concat([Buffer.from([0xff, 0xfe, 0x00, 0x00]), iconv.encode(body, encoding)])
|
||||
if (lower === "utf-32be")
|
||||
return Buffer.concat([Buffer.from([0x00, 0x00, 0xfe, 0xff]), iconv.encode(body, encoding)])
|
||||
const key = encoding === UTF8_BOM ? UTF8_BOM : encoding.toLowerCase()
|
||||
const bom = BOMS[key as keyof typeof BOMS]
|
||||
if (bom) return Buffer.concat([bom, iconv.encode(body, key === UTF8_BOM ? "utf-8" : key)])
|
||||
return iconv.encode(text, encoding)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// by exercising detect/decode/encode/read/write/readSync directly, without
|
||||
// going through the Effect runtime, agent harness, or tool pipeline. They are
|
||||
// cheap, fast, and cover the internal branches (BOM handling, ASCII/UTF-8
|
||||
// normalization, jschardet fallback, unsupported encoding rejection) that the
|
||||
// normalization, chardet fallback, unsupported encoding rejection) that the
|
||||
// integration tests cannot hit deterministically.
|
||||
|
||||
import { describe, expect, test } from "bun:test"
|
||||
@@ -35,10 +35,10 @@ describe("Encoding.detect", () => {
|
||||
expect(Encoding.detect(Buffer.alloc(0))).toBe(Encoding.DEFAULT)
|
||||
})
|
||||
|
||||
test("plain ASCII is normalized to utf-8 (not 'ascii')", () => {
|
||||
// jschardet reports "ascii" for pure-ASCII input; the namespace treats
|
||||
// that as UTF-8 because UTF-8 is an ASCII superset and iconv-lite doesn't
|
||||
// expose an "ascii" label that round-trips identically.
|
||||
test("plain ASCII is reported as utf-8", () => {
|
||||
// Plain ASCII is valid UTF-8, so the detector short-circuits on the
|
||||
// isUtf8 check and never reaches chardet. UTF-8 is an ASCII superset,
|
||||
// so this label round-trips identically through iconv-lite.
|
||||
expect(Encoding.detect(Buffer.from("plain ascii text\n"))).toBe("utf-8")
|
||||
})
|
||||
|
||||
@@ -52,8 +52,8 @@ describe("Encoding.detect", () => {
|
||||
})
|
||||
|
||||
test("BOM-less UTF-8 containing multi-byte chars is not misdetected", () => {
|
||||
// Regression guard: bytes that are valid UTF-8 must skip the jschardet
|
||||
// branch. jschardet has been known to misfire on short CJK samples.
|
||||
// Regression guard: bytes that are valid UTF-8 must skip the chardet
|
||||
// branch. Encoding detectors have been known to misfire on short CJK samples.
|
||||
expect(Encoding.detect(Buffer.from("한글 テスト 中文", "utf-8"))).toBe("utf-8")
|
||||
})
|
||||
|
||||
@@ -231,13 +231,23 @@ describe("Encoding.hasUtf32Bom", () => {
|
||||
})
|
||||
|
||||
describe("Encoding.read / Encoding.readSync / Encoding.write", () => {
|
||||
// chardet is noticeably more conservative than other detectors (jschardet,
|
||||
// ICU) on tiny samples: a 12-byte Shift_JIS phrase collides with the
|
||||
// windows-1252 profile and is misclassified. In practice this is fine,
|
||||
// because the tool pipeline only runs detection on files the agent is about
|
||||
// to read or patch — real source files and documents carry far more than 12
|
||||
// bytes of characteristic content, which is plenty for chardet to lock
|
||||
// onto the right encoding. The short-sample cliff only matters for
|
||||
// synthetic fixtures like this one, so we pad the sample to the same body
|
||||
// of Japanese text the rest of the suite already relies on.
|
||||
const shiftJisSample = "こんにちは、世界!日本語のテストです。"
|
||||
|
||||
test("read detects and decodes Shift_JIS asynchronously", async () => {
|
||||
await tmp(async (dir) => {
|
||||
const filepath = path.join(dir, "sj.txt")
|
||||
const text = "日本語テスト"
|
||||
await fs.writeFile(filepath, iconv.encode(text, "Shift_JIS"))
|
||||
await fs.writeFile(filepath, iconv.encode(shiftJisSample, "Shift_JIS"))
|
||||
const result = await Encoding.read(filepath)
|
||||
expect(result.text).toBe(text)
|
||||
expect(result.text).toBe(shiftJisSample)
|
||||
expect(result.encoding.toLowerCase()).toBe("shift_jis")
|
||||
})
|
||||
})
|
||||
@@ -245,8 +255,7 @@ describe("Encoding.read / Encoding.readSync / Encoding.write", () => {
|
||||
test("readSync mirrors read for the same input", async () => {
|
||||
await tmp(async (dir) => {
|
||||
const filepath = path.join(dir, "sj.txt")
|
||||
const text = "日本語テスト"
|
||||
await fs.writeFile(filepath, iconv.encode(text, "Shift_JIS"))
|
||||
await fs.writeFile(filepath, iconv.encode(shiftJisSample, "Shift_JIS"))
|
||||
const sync = Encoding.readSync(filepath)
|
||||
const async_ = await Encoding.read(filepath)
|
||||
expect(sync).toEqual(async_)
|
||||
|
||||
@@ -107,7 +107,7 @@ describe("Patch encoding preservation", () => {
|
||||
|
||||
test("preserves Shift_JIS encoding through update", async () => {
|
||||
const file = path.join(dir, "jp.txt")
|
||||
// jschardet needs enough characteristic bytes to identify Shift_JIS. A
|
||||
// chardet needs enough characteristic bytes to identify Shift_JIS. A
|
||||
// single 19-byte phrase looks like windows-1252, so the sample is padded
|
||||
// to match the body of Japanese text the tool tests already rely on.
|
||||
const sample = "こんにちは、世界!日本語のテストです。"
|
||||
|
||||
@@ -372,7 +372,7 @@ describe("tool encoding preservation", () => {
|
||||
provideTmpdirInstance((dir) =>
|
||||
Effect.gen(function* () {
|
||||
const filepath = path.join(dir, "doc.txt")
|
||||
// Pad with additional Shift_JIS text so jschardet has enough bytes
|
||||
// Pad with additional Shift_JIS text so chardet has enough bytes
|
||||
// to confidently identify the encoding.
|
||||
const pad = samples.shiftJis + "\n"
|
||||
const original = pad + "日本語\n日本語\n日本語\n" + pad
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bun
|
||||
// kilocode_change - new file
|
||||
|
||||
/**
|
||||
* 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/`) fails CI until the list is updated
|
||||
* deliberately.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
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",
|
||||
])
|
||||
|
||||
// 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 actualActive = new Set(readdirSync(DIR).filter(isWorkflow))
|
||||
|
||||
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} workflows).`)
|
||||
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)
|
||||
Reference in New Issue
Block a user