fix: skip removed packages during upstream merge

This commit is contained in:
Mark IJbema
2026-04-29 11:50:14 +02:00
parent 828755fea4
commit bb06f4d2ac
7 changed files with 108 additions and 30 deletions
+11 -9
View File
@@ -76,6 +76,7 @@ The merge automation follows this process, applying **all transformations BEFORE
- `<author>/opencode-<version>` - 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
+7
View File
@@ -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 })
@@ -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)
})
+21 -20
View File
@@ -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<string[]> {
.filter((f) => f.length > 0)
}
/**
* Get tracked files from the current branch.
*/
async function getTrackedFiles(): Promise<string[]> {
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<SkipResult[]
// Get all files involved in the merge
const stagedFiles = await getUpstreamFiles()
const unmergedFiles = await getUnmergedFiles()
const allFiles = [...new Set([...stagedFiles, ...unmergedFiles])]
const tracked = options.force ? await getTrackedFiles() : []
const allFiles = [...new Set([...stagedFiles, ...unmergedFiles, ...tracked])]
if (allFiles.length === 0) {
info("No files to process")
@@ -138,7 +139,7 @@ export async function skipFiles(options: SkipOptions = {}): Promise<SkipResult[]
if (!shouldSkip(file, patterns)) continue
// Check if file existed in Kilo before merge (HEAD~1 or the merge base)
const existedInKilo = await fileExistsInRef(file, "HEAD")
const existedInKilo = options.force ? false : await fileExistsInRef(file, "HEAD")
if (existedInKilo) {
debug(`Skipping ${file} - exists in Kilo, not removing`)
+34
View File
@@ -0,0 +1,34 @@
/**
* Match repository paths against exact, regex, or simple glob patterns.
*/
function esc(text: string): string {
return text.replace(/[|\\{}()[\]^$+?.]/g, "\\$&")
}
function glob(pattern: string): RegExp {
const source = pattern
.split("**")
.map((part) => 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))
}
+17
View File
@@ -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",
)
})
+2 -1
View File
@@ -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)
}
/**