From fb6df23a898a797bf1d3f3536cd37b273d52234a Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 17 Jun 2026 13:14:08 +0200 Subject: [PATCH] fix(testing): harden script tooling checks --- scripts/check-workflows.mjs | 65 ++++++++++++++++++++++++-- scripts/test-projects.test-support.mjs | 1 + test/scripts/check-workflows.test.ts | 58 +++++++++++++++++++++++ test/scripts/test-projects.test.ts | 12 +++++ 4 files changed, 133 insertions(+), 3 deletions(-) diff --git a/scripts/check-workflows.mjs b/scripts/check-workflows.mjs index 2658ac265360..ce991d971563 100644 --- a/scripts/check-workflows.mjs +++ b/scripts/check-workflows.mjs @@ -3,10 +3,12 @@ // Uses installed tools when present, otherwise falls back to pinned hooks where // possible, then runs repo-specific workflow guards. import { spawnSync } from "node:child_process"; -import { readdirSync } from "node:fs"; +import { mkdtempSync, readdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; import { join } from "node:path"; const ACTIONLINT_VERSION = "1.7.11"; +const PRE_COMMIT_VERSION = "4.2.0"; const WORKFLOW_DIR = ".github/workflows"; function commandExists(command, args = ["--version"]) { @@ -25,6 +27,59 @@ function run(command, args) { } } +function runChecked(command, args) { + const result = spawnSync(command, args, { stdio: "inherit" }); + if (result.error) { + return { + message: `[check-workflows] failed to run ${command}: ${result.error.message}`, + status: 1, + }; + } + if (result.status !== 0) { + return { + message: null, + status: result.status ?? 1, + }; + } + return null; +} + +function runPreCommitFromTempVenv(hook, hookArgs) { + if (!commandExists("python3", ["--version"])) { + return false; + } + const venvDir = mkdtempSync(join(tmpdir(), "openclaw-check-workflows-pre-commit-")); + const python = join(venvDir, process.platform === "win32" ? "Scripts/python.exe" : "bin/python"); + let failure; + try { + failure = runChecked("python3", ["-m", "venv", venvDir]); + if (!failure) { + failure = runChecked(python, [ + "-m", + "pip", + "install", + "--disable-pip-version-check", + `pre-commit==${PRE_COMMIT_VERSION}`, + ]); + } + if (!failure) { + failure = runChecked(python, ["-m", "pre_commit", ...hookArgs]); + } + if (failure) { + return false; + } + return true; + } finally { + rmSync(venvDir, { force: true, recursive: true }); + if (failure) { + if (failure.message) { + console.error(failure.message); + } + process.exit(failure.status); + } + } +} + function workflowFiles() { return readdirSync(WORKFLOW_DIR) .filter((file) => file.endsWith(".yml") || file.endsWith(".yaml")) @@ -42,9 +97,12 @@ function runPreCommitHook(hook, files) { run("python3", ["-m", "pre_commit", ...hookArgs]); return; } + if (runPreCommitFromTempVenv(hook, hookArgs)) { + return; + } console.error( - `[check-workflows] missing pre-commit runtime for ${hook}: install pre-commit or python3 pre_commit.`, + `[check-workflows] missing pre-commit runtime for ${hook}: install pre-commit or Python venv support for pre-commit ${PRE_COMMIT_VERSION}.`, ); process.exit(1); } @@ -57,7 +115,8 @@ if (commandExists("actionlint")) { run("go", ["run", `github.com/rhysd/actionlint/cmd/actionlint@v${ACTIONLINT_VERSION}`]); } else if ( commandExists("pre-commit") || - commandExists("python3", ["-m", "pre_commit", "--version"]) + commandExists("python3", ["-m", "pre_commit", "--version"]) || + commandExists("python3", ["--version"]) ) { runPreCommitHook("actionlint", workflows); } else { diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 73f9ca3e7530..5eb90218dd21 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -2042,6 +2042,7 @@ function classifyTarget(arg, cwd) { } if ( relative.startsWith("test/") || + relative === "src/scripts" || relative.startsWith("src/scripts/") || relative === "src/config/doc-baseline.integration.test.ts" || relative === "src/config/schema.base.generated.test.ts" || diff --git a/test/scripts/check-workflows.test.ts b/test/scripts/check-workflows.test.ts index 753abb6e9b9b..a3d142f21bdd 100644 --- a/test/scripts/check-workflows.test.ts +++ b/test/scripts/check-workflows.test.ts @@ -78,6 +78,64 @@ describe("check-workflows", () => { } }); + it("bootstraps pinned pre-commit in a temporary Python venv when needed", () => { + const tempDir = mkdtempSync(path.join(os.tmpdir(), "check-workflows-")); + try { + const binDir = path.join(tempDir, "bin"); + const markerPath = path.join(tempDir, "python.txt"); + mkdirSync(binDir); + writeFileSync(path.join(binDir, "node"), "#!/bin/sh\nexit 0\n", { mode: 0o755 }); + writeFileSync( + path.join(binDir, "python3"), + [ + "#!/bin/sh", + 'if [ "$1" = "--version" ]; then exit 0; fi', + 'if [ "$1" = "-m" ] && [ "$2" = "pre_commit" ] && [ "$3" = "--version" ]; then exit 1; fi', + 'if [ "$1" = "-m" ] && [ "$2" = "pip" ]; then', + ' printf "%s\\n" "$*" >> "$PRE_COMMIT_BOOTSTRAP_MARKER"', + " exit 0", + "fi", + 'if [ "$1" = "-m" ] && [ "$2" = "pre_commit" ]; then', + ' printf "%s\\n" "$*" >> "$PRE_COMMIT_BOOTSTRAP_MARKER"', + " exit 0", + "fi", + 'if [ "$1" = "-m" ] && [ "$2" = "venv" ]; then', + ' /bin/mkdir -p "$3/bin"', + ' /bin/cp "$0" "$3/bin/python"', + ' /bin/chmod +x "$3/bin/python"', + " exit 0", + "fi", + "exit 0", + "", + ].join("\n"), + { mode: 0o755 }, + ); + + const result = spawnSync(process.execPath, [scriptPath], { + encoding: "utf8", + env: { + ...process.env, + PATH: binDir, + PRE_COMMIT_BOOTSTRAP_MARKER: markerPath, + }, + }); + + expect(result.status).toBe(0); + const pythonArgs = readFileSync(markerPath, "utf8"); + expect(pythonArgs).toContain( + "-m pip install --disable-pip-version-check pre-commit==4.2.0", + ); + expect(pythonArgs).toContain( + "-m pre_commit run --config .pre-commit-config.yaml actionlint --files", + ); + expect(pythonArgs).toContain( + "-m pre_commit run --config .pre-commit-config.yaml zizmor --files", + ); + } finally { + rmSync(tempDir, { force: true, recursive: true }); + } + }); + it("keeps Windows WSL2 probe output normalized through the shared wrapper", () => { const workflow = readFileSync(".github/workflows/windows-testbox-probe.yml", "utf8"); diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index c9afb9be7c13..fe3d90b80392 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -967,6 +967,18 @@ describe("scripts/test-projects changed-target routing", () => { ]); }); + it("routes the src scripts test root to the tooling shard", () => { + expect(findUnmatchedExplicitTestTargets(["src/scripts"], process.cwd())).toEqual([]); + expect(buildVitestRunPlans(["src/scripts"], process.cwd())).toEqual([ + { + config: "test/vitest/vitest.tooling.config.ts", + forwardedArgs: [], + includePatterns: ["src/scripts/**/*.test.ts"], + watchMode: false, + }, + ]); + }); + it("includes the isolated tooling shard for broad shell helper globs", () => { expect(buildVitestRunPlans(["test/scripts/*.test.ts"], process.cwd())).toEqual([ {