From bb06f4d2ac7b84a4f415eaf5df099ce879e26202 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Wed, 29 Apr 2026 11:50:14 +0200 Subject: [PATCH] fix: skip removed packages during upstream merge --- script/upstream/README.md | 20 +++++---- script/upstream/merge.ts | 7 ++++ script/upstream/transforms/skip-files.test.ts | 16 ++++++++ script/upstream/transforms/skip-files.ts | 41 ++++++++++--------- script/upstream/utils/match.ts | 34 +++++++++++++++ script/upstream/utils/report.test.ts | 17 ++++++++ script/upstream/utils/report.ts | 3 +- 7 files changed, 108 insertions(+), 30 deletions(-) create mode 100644 script/upstream/transforms/skip-files.test.ts create mode 100644 script/upstream/utils/match.ts create mode 100644 script/upstream/utils/report.test.ts diff --git a/script/upstream/README.md b/script/upstream/README.md index 97944397c22..d231d2a4fde 100644 --- a/script/upstream/README.md +++ b/script/upstream/README.md @@ -76,6 +76,7 @@ The merge automation follows this process, applying **all transformations BEFORE - `/opencode-` - Transformed upstream branch 5. **Apply ALL transformations to upstream branch (PRE-MERGE)**: + - Remove files that should not exist in Kilo (`skipFiles`) - Transform package names (opencode-ai -> @kilocode/cli) - Preserve Kilo's versions - Transform i18n files with Kilo branding @@ -161,15 +162,16 @@ Configuration is defined in `utils/config.ts`: The following transforms are applied to the opencode branch before merging: -1. **Package names** - `opencode-ai` -> `@kilocode/cli`, etc. -2. **Versions** - Preserve Kilo's version numbers -3. **i18n files** - OpenCode -> Kilo in user-visible strings -4. **Branding files** - UI components, configs with branding only -5. **Tauri configs** - Desktop app identifiers, names -6. **package.json** - Names, dependencies, Kilo injections -7. **Scripts** - GitHub API references -8. **Extensions** - Zed, etc. -9. **Web/docs** - Documentation files +1. **Skip files** - Remove upstream-only packages/files that should not exist in Kilo +2. **Package names** - `opencode-ai` -> `@kilocode/cli`, etc. +3. **Versions** - Preserve Kilo's version numbers +4. **i18n files** - OpenCode -> Kilo in user-visible strings +5. **Branding files** - UI components, configs with branding only +6. **Tauri configs** - Desktop app identifiers, names +7. **package.json** - Names, dependencies, Kilo injections +8. **Scripts** - GitHub API references +9. **Extensions** - Zed, etc. +10. **Web/docs** - Documentation files ### Post-Merge Strategies diff --git a/script/upstream/merge.ts b/script/upstream/merge.ts index ac80298da96..42489fcbe6a 100644 --- a/script/upstream/merge.ts +++ b/script/upstream/merge.ts @@ -405,6 +405,13 @@ async function main() { // This reduces conflicts by transforming upstream code to Kilo conventions BEFORE merging logger.step(6, 8, "Applying transformations to opencode branch (pre-merge)...") + logger.info("Removing files skipped in Kilo...") + const skips = await skipFiles({ dryRun: false, verbose: options.verbose, force: true }) + const count = skips.filter((r) => r.action === "removed").length + if (count > 0) { + logger.success(`Removed ${count} skipped file(s) from opencode branch`) + } + // 6a. Transform package names (opencode-ai -> @kilocode/cli) logger.info("Transforming package names...") const nameResults = await transformPackageNames({ dryRun: false, verbose: options.verbose }) diff --git a/script/upstream/transforms/skip-files.test.ts b/script/upstream/transforms/skip-files.test.ts new file mode 100644 index 00000000000..e3b1229c02e --- /dev/null +++ b/script/upstream/transforms/skip-files.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from "bun:test" +import { shouldSkip } from "./skip-files" + +test("matches hosted package glob paths", () => { + expect(shouldSkip("packages/web/package.json", ["packages/web/**"])).toBe(true) + expect(shouldSkip("packages/web/src/content/docs/ja/zen.mdx", ["packages/web/**"])).toBe(true) + expect(shouldSkip("packages/console/app/package.json", ["packages/console/**"])).toBe(true) +}) + +test("does not match sibling package glob paths", () => { + expect(shouldSkip("packages/app/package.json", ["packages/web/**"])).toBe(false) +}) + +test("matches extension glob paths", () => { + expect(shouldSkip(".github/VOUCHED.td", [".github/VOUCHED.*"])).toBe(true) +}) diff --git a/script/upstream/transforms/skip-files.ts b/script/upstream/transforms/skip-files.ts index ded740a4cd2..c41a07a4ee7 100644 --- a/script/upstream/transforms/skip-files.ts +++ b/script/upstream/transforms/skip-files.ts @@ -13,6 +13,7 @@ import { $ } from "bun" import { info, success, warn, debug } from "../utils/logger" import { defaultConfig } from "../utils/config" +import { matches } from "../utils/match" export interface SkipResult { file: string @@ -24,30 +25,14 @@ export interface SkipOptions { dryRun?: boolean verbose?: boolean patterns?: string[] + force?: boolean } /** * Check if a file matches any skip patterns */ export function shouldSkip(filePath: string, patterns: string[]): boolean { - return patterns.some((pattern) => { - // Exact match - if (filePath === pattern) return true - - // Regex pattern (e.g., README\.[a-z]+\.md) - if (pattern.startsWith("^") || pattern.includes("\\")) { - const regex = new RegExp(pattern) - return regex.test(filePath) - } - - // Glob-style pattern - if (pattern.includes("*")) { - const regex = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$") - return regex.test(filePath) - } - - return false - }) + return matches(filePath, patterns) } /** @@ -81,6 +66,21 @@ async function getUnmergedFiles(): Promise { .filter((f) => f.length > 0) } +/** + * Get tracked files from the current branch. + */ +async function getTrackedFiles(): Promise { + const result = await $`git ls-files`.quiet().nothrow() + + if (result.exitCode !== 0) return [] + + return result.stdout + .toString() + .trim() + .split("\n") + .filter((f) => f.length > 0) +} + /** * Check if a file exists in a specific git ref */ @@ -125,7 +125,8 @@ export async function skipFiles(options: SkipOptions = {}): Promise part.split("*").map(esc).join("[^/]*")) + .join(".*") + + return new RegExp(`^${source}$`) +} + +export function match(path: string, pattern: string): boolean { + if (path === pattern) return true + + if (pattern.startsWith("^") || pattern.includes("\\")) { + return new RegExp(pattern).test(path) + } + + if (pattern.includes("*")) { + return glob(pattern).test(path) + } + + return false +} + +export function matches(path: string, patterns: string[]): boolean { + return patterns.some((pattern) => match(path, pattern)) +} diff --git a/script/upstream/utils/report.test.ts b/script/upstream/utils/report.test.ts new file mode 100644 index 00000000000..9d0816c51c2 --- /dev/null +++ b/script/upstream/utils/report.test.ts @@ -0,0 +1,17 @@ +import { expect, test } from "bun:test" +import { getRecommendation } from "./report" + +test("recommends skip for hosted package globs", () => { + expect(getRecommendation("packages/web/src/content/docs/ja/zen.mdx", [], ["packages/web/**"]).recommendation).toBe( + "skip", + ) + expect(getRecommendation("packages/console/app/package.json", [], ["packages/console/**"]).recommendation).toBe( + "skip", + ) +}) + +test("does not recommend skip for unrelated packages", () => { + expect(getRecommendation("packages/app/package.json", [], ["packages/web/**"]).recommendation).toBe( + "package-transform", + ) +}) diff --git a/script/upstream/utils/report.ts b/script/upstream/utils/report.ts index eca41714d84..939e44a6d5f 100644 --- a/script/upstream/utils/report.ts +++ b/script/upstream/utils/report.ts @@ -4,6 +4,7 @@ */ import { $ } from "bun" +import { matches } from "./match" export interface ConflictReport { timestamp: string @@ -119,7 +120,7 @@ export function classifyFile(path: string): ConflictFile["type"] { * Check if a file should be skipped (not added from upstream) */ function shouldSkipFile(path: string, skipPatterns: string[]): boolean { - return skipPatterns.some((pattern) => path === pattern || path.includes(pattern)) + return matches(path, skipPatterns) } /**