From be95b78ae6a6d4ea6283f07a48814428f87cf66f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Jul 2026 10:15:47 -0700 Subject: [PATCH] fix(release): handle large preparation diffs (#113760) --- scripts/release-prepare.ts | 6 +++++- test/scripts/release-prepare.test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/scripts/release-prepare.ts b/scripts/release-prepare.ts index e269d5193424..a5cd50778da5 100644 --- a/scripts/release-prepare.ts +++ b/scripts/release-prepare.ts @@ -40,6 +40,7 @@ type WorktreeState = { const DEFAULT_JOBS = 4; const MAX_JOBS = 16; +const GIT_OUTPUT_MAX_BUFFER_BYTES = 64 * 1024 * 1024; export function parseReleasePrepareArgs(argv: string[]): ReleasePrepareArgs { let android = false; @@ -298,7 +299,7 @@ export function runReleasePrepareStep( return result.status ?? 1; } -function readWorktreeState(rootDir: string): WorktreeState { +export function readWorktreeState(rootDir: string): WorktreeState { const head = git(rootDir, ["rev-parse", "HEAD"]); const status = git(rootDir, ["status", "--porcelain=v1", "--untracked-files=all"]); const diff = git(rootDir, ["diff", "--binary", "HEAD"]); @@ -334,6 +335,9 @@ function git(cwd: string, args: string[]): string { cwd, encoding: "utf8", env: process.env, + // Version preparation can legitimately create multi-megabyte generated diffs. + // Keep the fingerprint capture bounded without inheriting Node's 1 MiB default. + maxBuffer: GIT_OUTPUT_MAX_BUFFER_BYTES, stdio: ["ignore", "pipe", "pipe"], }); if (result.status !== 0) { diff --git a/test/scripts/release-prepare.test.ts b/test/scripts/release-prepare.test.ts index 6c3953cf0079..506b0a15595b 100644 --- a/test/scripts/release-prepare.test.ts +++ b/test/scripts/release-prepare.test.ts @@ -1,3 +1,7 @@ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; // Release prepare tests cover shadow planning, cutover commands, and candidate manifests. import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it, vi } from "vitest"; @@ -5,6 +9,7 @@ import { buildReleasePreparationManifest, createReleasePrepareSteps, parseReleasePrepareArgs, + readWorktreeState, runReleasePrepareStep, runReleasePrepareSteps, } from "../../scripts/release-prepare.ts"; @@ -159,6 +164,29 @@ describe("release preparation plan", () => { }); describe("release preparation manifest", () => { + it("fingerprints generated diffs larger than Node's default child buffer", () => { + const rootDir = mkdtempSync(path.join(tmpdir(), "openclaw-release-prepare-")); + try { + execFileSync("git", ["init", "-q"], { cwd: rootDir }); + execFileSync("git", ["config", "user.email", "release-test@openclaw.invalid"], { + cwd: rootDir, + }); + execFileSync("git", ["config", "user.name", "OpenClaw Release Test"], { cwd: rootDir }); + writeFileSync(path.join(rootDir, "package.json"), '{"version":"2026.7.2"}\n'); + writeFileSync(path.join(rootDir, "generated.txt"), `${"a".repeat(2 * 1024 * 1024)}\n`); + execFileSync("git", ["add", "."], { cwd: rootDir }); + execFileSync("git", ["commit", "-q", "-m", "test fixture"], { cwd: rootDir }); + writeFileSync(path.join(rootDir, "generated.txt"), `${"b".repeat(2 * 1024 * 1024)}\n`); + + const state = readWorktreeState(rootDir); + + expect(state.changedFiles).toEqual(["generated.txt"]); + expect(state.fingerprint).toMatch(/^[0-9a-f]{64}$/u); + } finally { + rmSync(rootDir, { force: true, recursive: true }); + } + }); + it("binds the plan to the exact source and worktree fingerprint", () => { const steps = runReleasePrepareSteps({ cwd: "/repo",