diff --git a/.opencode-version b/.opencode-version new file mode 100644 index 0000000000..9866d34564 --- /dev/null +++ b/.opencode-version @@ -0,0 +1 @@ +v1.14.33 diff --git a/script/upstream/README.md b/script/upstream/README.md index ed37cd2342..2024a11ad6 100644 --- a/script/upstream/README.md +++ b/script/upstream/README.md @@ -243,7 +243,9 @@ Options: --dry-run Show what would change without writing the file ``` -The command finds the newest upstream tag already merged into `HEAD`, reads that upstream version of the file, applies the same branding transforms used by upstream merge automation, strips existing `kilocode_change` markers from the current file, and adds fresh markers around the remaining lines that differ from upstream. +The command finds the newest upstream tag already merged into `HEAD` (read from `.opencode-version` at the repo root, falling back to an `ls-remote` + `merge-base --is-ancestor` walk), reads that upstream version of the file, applies the same branding transforms used by upstream merge automation, strips existing `kilocode_change` markers from the current file, and adds fresh markers around the remaining lines that differ from upstream. + +The `.opencode-version` file is a single-line tag (e.g. `v1.14.33`) recorded by `merge.ts` after every successful upstream merge. Editing it by hand pins the "last merged" tag for the per-file commands above; delete it to fall back to the slower automatic discovery. ### reset-to-upstream.ts diff --git a/script/upstream/merge.ts b/script/upstream/merge.ts index 24587c9ece..bca7a4b497 100644 --- a/script/upstream/merge.ts +++ b/script/upstream/merge.ts @@ -38,6 +38,7 @@ import { transformConflictedScripts, transformAllScripts } from "./transforms/tr import { transformConflictedExtensions, transformAllExtensions } from "./transforms/transform-extensions" import { transformConflictedWeb, transformAllWeb } from "./transforms/transform-web" import { resolveLockFileConflicts, regenerateLockFiles } from "./transforms/lock-files" +import { writeVersion } from "./utils/upstream" interface MergeOptions { version?: string @@ -508,6 +509,11 @@ async function main() { const keepOursResults = await resetToOurs(config.keepOurs, { dryRun: false, verbose: options.verbose }) logger.success(`Reset ${keepOursResults.length} files to Kilo's version`) + // 6k. Record the last merged upstream tag so future automation can find it + // without walking ls-remote + isAncestor for every tag. + const versionFile = await writeVersion(targetVersion.tag) + logger.success(`Recorded ${targetVersion.tag} in ${versionFile.split("/").pop()}`) + // Clean untracked build artifacts from Kilo-specific directories. // These packages don't exist in upstream, so their .gitignore files are absent // on the opencode branch. Artifacts like bin/, out/, .next/ etc. would otherwise diff --git a/script/upstream/utils/upstream.ts b/script/upstream/utils/upstream.ts index 2ab0c1b353..c63425eed4 100644 --- a/script/upstream/utils/upstream.ts +++ b/script/upstream/utils/upstream.ts @@ -15,6 +15,12 @@ import { isAncestor } from "./git" const url = "https://github.com/anomalyco/opencode.git" const workflows = [".github/workflows/publish.yml", ".github/workflows/beta.yml"] +/** + * Repo-relative path of the file that records the last merged upstream tag. + * Single line containing the upstream tag (e.g. `v1.14.33`). + */ +export const versionFile = ".opencode-version" + export async function root() { return (await $`git rev-parse --show-toplevel`.text()).trim() } @@ -39,6 +45,9 @@ export async function remote() { } export async function last(): Promise { + const recorded = await readVersionFile() + if (recorded) return recorded + const source = await remote() info(`Fetching upstream tags from ${source}...`) @@ -53,6 +62,59 @@ export async function last(): Promise { throw new Error("Could not find a merged upstream tag in HEAD") } +/** + * Read the recorded last-merged upstream tag from `.opencode-version`. Returns + * null if the file is missing/empty, or if the recorded tag cannot be resolved + * to a commit (e.g. tags have not been fetched yet). Falls back to the + * isAncestor-based discovery in `last()`. + */ +async function readVersionFile(): Promise { + const repo = await root() + const file = Bun.file(`${repo}/${versionFile}`) + if (!(await file.exists())) return null + + const tag = (await file.text()).trim() + if (!tag) return null + + const version = parseVersion(tag) + if (!version) { + warn(`${versionFile} contains '${tag}' which is not a valid version tag; ignoring`) + return null + } + + const commit = await resolveTag(tag) + if (!commit) return null + + return { version, tag, commit } +} + +async function resolveTag(tag: string): Promise { + const local = await $`git rev-parse --verify --quiet ${tag}^{commit}`.quiet().nothrow() + if (local.exitCode === 0) return local.stdout.toString().trim() + + const source = await remote() + info(`Tag ${tag} not present locally; fetching from ${source}...`) + const fetch = await $`git fetch ${source} tag ${tag} --no-tags`.quiet().nothrow() + if (fetch.exitCode !== 0) { + warn(`Failed to fetch tag ${tag}: ${fetch.stderr.toString()}`) + return null + } + + const after = await $`git rev-parse --verify --quiet ${tag}^{commit}`.quiet().nothrow() + return after.exitCode === 0 ? after.stdout.toString().trim() : null +} + +/** + * Record the merged upstream tag in `.opencode-version` so subsequent runs of + * `last()` resolve instantly without an `ls-remote` walk. + */ +export async function writeVersion(tag: string): Promise { + const repo = await root() + const path = `${repo}/${versionFile}` + await Bun.write(path, `${tag}\n`) + return path +} + export async function versions(source: string): Promise { const result = await $`git ls-remote --tags ${source}`.quiet().nothrow() if (result.exitCode !== 0) throw new Error(`Failed to list upstream tags: ${result.stderr.toString()}`)