test: narrow live Docker package script changes

This commit is contained in:
Peter Steinberger
2026-04-26 01:59:07 +01:00
parent 57f05128cb
commit 87142b5fb1
4 changed files with 257 additions and 6 deletions

View File

@@ -1,5 +1,5 @@
import { execFileSync } from "node:child_process";
import { appendFileSync } from "node:fs";
import { appendFileSync, existsSync, readFileSync } from "node:fs";
import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
const DOCS_PATH_RE = /^(?:docs\/|README\.md$|AGENTS\.md$|.*\.mdx?$)/u;
@@ -12,6 +12,7 @@ const ROOT_GLOBAL_PATH_RE =
/^(?:package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|tsdown\.config\.ts$|vitest\.config\.ts$)/u;
const LIVE_DOCKER_TOOLING_PATH_RE =
/^(?:scripts\/test-docker-all\.mjs|scripts\/test-docker-all\.sh|scripts\/lib\/live-docker-auth\.sh|scripts\/test-live-(?:acp-bind|cli-backend|codex-harness|gateway-models|models)-docker\.sh|src\/gateway\/gateway-acp-bind\.live\.test\.ts|src\/gateway\/live-agent-probes\.test\.ts)$/u;
const LIVE_DOCKER_PACKAGE_SCRIPT_RE = /^test:docker:live-[\w:-]+$/u;
const TEST_PATH_RE =
/(?:^|\/)(?:test|__tests__)\/|(?:\.|\/)(?:test|spec|e2e|browser\.test)\.[cm]?[jt]sx?$/u;
const PUBLIC_EXTENSION_CONTRACT_RE =
@@ -66,9 +67,10 @@ export function createEmptyChangedLanes() {
/**
* @param {string[]} changedPaths
* @param {{ packageJsonChangeKind?: "liveDockerTooling" | null }} [options]
* @returns {ChangedLaneResult}
*/
export function detectChangedLanes(changedPaths) {
export function detectChangedLanes(changedPaths, options = {}) {
const paths = [...new Set(changedPaths.map(normalizeChangedPath).filter(Boolean))]
.toSorted((left, right) => left.localeCompare(right))
.filter((changedPath) => changedPath !== "--");
@@ -76,6 +78,8 @@ export function detectChangedLanes(changedPaths) {
const reasons = [];
let extensionImpactFromCore = false;
let hasNonDocs = false;
const packageJsonIsLiveDockerTooling =
paths.includes("package.json") && options.packageJsonChangeKind === "liveDockerTooling";
if (paths.length === 0) {
reasons.push("no changed paths");
@@ -83,6 +87,7 @@ export function detectChangedLanes(changedPaths) {
}
if (
!packageJsonIsLiveDockerTooling &&
paths.some((changedPath) => RELEASE_METADATA_PATHS.has(changedPath)) &&
paths.every(
(changedPath) => RELEASE_METADATA_PATHS.has(changedPath) || DOCS_PATH_RE.test(changedPath),
@@ -104,6 +109,12 @@ export function detectChangedLanes(changedPaths) {
hasNonDocs = true;
if (changedPath === "package.json" && packageJsonIsLiveDockerTooling) {
lanes.liveDockerTooling = true;
reasons.push(`${changedPath}: live Docker package scripts`);
continue;
}
if (LIVE_DOCKER_TOOLING_PATH_RE.test(changedPath)) {
lanes.liveDockerTooling = true;
reasons.push(`${changedPath}: live Docker tooling surface`);
@@ -231,6 +242,94 @@ export function listStagedChangedPaths() {
return output.split("\n").map(normalizeChangedPath).filter(Boolean);
}
export function classifyPackageJsonChangeFromGit(params) {
try {
const { before, after } = readPackageJsonBeforeAfter(params);
return isLiveDockerPackageScriptOnlyChange(before, after) ? "liveDockerTooling" : null;
} catch {
return null;
}
}
export function isLiveDockerPackageScriptOnlyChange(before, after) {
const beforePackage = JSON.parse(before);
const afterPackage = JSON.parse(after);
const beforeAllowed = extractLiveDockerPackageScripts(beforePackage);
const afterAllowed = extractLiveDockerPackageScripts(afterPackage);
const beforeStripped = stripLiveDockerPackageScripts(beforePackage);
const afterStripped = stripLiveDockerPackageScripts(afterPackage);
return (
stableJson(beforeStripped) === stableJson(afterStripped) &&
stableJson(beforeAllowed) !== stableJson(afterAllowed)
);
}
function readPackageJsonBeforeAfter(params) {
const before = readGitText(params.staged ? "HEAD" : params.base, "package.json");
if (params.staged) {
return { before, after: readGitText("INDEX", "package.json") };
}
let after = readGitText(params.head ?? "HEAD", "package.json");
if (params.includeWorktree !== false && existsSync("package.json")) {
const worktree = readGitText("WORKTREE", "package.json");
if (worktree !== after) {
after = worktree;
}
}
return { before, after };
}
function readGitText(ref, filePath) {
if (ref === "WORKTREE") {
return readFileSync(filePath, "utf8");
}
const spec = ref === "INDEX" ? `:${filePath}` : `${ref}:${filePath}`;
return execFileSync("git", ["show", spec], {
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf8",
maxBuffer: 16 * 1024 * 1024,
});
}
function extractLiveDockerPackageScripts(packageJson) {
const scripts = packageJson?.scripts;
if (!scripts || typeof scripts !== "object" || Array.isArray(scripts)) {
return {};
}
return Object.fromEntries(
Object.entries(scripts).filter(([name]) => LIVE_DOCKER_PACKAGE_SCRIPT_RE.test(name)),
);
}
function stripLiveDockerPackageScripts(packageJson) {
const clone = JSON.parse(JSON.stringify(packageJson));
const scripts = clone.scripts;
if (!scripts || typeof scripts !== "object" || Array.isArray(scripts)) {
return clone;
}
for (const name of Object.keys(scripts)) {
if (LIVE_DOCKER_PACKAGE_SCRIPT_RE.test(name)) {
delete scripts[name];
}
}
return clone;
}
function stableJson(value) {
if (Array.isArray(value)) {
return `[${value.map(stableJson).join(",")}]`;
}
if (value && typeof value === "object") {
return `{${Object.keys(value)
.toSorted((left, right) => left.localeCompare(right))
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
.join(",")}}`;
}
return JSON.stringify(value);
}
export function writeChangedLaneGitHubOutput(result, outputPath = process.env.GITHUB_OUTPUT) {
if (!outputPath) {
throw new Error("GITHUB_OUTPUT is required");
@@ -319,7 +418,14 @@ if (isDirectRun()) {
: args.staged
? listStagedChangedPaths()
: listChangedPathsFromGit({ base: args.base, head: args.head });
const result = detectChangedLanes(paths);
const packageJsonChangeKind = paths.includes("package.json")
? classifyPackageJsonChangeFromGit({
base: args.base,
head: args.head,
staged: args.staged,
})
: null;
const result = detectChangedLanes(paths, { packageJsonChangeKind });
if (args.githubOutput) {
writeChangedLaneGitHubOutput(result);
}

View File

@@ -1,5 +1,6 @@
import { performance } from "node:perf_hooks";
import {
classifyPackageJsonChangeFromGit,
detectChangedLanes,
listChangedPathsFromGit,
listStagedChangedPaths,
@@ -407,7 +408,14 @@ if (isDirectRun()) {
: args.staged
? listStagedChangedPaths()
: listChangedPathsFromGit({ base: args.base, head: args.head });
const result = detectChangedLanes(paths);
const packageJsonChangeKind = paths.includes("package.json")
? classifyPackageJsonChangeFromGit({
base: args.base,
head: args.head,
staged: args.staged,
})
: null;
const result = detectChangedLanes(paths, { packageJsonChangeKind });
process.exitCode = await runChangedCheck(result, {
...args,
explicitPaths: args.paths.length > 0,