diff --git a/.github/workflows/ci-check-testbox.yml b/.github/workflows/ci-check-testbox.yml index 7363b71b7789..be79b93fd211 100644 --- a/.github/workflows/ci-check-testbox.yml +++ b/.github/workflows/ci-check-testbox.yml @@ -15,6 +15,9 @@ permissions: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + PNPM_CONFIG_MODULES_DIR: "/tmp/openclaw-pnpm-node-modules" + PNPM_CONFIG_STORE_DIR: "/tmp/openclaw-pnpm-store" + PNPM_CONFIG_VIRTUAL_STORE_DIR: "/tmp/openclaw-pnpm-virtual-store" jobs: check: diff --git a/docs/ci.md b/docs/ci.md index 21f9df119727..a75c91ac4968 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -566,6 +566,13 @@ The repo wrapper refuses a stale Crabbox binary that does not advertise `blacksm node scripts/crabbox-wrapper.mjs run --provider blacksmith-testbox --timing-json --shell -- "pnpm test " ``` +Blacksmith-backed runs require Crabbox 0.22.0 or newer so the wrapper gets the current Testbox sync, queue, and cleanup behavior. When using the sibling checkout, rebuild the ignored local binary before timing or proof work: + +```bash +version="$(git -C ../crabbox describe --tags --always --dirty | sed 's/^v//')" \ + && go build -C ../crabbox -trimpath -ldflags "-s -w -X github.com/openclaw/crabbox/internal/cli.version=${version}" -o bin/crabbox ./cmd/crabbox +``` + Changed gate: ```bash diff --git a/scripts/crabbox-wrapper.mjs b/scripts/crabbox-wrapper.mjs index 9d9e60dcefa1..0f62d0f29402 100755 --- a/scripts/crabbox-wrapper.mjs +++ b/scripts/crabbox-wrapper.mjs @@ -127,6 +127,7 @@ function spawnInvocation(command, commandArgs, env, platform) { const cmdMetaCharactersRe = /([()\][%!^"`<>&|;, *?])/g; const jsRuntimeEntrypoints = new Set(["pnpm", "npm", "npx", "corepack", "node", "yarn", "bun"]); const awsMacosCorepackEntrypoints = new Set(["pnpm", "yarn", "corepack"]); +const minimumBlacksmithCrabboxVersion = [0, 22, 0]; const shellControlCommandPrefixes = new Set([ "if", "while", @@ -182,6 +183,47 @@ function checkedOutput(command, commandArgs) { }; } +function parseCrabboxVersion(value) { + const match = `${value}`.match(/\bv?(\d+)\.(\d+)\.(\d+)(?:-([^\s+]+))?(?:\+[^\s]+)?\b/u); + if (!match) { + return null; + } + return { + tuple: match.slice(1, 4).map((part) => Number.parseInt(part, 10)), + suffix: match[4] ?? "", + }; +} + +function compareVersionTuples(left, right) { + for (let index = 0; index < 3; index += 1) { + const diff = left[index] - right[index]; + if (diff !== 0) { + return diff; + } + } + return 0; +} + +function formatVersionTuple(version) { + return version.join("."); +} + +function isPostReleaseDescribeSuffix(suffix) { + return /^\d+-g[0-9a-f]+(?:-dirty)?$/iu.test(suffix); +} + +function satisfiesMinimumCrabboxVersion(version, minimum) { + const parsed = parseCrabboxVersion(version); + if (!parsed) { + return false; + } + const comparison = compareVersionTuples(parsed.tuple, minimum); + if (comparison !== 0) { + return comparison > 0; + } + return !parsed.suffix || isPostReleaseDescribeSuffix(parsed.suffix); +} + function gitOutput(commandArgs) { const gitBinary = resolvePathBinary("git", process.env, process.platform) ?? "git"; const invocation = spawnInvocation(gitBinary, commandArgs, process.env, process.platform); @@ -1826,6 +1868,7 @@ function isProviderAdvertised(provider, advertisedProviders) { const providers = parseProvidersFromHelp(help.text); const displayBinary = binary === "crabbox" ? "crabbox" : relative(repoRoot, binary); const provider = selectedProvider(args, providers); +const canonicalProvider = providerAliases.get(provider) ?? provider; const commandProviderValue = commandProvider(args); let normalizedArgs = ensureAwsMacOnDemandMarket( ensureAzureWindowsProvider(args, provider, providers), @@ -1854,9 +1897,22 @@ if (provider && !isProviderAdvertised(provider, providers)) { process.exit(2); } +if (canonicalProvider === "blacksmith-testbox") { + if (!satisfiesMinimumCrabboxVersion(version.text, minimumBlacksmithCrabboxVersion)) { + console.error( + [ + `[crabbox] provider=blacksmith-testbox requires Crabbox >= ${formatVersionTuple(minimumBlacksmithCrabboxVersion)} for current Testbox sync, queue, and cleanup behavior.`, + `[crabbox] selected binary reported version=${version.text || "unknown"}.`, + "[crabbox] if using ../crabbox, rebuild it: version=$(git -C ../crabbox describe --tags --always --dirty | sed 's/^v//') && go build -C ../crabbox -trimpath -ldflags \"-s -w -X github.com/openclaw/crabbox/internal/cli.version=${version}\" -o bin/crabbox ./cmd/crabbox", + ].join("\n"), + ); + process.exit(2); + } +} + enforceBrokeredAws(normalizedArgs, provider); -if (provider === "blacksmith-testbox") { +if (canonicalProvider === "blacksmith-testbox") { const envProvider = process.env.CRABBOX_PROVIDER?.trim(); const source = commandProviderValue ? "explicit" diff --git a/test/scripts/crabbox-wrapper.test.ts b/test/scripts/crabbox-wrapper.test.ts index d009349cc7eb..468ec2fa6cd1 100644 --- a/test/scripts/crabbox-wrapper.test.ts +++ b/test/scripts/crabbox-wrapper.test.ts @@ -38,7 +38,7 @@ function writeFakeCrabbox(binDir: string, helpText: string): string { const script = [ "#!/bin/sh", 'if [ "$1" = "--version" ]; then', - ' printf "%s\\n" "crabbox 0.15.0"', + ' printf "%s\\n" "${OPENCLAW_FAKE_CRABBOX_VERSION:-crabbox 0.22.1}"', " exit 0", "fi", 'if [ "$1" = "run" ] && [ "$2" = "--help" ]; then', @@ -118,7 +118,7 @@ function writeFakeCrabbox(binDir: string, helpText: string): string { "#!/usr/bin/env node", "const args = process.argv.slice(2);", 'if (args[0] === "--version") {', - ' console.log("crabbox 0.15.0");', + " console.log(process.env.OPENCLAW_FAKE_CRABBOX_VERSION || 'crabbox 0.22.1');", " process.exit(0);", "}", 'if (args[0] === "run" && args[1] === "--help") {', @@ -338,6 +338,52 @@ describe.concurrent("scripts/crabbox-wrapper", () => { expect(parseFakeCrabboxOutput(result).args).toContain("local-container"); }); + it("requires a current Crabbox binary for Blacksmith Testbox runs", () => { + const result = runWrapper( + "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n", + ["run", "--provider", "blacksmith-testbox", "--", "echo ok"], + { env: { OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.21.9" } }, + ); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(""); + expect(result.stderr).toContain("provider=blacksmith-testbox requires Crabbox >= 0.22.0"); + expect(result.stderr).toContain("selected binary reported version=crabbox 0.21.9"); + }); + + it("applies the Blacksmith version gate to provider aliases", () => { + const result = runWrapper( + "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n", + ["run", "--provider", "blacksmith", "--", "echo ok"], + { env: { OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.21.9" } }, + ); + + expect(result.status).toBe(2); + expect(result.stderr).toContain("provider=blacksmith-testbox requires Crabbox >= 0.22.0"); + }); + + it("rejects prerelease Crabbox builds at the Blacksmith minimum boundary", () => { + const result = runWrapper( + "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n", + ["run", "--provider", "blacksmith-testbox", "--", "echo ok"], + { env: { OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.22.0-rc.1" } }, + ); + + expect(result.status).toBe(2); + expect(result.stderr).toContain("selected binary reported version=crabbox 0.22.0-rc.1"); + }); + + it("accepts post-release Crabbox describe builds at the Blacksmith minimum boundary", () => { + const result = runWrapper( + "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n", + ["run", "--provider", "blacksmith-testbox", "--", "echo ok"], + { env: { OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.22.0-3-gabc1234" } }, + ); + + expect(result.status).toBe(0); + expect(parseFakeCrabboxOutput(result).args).toContain("blacksmith-testbox"); + }); + it("only forces the short local-container Docker work root on Linux", () => { const result = runWrapper( "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n", diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 9bab6a69e8ff..7367600daf7a 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -932,10 +932,14 @@ describe("package artifact reuse", () => { }); it("fails Testbox changed-check delegation when the remote command fails", () => { + const workflow = readFileSync(CI_CHECK_TESTBOX_WORKFLOW, "utf8"); const runTestboxStep = workflowJob(CI_CHECK_TESTBOX_WORKFLOW, "check").steps?.find( (step) => step.name === "Run Testbox", ); + expect(workflow).toContain('PNPM_CONFIG_MODULES_DIR: "/tmp/openclaw-pnpm-node-modules"'); + expect(workflow).toContain('PNPM_CONFIG_STORE_DIR: "/tmp/openclaw-pnpm-store"'); + expect(workflow).toContain('PNPM_CONFIG_VIRTUAL_STORE_DIR: "/tmp/openclaw-pnpm-virtual-store"'); expect(runTestboxStep?.uses).toContain("useblacksmith/run-testbox@"); expect(runTestboxStep?.["continue-on-error"]).toBeUndefined(); });