diff --git a/.github/workflows/ci-check-testbox.yml b/.github/workflows/ci-check-testbox.yml index 0d96b7e2b680..300abf254240 100644 --- a/.github/workflows/ci-check-testbox.yml +++ b/.github/workflows/ci-check-testbox.yml @@ -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 diff --git a/docs/ci.md b/docs/ci.md index fce2b21038a2..e6c12c027217 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -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. diff --git a/scripts/run-oxlint-shards.mts b/scripts/run-oxlint-shards.mts index f3eebdb19624..744362fa3ea9 100644 --- a/scripts/run-oxlint-shards.mts +++ b/scripts/run-oxlint-shards.mts @@ -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(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"); diff --git a/scripts/run-oxlint.mts b/scripts/run-oxlint.mts index d3081dafbf22..18846b61bc75 100644 --- a/scripts/run-oxlint.mts +++ b/scripts/run-oxlint.mts @@ -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)) + ); } /** diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index 1aa62ecbf9de..285e52538621 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -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", ); diff --git a/test/scripts/run-oxlint.test.ts b/test/scripts/run-oxlint.test.ts index 5255bcdffc29..7eaaacf9c249 100644 --- a/test/scripts/run-oxlint.test.ts +++ b/test/scripts/run-oxlint.test.ts @@ -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;