fix(tooling): bound formatter arguments for large changed checks (#162703)

Reuse the docs formatter batching owner so changed-file checks retain every selected path without overflowing the pnpm shell command. Preserve formatting flags and serial failure propagation.

Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
This commit is contained in:
RoboClaw 2026-10-01 06:23:57 -07:00 • committed by GitHub
parent 6b8a374c18
commit cc38828234
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 70 additions and 43 deletions

View file

@ -36,6 +36,7 @@ import { getChangedPathFacts, normalizeChangedPath } from "./lib/changed-path-fa
import { printTimingSummary } from "./lib/check-timing-summary.mts";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
import { runWithFailedTrailer } from "./lib/failed-trailer.mts";
import { chunkFormatFilesForCommand } from "./lib/format-command-batches.mts";
import { resolveLocalCheckEnv } from "./lib/local-check-runtime.mts";
import { runManagedCommand } from "./lib/managed-child-process.mts";
import { readNativeTypeScriptConfig } from "./lib/native-typescript-config.mts";
@ -785,15 +786,9 @@ export function createChangedCheckPlan(
add("duplicate scan target coverage", ["dup:check:coverage"]);
broadAudits.add(add("coercion helper declaration guard", ["check:coercion-helpers"]));
add("dependency pin guard", ["deps:pins:check"]);
if (result.paths.length > 0) {
lintChecks.add(
add("format changed files", [
"format:check",
"--no-error-on-unmatched-pattern",
"--",
...result.paths,
]),
);
const formatPrefix = ["format:check", "--no-error-on-unmatched-pattern", "--"];
for (const files of chunkFormatFilesForCommand(result.paths, formatPrefix)) {
lintChecks.add(add("format changed files", [...formatPrefix, ...files]));
}
const npmLockGuardCommand = createNpmLockGuardCommand(result.paths);
if (npmLockGuardCommand) {

View file

@ -6,6 +6,10 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import {
chunkFormatFilesForCommand,
FORMAT_MAX_COMMAND_LINE_BYTES,
} from "./lib/format-command-batches.mts";
import { resolveRepoToolBinPath } from "./lib/local-check-runtime.mts";
import { outputTail, spawnOutputText } from "./lib/output-tail.mts";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
@ -13,7 +17,6 @@ import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "./windows-cmd-
const ROOT = resolveRepoRoot(import.meta.url);
const CHECK = process.argv.includes("--check");
const DOCS_FORMAT_MAX_BUFFER_BYTES = 1024 * 1024 * 16;
const DOCS_FORMAT_MAX_COMMAND_LINE_BYTES = 24 * 1024;
const FAILURE_OUTPUT_TAIL_BYTES = 16 * 1024;
type CommandResult = {
@ -91,37 +94,6 @@ export function docsFiles(root = ROOT, deps: FormatDeps = {}) {
.filter((relativePath) => (deps.existsSync ?? fs.existsSync)(path.join(root, relativePath)));
}
function commandLineBytes(args: string[]) {
return args.reduce((total, arg) => total + Buffer.byteLength(arg, "utf8") + 3, 0);
}
function chunkFilesForCommand(
files: string[],
prefixArgs: string[],
maxBytes = DOCS_FORMAT_MAX_COMMAND_LINE_BYTES,
) {
const chunks: string[][] = [];
let chunk: string[] = [];
let chunkBytes = commandLineBytes(prefixArgs);
for (const file of files) {
const fileBytes = Buffer.byteLength(file, "utf8") + 3;
if (chunk.length > 0 && chunkBytes + fileBytes > maxBytes) {
chunks.push(chunk);
chunk = [];
chunkBytes = commandLineBytes(prefixArgs);
}
chunk.push(file);
chunkBytes += fileBytes;
}
if (chunk.length > 0) {
chunks.push(chunk);
}
return chunks;
}
export function resolveOxfmtInvocation(args: string[], params: OxfmtParams = {}) {
const repoRoot = params.repoRoot ?? ROOT;
const platform = params.platform ?? process.platform;
@ -160,10 +132,10 @@ export function runOxfmt(files: string[], params: OxfmtParams = {}, deps: Format
const repoRoot = params.repoRoot ?? ROOT;
const spawnSyncImpl = deps.spawnSync ?? spawnSync;
const prefixArgs = ["--write", "--threads=1", "--config", path.join(repoRoot, ".oxfmtrc.jsonc")];
for (const chunk of chunkFilesForCommand(
for (const chunk of chunkFormatFilesForCommand(
files,
prefixArgs,
params.maxCommandLineBytes ?? DOCS_FORMAT_MAX_COMMAND_LINE_BYTES,
params.maxCommandLineBytes ?? FORMAT_MAX_COMMAND_LINE_BYTES,
)) {
const invocation = resolveOxfmtInvocation([...prefixArgs, ...chunk], {
comSpec: params.comSpec,

View file

@ -0,0 +1,33 @@
// Keep formatter arguments bounded even when pnpm joins them into one shell command.
export const FORMAT_MAX_COMMAND_LINE_BYTES = 24 * 1024;
function commandLineBytes(args: string[]) {
return args.reduce((total, arg) => total + Buffer.byteLength(arg, "utf8") + 3, 0);
}
export function chunkFormatFilesForCommand(
files: string[],
prefixArgs: string[],
maxBytes = FORMAT_MAX_COMMAND_LINE_BYTES,
) {
const chunks: string[][] = [];
let chunk: string[] = [];
let chunkBytes = commandLineBytes(prefixArgs);
for (const file of files) {
const fileBytes = Buffer.byteLength(file, "utf8") + 3;
if (chunk.length > 0 && chunkBytes + fileBytes > maxBytes) {
chunks.push(chunk);
chunk = [];
chunkBytes = commandLineBytes(prefixArgs);
}
chunk.push(file);
chunkBytes += fileBytes;
}
if (chunk.length > 0) {
chunks.push(chunk);
}
return chunks;
}

View file

@ -0,0 +1,27 @@
import { expect, it } from "vitest";
import { detectChangedLanes } from "../../scripts/changed-lanes.mts";
import { createChangedCheckPlan } from "../../scripts/check-changed.mts";
it.each([false, true])(
"keeps every formatter path within the argv budget (lintOnly=%s)",
(lintOnly) => {
const paths = Array.from(
{ length: 1_600 },
(_, index) => `docs/${"long目录_".repeat(8)}file-${index} name's.md`,
).toSorted((left, right) => left.localeCompare(right));
const commands = createChangedCheckPlan(detectChangedLanes(paths), {
lintOnly,
}).commands.filter((command) => command.args[0] === "format:check");
expect(commands.length).toBeGreaterThan(1);
expect(commands.flatMap((command) => command.args.slice(3))).toEqual(paths);
for (const command of commands) {
expect(command.args.slice(0, 3)).toEqual([
"format:check",
"--no-error-on-unmatched-pattern",
"--",
]);
expect(Buffer.byteLength(command.args.join(" "), "utf8")).toBeLessThanOrEqual(24 * 1024);
}
},
);