perf(checks): accelerate fresh changed gates (#121805)

This commit is contained in:
Josh Avant
2026-08-10 21:30:24 -05:00
committed by GitHub
parent b953cb2597
commit bdd35432dd
6 changed files with 120 additions and 30 deletions
+4 -2
View File
@@ -45,7 +45,9 @@ jobs:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: ${{ github.event_name == 'pull_request' && '2' || '0' }}
# Dispatch hydrates the pinned workflow ref; changed-gate sync later
# reconstructs its exact base and final tree. PR validation keeps both commits.
fetch-depth: ${{ github.event_name == 'pull_request' && '2' || '1' }}
persist-credentials: false
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
@@ -65,7 +67,7 @@ jobs:
- name: Prepare Testbox shell
uses: ./.github/actions/prepare-testbox-shell
with:
base-ref: ${{ github.event.pull_request.base.sha || 'refs/remotes/origin/main' }}
base-ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || 'HEAD' }}
- name: Hydrate Testbox provider env helper
shell: bash
+2
View File
@@ -705,6 +705,8 @@ Docker, package lanes, E2E, live proof, and CI parity. Trusted maintainer heavy
proof defaults to `blacksmith-testbox`, and `.crabbox.yaml` now defaults to it. Its configured
workflow hydrates provider and agent credentials, so untrusted contributor or
fork code must use secretless fork CI or sanitized direct AWS Crabbox instead.
The check workflow hydrates its pinned dispatch commit with a depth-1 checkout;
the changed gate later reconstructs the exact merge base and synced final tree.
Sanitized AWS runs set `CRABBOX_ENV_ALLOW=CI`, pass
`--no-hydrate`, and use a fresh temporary remote `HOME`; this prevents the repo
`OPENCLAW_*` allowlist and existing auth profiles from reaching untrusted code.
+29 -14
View File
@@ -10,6 +10,7 @@ import {
resolveRepoToolBinPath,
shouldAcquireLocalHeavyCheckLockForOxlint,
} from "./lib/local-heavy-check-runtime.mts";
import { shouldPrepareExtensionPackageBoundaryArtifacts } from "./run-oxlint.mts";
const DEFAULT_WINDOWS_EXTENSION_CHUNK_SIZE = 8;
const DEFAULT_SHARD_HEARTBEAT_MS = 30_000;
@@ -283,23 +284,28 @@ export async function main(
const selectedShards = filterOxlintShards(shards, shardArgs.only);
ensureRepoToolNodeModulesLink(resolveRepoToolBinPath("oxlint"));
const prepareResult = spawnSync(
process.execPath,
[
"--import",
"tsx",
path.resolve("scripts", "prepare-extension-package-boundary-artifacts.mts"),
],
{
stdio: "inherit",
env,
},
);
const prepareResult = shouldPrepareExtensionPackageBoundaryArtifactsForShards(
selectedShards,
shardArgs.oxlintArgs,
)
? spawnSync(
process.execPath,
[
"--import",
"tsx",
path.resolve("scripts", "prepare-extension-package-boundary-artifacts.mts"),
],
{
stdio: "inherit",
env,
},
)
: undefined;
if (prepareResult.error) {
if (prepareResult?.error) {
throw prepareResult.error;
}
if ((prepareResult.status ?? 1) !== 0) {
if (prepareResult && (prepareResult.status ?? 1) !== 0) {
process.exitCode = prepareResult.status ?? 1;
} else {
const shardConcurrency = resolveOxlintShardConcurrency({
@@ -398,6 +404,15 @@ export function filterOxlintShards<T extends { name: string }>(shards: T[], only
);
}
export function shouldPrepareExtensionPackageBoundaryArtifactsForShards(
shards: readonly OxlintShard[],
extraArgs: readonly string[] = [],
) {
return shards.some((shard) =>
shouldPrepareExtensionPackageBoundaryArtifacts([...shard.args, ...extraArgs]),
);
}
function requireShardSelector(value: string | undefined) {
if (!value || value.startsWith("-")) {
throw new Error("--only requires a shard name");
+21 -1
View File
@@ -43,13 +43,33 @@ const OXLINT_VALUE_FLAGS = new Set([
"--tsconfig",
"--warn",
]);
const OXLINT_BOUNDARY_FREE_TS_CONFIGS = new Set([
"config/tsconfig/oxlint.core.json",
"config/tsconfig/oxlint.scripts.json",
]);
const OPENCLAW_FOCUSED_CONFIG_FLAG = "--openclaw-focused-config";
/**
* Returns whether oxlint args need package-boundary declaration artifacts first.
*/
export function shouldPrepareExtensionPackageBoundaryArtifacts(args: string[]) {
return !args.some((arg) => OXLINT_PREPARE_SKIP_FLAGS.has(arg));
if (args.some((arg) => OXLINT_PREPARE_SKIP_FLAGS.has(arg))) {
return false;
}
const tsconfigs = args.flatMap((arg, index) => {
if (arg === "--tsconfig") {
const value = args[index + 1];
return value === undefined ? [] : [value];
}
return arg.startsWith("--tsconfig=") ? [arg.slice("--tsconfig=".length)] : [];
});
// Core and script lint resolve workspace sources through the root tsconfig;
// generated plugin package declarations are only an extension-lint input.
return (
tsconfigs.length === 0 ||
tsconfigs.some((tsconfig) => !OXLINT_BOUNDARY_FREE_TS_CONFIGS.has(tsconfig))
);
}
/**
+33 -13
View File
@@ -4372,28 +4372,48 @@ NODE
}
});
it("prepares all Testbox checkouts from one maintained owner", () => {
it("prepares Testbox checkouts with one maintained owner and scoped history", () => {
const workflowPaths = [
".github/workflows/ci-check-testbox.yml",
".github/workflows/ci-check-arm-testbox.yml",
".github/workflows/ci-build-artifacts-testbox.yml",
];
[
".github/workflows/ci-check-testbox.yml",
"1",
"${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || 'HEAD' }}",
],
[
".github/workflows/ci-check-arm-testbox.yml",
"0",
"${{ github.event.pull_request.base.sha || 'refs/remotes/origin/main' }}",
],
[
".github/workflows/ci-build-artifacts-testbox.yml",
"0",
"${{ github.event.pull_request.base.sha || 'refs/remotes/origin/main' }}",
],
] as const;
for (const workflowPath of workflowPaths) {
for (const [workflowPath, dispatchFetchDepth, baseRef] of workflowPaths) {
const workflow = parse(readFileSync(workflowPath, "utf8"));
const job = Object.values(workflow.jobs)[0] as { steps: WorkflowStep[] };
const checkoutStep = job.steps.find((step) => step.name === "Checkout");
const prepareStep = job.steps.find((step) => step.name === "Prepare Testbox shell");
expect(checkoutStep?.uses, workflowPath).toBe(CHECKOUT_V6);
expect(checkoutStep?.with, workflowPath).toEqual({
"fetch-depth": "${{ github.event_name == 'pull_request' && '2' || '0' }}",
"persist-credentials": false,
});
expect(checkoutStep?.with?.["persist-credentials"], workflowPath).toBe(false);
for (const [eventName, expectedDepth] of [
["pull_request", "2"],
["workflow_dispatch", dispatchFetchDepth],
] as const) {
expect(
evaluateWorkflowExpression(checkoutStep?.with?.["fetch-depth"], {
eventName,
repository: "openclaw/openclaw",
runAttempt: 1,
}),
`${workflowPath} ${eventName}`,
).toBe(expectedDepth);
}
expect(prepareStep?.uses, workflowPath).toBe("./.github/actions/prepare-testbox-shell");
expect(prepareStep?.with?.["base-ref"], workflowPath).toBe(
"${{ github.event.pull_request.base.sha || 'refs/remotes/origin/main' }}",
);
expect(prepareStep?.with?.["base-ref"], workflowPath).toBe(baseRef);
const ensureBaseStep = job.steps.find(
(step: WorkflowStep) => step.name === "Ensure Testbox base commit",
);
+31
View File
@@ -16,6 +16,7 @@ import {
resolveOxlintShardConcurrency,
resolveWindowsExtensionChunkSize,
runShard,
shouldPrepareExtensionPackageBoundaryArtifactsForShards,
shouldRunOxlintShardsSerial,
} from "../../scripts/run-oxlint-shards.mts";
import {
@@ -192,6 +193,27 @@ describe("run-oxlint", () => {
expect(shouldPrepareExtensionPackageBoundaryArtifacts([])).toBe(true);
expect(shouldPrepareExtensionPackageBoundaryArtifacts(["src/index.ts"])).toBe(true);
expect(shouldPrepareExtensionPackageBoundaryArtifacts(["--type-aware"])).toBe(true);
expect(
shouldPrepareExtensionPackageBoundaryArtifacts([
"--tsconfig",
"config/tsconfig/oxlint.extensions.json",
"extensions/telegram/src/index.ts",
]),
).toBe(true);
expect(
shouldPrepareExtensionPackageBoundaryArtifacts([
"--tsconfig=config/tsconfig/oxlint.core.json",
"--tsconfig=config/tsconfig/oxlint.extensions.json",
]),
).toBe(true);
});
it.each([
["--tsconfig", "config/tsconfig/oxlint.core.json", "src/index.ts"],
["--tsconfig=config/tsconfig/oxlint.core.json", "src/index.ts"],
["--tsconfig", "config/tsconfig/oxlint.scripts.json", "scripts/check-changed.mts"],
])("skips extension artifacts for an exact source-backed config: %s", (...args) => {
expect(shouldPrepareExtensionPackageBoundaryArtifacts(args)).toBe(false);
});
it("skips artifact preparation for metadata-only oxlint commands", () => {
@@ -201,6 +223,15 @@ describe("run-oxlint", () => {
expect(shouldPrepareExtensionPackageBoundaryArtifacts(["--rules"])).toBe(false);
});
it("prepares shard artifacts only when a selected config consumes them", () => {
const core = oxlintShard("core", "core", "src");
const scripts = oxlintShard("scripts", "scripts", "scripts");
const extensions = oxlintShard("extensions", "extensions", "extensions");
expect(shouldPrepareExtensionPackageBoundaryArtifactsForShards([core, scripts])).toBe(false);
expect(shouldPrepareExtensionPackageBoundaryArtifactsForShards([core, extensions])).toBe(true);
});
it("does not run package-boundary artifact prep twice in pnpm check", () => {
const packageJson = JSON.parse(readFileSync("package.json", "utf8")) as {
scripts: Record<string, string>;