From 5fa348f3ed6254591d677e868382ba25ef3b0dcb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 15 Jul 2026 00:53:47 +0100 Subject: [PATCH] fix(release): pin max-lines debt to branch merge base --- scripts/check-max-lines-ratchet.mjs | 38 ++++++++----- test/scripts/check-max-lines-ratchet.test.ts | 57 ++++++++++++++++++++ 2 files changed, 82 insertions(+), 13 deletions(-) diff --git a/scripts/check-max-lines-ratchet.mjs b/scripts/check-max-lines-ratchet.mjs index 4765d7231c61..9cccf5bd20c6 100644 --- a/scripts/check-max-lines-ratchet.mjs +++ b/scripts/check-max-lines-ratchet.mjs @@ -276,19 +276,31 @@ function readBaselineAtRef(root, ref) { function resolveDefaultBase(root, staged) { const candidates = staged ? ["HEAD"] : ["origin/main", "HEAD"]; - return ( - candidates.find((ref) => { - try { - execFileSync("git", ["rev-parse", "--verify", ref + "^{commit}"], { - cwd: root, - stdio: "ignore", - }); - return true; - } catch { - return false; - } - }) ?? null - ); + const resolved = candidates.find((ref) => { + try { + execFileSync("git", ["rev-parse", "--verify", ref + "^{commit}"], { + cwd: root, + stdio: "ignore", + }); + return true; + } catch { + return false; + } + }); + if (!resolved || staged || resolved !== "origin/main") { + return resolved ?? null; + } + // A release or long-lived branch owns the suppression debt from its fork. + // Comparing against moving main would turn unrelated debt cleanup into a blocker. + try { + return execFileSync("git", ["merge-base", "HEAD", resolved], { + cwd: root, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + } catch { + return resolved; + } } function writeBaseline(root, entries) { diff --git a/test/scripts/check-max-lines-ratchet.test.ts b/test/scripts/check-max-lines-ratchet.test.ts index 9b9ff7f2a19d..835dfcd2bbec 100644 --- a/test/scripts/check-max-lines-ratchet.test.ts +++ b/test/scripts/check-max-lines-ratchet.test.ts @@ -233,6 +233,63 @@ describe("check-max-lines-ratchet", () => { expect(main(root)).toBe(1); }); + it("compares divergent worktrees against their main merge base", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-max-lines-diverged-")); + tempDirs.push(root); + fs.mkdirSync(path.join(root, "config"), { recursive: true }); + fs.mkdirSync(path.join(root, "src"), { recursive: true }); + fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), "src/a.ts\nsrc/b.ts\n"); + fs.writeFileSync(path.join(root, "src/a.ts"), "/* oxlint-disable max-lines */\n"); + fs.writeFileSync(path.join(root, "src/b.ts"), "/* oxlint-disable max-lines */\n"); + for (const args of [ + ["init"], + ["config", "user.email", "test@example.com"], + ["config", "user.name", "Test"], + ["add", "."], + ["commit", "-m", "base"], + ["branch", "release"], + ]) { + git(root, args); + } + + fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), "src/a.ts\n"); + fs.writeFileSync(path.join(root, "src/b.ts"), "export const b = 1;\n"); + git(root, ["add", "."]); + git(root, ["commit", "-m", "shrink main debt"]); + git(root, ["update-ref", "refs/remotes/origin/main", "HEAD"]); + git(root, ["checkout", "release"]); + + expect(main(root)).toBe(0); + }); + + it("falls back to main when no merge base is available", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-max-lines-disconnected-")); + tempDirs.push(root); + fs.mkdirSync(path.join(root, "config"), { recursive: true }); + fs.mkdirSync(path.join(root, "src"), { recursive: true }); + fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), "src/a.ts\n"); + fs.writeFileSync(path.join(root, "src/a.ts"), "/* oxlint-disable max-lines */\n"); + for (const args of [ + ["init"], + ["config", "user.email", "test@example.com"], + ["config", "user.name", "Test"], + ["add", "."], + ["commit", "-m", "release"], + ["branch", "-m", "release"], + ]) { + git(root, args); + } + git(root, ["checkout", "--orphan", "main"]); + fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), ""); + fs.writeFileSync(path.join(root, "src/a.ts"), "export const a = 1;\n"); + git(root, ["add", "."]); + git(root, ["commit", "-m", "disconnected main"]); + git(root, ["update-ref", "refs/remotes/origin/main", "HEAD"]); + git(root, ["checkout", "release"]); + + expect(main(root)).toBe(1); + }); + it("checks staged content instead of unstaged worktree edits", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-max-lines-staged-")); tempDirs.push(root);