mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
fix(tooling): harden changed checks for sparse worktrees
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
|
||||
import { printTimingSummary } from "./lib/check-timing-summary.mjs";
|
||||
import { runManagedCommand } from "./lib/managed-child-process.mjs";
|
||||
import { isCiLikeEnv } from "./lib/vitest-local-scheduling.mjs";
|
||||
import { resolveChangedTestTargetPlan } from "./test-projects.test-support.mjs";
|
||||
|
||||
export const CHANGED_CHECK_VITEST_NO_OUTPUT_TIMEOUT_MS = "600000";
|
||||
@@ -15,13 +16,30 @@ const VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS";
|
||||
const VITEST_NO_OUTPUT_RETRY_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_RETRY";
|
||||
|
||||
export function createChangedCheckVitestEnv(baseEnv = process.env) {
|
||||
return {
|
||||
const env = {
|
||||
...baseEnv,
|
||||
[VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY]:
|
||||
baseEnv[VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY]?.trim() ||
|
||||
CHANGED_CHECK_VITEST_NO_OUTPUT_TIMEOUT_MS,
|
||||
[VITEST_NO_OUTPUT_RETRY_ENV_KEY]: baseEnv[VITEST_NO_OUTPUT_RETRY_ENV_KEY]?.trim() || "0",
|
||||
};
|
||||
|
||||
const hasWorkerOverride = Boolean(
|
||||
(baseEnv.OPENCLAW_VITEST_MAX_WORKERS ?? baseEnv.OPENCLAW_TEST_WORKERS)?.trim(),
|
||||
);
|
||||
const hasParallelOverride = Boolean(baseEnv.OPENCLAW_TEST_PROJECTS_PARALLEL?.trim());
|
||||
const serialOverride = baseEnv.OPENCLAW_TEST_PROJECTS_SERIAL?.trim();
|
||||
if (
|
||||
!isCiLikeEnv(baseEnv) &&
|
||||
!hasWorkerOverride &&
|
||||
!hasParallelOverride &&
|
||||
serialOverride !== "0"
|
||||
) {
|
||||
env.OPENCLAW_TEST_PROJECTS_SERIAL = serialOverride || "1";
|
||||
env.OPENCLAW_VITEST_MAX_WORKERS = "1";
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
export function createChangedCheckPlan(result, options = {}) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
acquireLocalHeavyCheckLockSync,
|
||||
@@ -20,10 +22,105 @@ const OXLINT_PREPARE_SKIP_FLAGS = new Set([
|
||||
"--init",
|
||||
"--lsp",
|
||||
]);
|
||||
const OXLINT_VALUE_FLAGS = new Set([
|
||||
"--config",
|
||||
"--deny",
|
||||
"--env",
|
||||
"--format",
|
||||
"--globals",
|
||||
"--ignore-path",
|
||||
"--max-warnings",
|
||||
"--output-file",
|
||||
"--plugin",
|
||||
"--rules",
|
||||
"--tsconfig",
|
||||
"--warn",
|
||||
]);
|
||||
|
||||
export function shouldPrepareExtensionPackageBoundaryArtifacts(args) {
|
||||
return !args.some((arg) => OXLINT_PREPARE_SKIP_FLAGS.has(arg));
|
||||
}
|
||||
|
||||
export function filterSparseMissingOxlintTargets(
|
||||
args,
|
||||
{
|
||||
cwd = process.cwd(),
|
||||
fileExists = fs.existsSync,
|
||||
isSparseCheckoutEnabled = getSparseCheckoutEnabled,
|
||||
isTrackedPath = hasTrackedPath,
|
||||
} = {},
|
||||
) {
|
||||
if (!isSparseCheckoutEnabled({ cwd })) {
|
||||
return { args, hadExplicitTargets: false, remainingExplicitTargets: 0, skippedTargets: [] };
|
||||
}
|
||||
|
||||
const filteredArgs = [];
|
||||
const skippedTargets = [];
|
||||
let hadExplicitTargets = false;
|
||||
let remainingExplicitTargets = 0;
|
||||
let consumeNextValue = false;
|
||||
|
||||
for (const arg of args) {
|
||||
if (consumeNextValue) {
|
||||
filteredArgs.push(arg);
|
||||
consumeNextValue = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === "--") {
|
||||
filteredArgs.push(arg);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith("--")) {
|
||||
filteredArgs.push(arg);
|
||||
if (!arg.includes("=") && OXLINT_VALUE_FLAGS.has(arg)) {
|
||||
consumeNextValue = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith("-")) {
|
||||
filteredArgs.push(arg);
|
||||
continue;
|
||||
}
|
||||
|
||||
hadExplicitTargets = true;
|
||||
const absoluteTarget = path.resolve(cwd, arg);
|
||||
if (!fileExists(absoluteTarget) && isTrackedPath({ cwd, target: arg })) {
|
||||
skippedTargets.push(arg);
|
||||
continue;
|
||||
}
|
||||
|
||||
remainingExplicitTargets += 1;
|
||||
filteredArgs.push(arg);
|
||||
}
|
||||
|
||||
return { args: filteredArgs, hadExplicitTargets, remainingExplicitTargets, skippedTargets };
|
||||
}
|
||||
|
||||
function getSparseCheckoutEnabled({ cwd }) {
|
||||
const result = spawnSync("git", ["config", "--get", "--bool", "core.sparseCheckout"], {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
return result.status === 0 && result.stdout.trim() === "true";
|
||||
}
|
||||
|
||||
function hasTrackedPath({ cwd, target }) {
|
||||
const result = spawnSync("git", ["ls-files", "--", target], {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
return result.status === 0 && result.stdout.trim().length > 0;
|
||||
}
|
||||
|
||||
async function prepareExtensionPackageBoundaryArtifacts(env) {
|
||||
const releaseArtifactsLock = acquireLocalHeavyCheckLockSync({
|
||||
cwd: process.cwd(),
|
||||
@@ -50,7 +147,19 @@ async function prepareExtensionPackageBoundaryArtifacts(env) {
|
||||
}
|
||||
|
||||
export async function main(argv = process.argv.slice(2), runtimeEnv = process.env) {
|
||||
const { args: finalArgs, env } = applyLocalOxlintPolicy(argv, runtimeEnv);
|
||||
const { args: policyArgs, env } = applyLocalOxlintPolicy(argv, runtimeEnv);
|
||||
const sparseTargets = filterSparseMissingOxlintTargets(policyArgs);
|
||||
const finalArgs = sparseTargets.args;
|
||||
if (sparseTargets.skippedTargets.length > 0) {
|
||||
console.error(
|
||||
`[oxlint] sparse checkout is missing tracked target(s); skipping ${sparseTargets.skippedTargets.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (sparseTargets.hadExplicitTargets && sparseTargets.remainingExplicitTargets === 0) {
|
||||
console.error("[oxlint] no present sparse-checkout targets remain; skipping oxlint.");
|
||||
return;
|
||||
}
|
||||
|
||||
const releaseLock =
|
||||
env.OPENCLAW_OXLINT_SKIP_LOCK === "1"
|
||||
? () => {}
|
||||
|
||||
@@ -232,6 +232,7 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
|
||||
"scripts/run-vitest.mjs",
|
||||
["test/scripts/test-projects.test.ts", "test/scripts/vitest-local-scheduling.test.ts"],
|
||||
],
|
||||
["scripts/run-oxlint.mjs", ["test/scripts/run-oxlint.test.ts"]],
|
||||
["scripts/test-extension-batch.mjs", ["test/scripts/test-extension.test.ts"]],
|
||||
["scripts/lib/extension-test-plan.mjs", ["test/scripts/test-extension.test.ts"]],
|
||||
["scripts/lib/vitest-batch-runner.mjs", ["test/scripts/test-extension.test.ts"]],
|
||||
|
||||
@@ -370,6 +370,8 @@ describe("scripts/changed-lanes", () => {
|
||||
PATH: "/usr/bin",
|
||||
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: CHANGED_CHECK_VITEST_NO_OUTPUT_TIMEOUT_MS,
|
||||
OPENCLAW_VITEST_NO_OUTPUT_RETRY: "0",
|
||||
OPENCLAW_TEST_PROJECTS_SERIAL: "1",
|
||||
OPENCLAW_VITEST_MAX_WORKERS: "1",
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -382,4 +384,16 @@ describe("scripts/changed-lanes", () => {
|
||||
OPENCLAW_VITEST_NO_OUTPUT_RETRY: "1",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not force serial changed-check tests in CI or when workers are explicit", () => {
|
||||
expect(createChangedCheckVitestEnv({ CI: "true" })).not.toHaveProperty(
|
||||
"OPENCLAW_VITEST_MAX_WORKERS",
|
||||
);
|
||||
expect(createChangedCheckVitestEnv({ OPENCLAW_VITEST_MAX_WORKERS: "4" })).toMatchObject({
|
||||
OPENCLAW_VITEST_MAX_WORKERS: "4",
|
||||
});
|
||||
expect(
|
||||
createChangedCheckVitestEnv({ OPENCLAW_TEST_PROJECTS_PARALLEL: "4" }),
|
||||
).not.toHaveProperty("OPENCLAW_TEST_PROJECTS_SERIAL");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { shouldPrepareExtensionPackageBoundaryArtifacts } from "../../scripts/run-oxlint.mjs";
|
||||
import {
|
||||
filterSparseMissingOxlintTargets,
|
||||
shouldPrepareExtensionPackageBoundaryArtifacts,
|
||||
} from "../../scripts/run-oxlint.mjs";
|
||||
|
||||
describe("run-oxlint", () => {
|
||||
it("prepares extension package boundary artifacts for normal lint runs", () => {
|
||||
@@ -30,4 +33,36 @@ describe("run-oxlint", () => {
|
||||
expect(shardedLintRunner).toContain("prepare-extension-package-boundary-artifacts.mjs");
|
||||
expect(shardedLintRunner).toContain('OPENCLAW_OXLINT_SKIP_PREPARE: "1"');
|
||||
});
|
||||
|
||||
it("filters tracked targets missing from sparse checkouts", () => {
|
||||
const result = filterSparseMissingOxlintTargets(
|
||||
["--tsconfig", "tsconfig.oxlint.core.json", "src", "ui", "packages", "--threads=1"],
|
||||
{
|
||||
fileExists: (target: string) => target.endsWith("/src"),
|
||||
isSparseCheckoutEnabled: () => true,
|
||||
isTrackedPath: ({ target }: { target: string }) => target === "ui" || target === "packages",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
args: ["--tsconfig", "tsconfig.oxlint.core.json", "src", "--threads=1"],
|
||||
hadExplicitTargets: true,
|
||||
remainingExplicitTargets: 1,
|
||||
skippedTargets: ["ui", "packages"],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps missing untracked oxlint targets so typos still fail", () => {
|
||||
const result = filterSparseMissingOxlintTargets(["src", "typo"], {
|
||||
fileExists: (target: string) => target.endsWith("/src"),
|
||||
isSparseCheckoutEnabled: () => true,
|
||||
isTrackedPath: () => false,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
args: ["src", "typo"],
|
||||
remainingExplicitTargets: 2,
|
||||
skippedTargets: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user