openclaw/scripts/check-workflows.mts
RoboClaw f6c2aeca54
chore: refresh dependencies with a seven-day cooldown (#182010)
* chore: refresh dependencies with a seven-day cooldown

Update qualified runtime, development and release dependencies published by 2026-10-16T22:58:39Z. Preserve inherited newer pins, existing patched behavior and dependency-edge versions.

Co-authored-by: steipete <58493+steipete@users.noreply.github.com>

* chore: refresh dependencies with a seven-day cooldown

Worked on by:
- @steipete

Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
OpenClaw-Publication: 591dfc0d-440f-43f7-a2e4-24a4c805aa0f

* fix: qualify dependency updates against runtime consumers

Retain npm and virtual-core versions that preserve existing bundle security and picker behavior. Migrate updated test contracts and remove the Matrix cleanup superseded by upstream.

Co-authored-by: steipete <58493+steipete@users.noreply.github.com>

* Publish openclaw/ember-langoustine-2

Worked on by:
- @steipete

Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
OpenClaw-Publication: a623131a-ce4f-4659-880a-c6220eb6942d

---------

Co-authored-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com>
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
2026-10-23 19:18:21 -07:00

203 lines
6.9 KiB
TypeScript

#!/usr/bin/env node
// Uses qualified installed tools, otherwise falls back to pinned hooks where
// possible, then runs repo-specific workflow guards.
import { spawnSync } from "node:child_process";
import { mkdtempSync, readdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const ACTIONLINT_REVISION = "011a6d15e749bb3f2d771eed9c7aa0e7e3e10ee7";
const PRE_COMMIT_VERSION = "4.7.0";
// pre-commit 4.7.0 declares requires-python >=3.10, so an older interpreter only
// fails after a venv build and a network pip install.
const PRE_COMMIT_PYTHON_FLOOR = "3.10";
const WORKFLOW_DIR = ".github/workflows";
function commandExists(command: string, args: readonly string[] = ["--version"]): boolean {
const result = spawnSync(command, args, { stdio: "ignore" });
return !result.error && result.status === 0;
}
function hasPinnedActionlint(): boolean {
const result = spawnSync("actionlint", ["--version"], { encoding: "utf8" });
if (result.error || result.status !== 0) {
return false;
}
// Released 1.7.12 can deadlock on Darwin. Only reuse the Go build of our CI
// pin; release and unknown local builds do not establish that fix's presence.
const version = result.stdout.split(/\r?\n/u, 1)[0]?.trim() ?? "";
const revision = /^v\d+\.\d+\.\d+-\d+\.\d{14}-([a-f0-9]{12})$/u.exec(version)?.[1];
if (revision === ACTIONLINT_REVISION.slice(0, 12)) {
return true;
}
console.warn(
`[check-workflows] installed actionlint does not match ${ACTIONLINT_REVISION}; using pinned fallback.`,
);
return false;
}
function probePythonVersion(
command: string,
): { runnable: false } | { runnable: true; version?: string } {
const result = spawnSync(command, ["--version"], { encoding: "utf8" });
if (result.error || result.status !== 0) {
return { runnable: false };
}
const match = /Python (\d+\.\d+(?:\.\d+)?)/u.exec(`${result.stdout ?? ""}${result.stderr ?? ""}`);
const version = match?.[1];
return version ? { runnable: true, version } : { runnable: true };
}
function isBelowPythonFloor(version: string, floor: string): boolean {
const [major = 0, minor = 0] = version.split(".").map((part) => Number(part));
const [floorMajor = 0, floorMinor = 0] = floor.split(".").map((part) => Number(part));
return major < floorMajor || (major === floorMajor && minor < floorMinor);
}
function run(command: string, args: readonly string[]): void {
const failure = runChecked(command, args);
if (failure) {
exitWithFailure(failure);
}
}
function runChecked(command: string, args: readonly string[]) {
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 exitWithFailure(failure: NonNullable<ReturnType<typeof runChecked>>): never {
if (failure.message) {
console.error(failure.message);
}
process.exit(failure.status);
}
function runGoActionlint(files: string[]): boolean {
if (!commandExists("go", ["version"])) {
return false;
}
const binDir = mkdtempSync(join(tmpdir(), "openclaw-check-workflows-actionlint-"));
let lintFailure: ReturnType<typeof runChecked> = null;
try {
const installed = spawnSync(
"go",
["install", `github.com/rhysd/actionlint/cmd/actionlint@${ACTIONLINT_REVISION}`],
{ stdio: "inherit", env: { ...process.env, GOBIN: binDir } },
);
// An unavailable pin can still use a cached hook. Lint diagnostics must stay
// terminal, so acquisition and execution cannot share a go run exit status.
if (installed.error || installed.status !== 0) {
return false;
}
lintFailure = runChecked(
join(binDir, process.platform === "win32" ? "actionlint.exe" : "actionlint"),
files,
);
} finally {
rmSync(binDir, { force: true, recursive: true });
}
if (lintFailure) {
exitWithFailure(lintFailure);
}
return true;
}
function runPreCommitFromTempVenv(hookArgs: string[]): boolean {
const pythonProbe = probePythonVersion("python3");
if (!pythonProbe.runnable) {
return false;
}
if (pythonProbe.version && isBelowPythonFloor(pythonProbe.version, PRE_COMMIT_PYTHON_FLOOR)) {
console.error(
`[check-workflows] python3 is ${pythonProbe.version}, but pre-commit ${PRE_COMMIT_VERSION} requires Python >=${PRE_COMMIT_PYTHON_FLOOR}. Install a newer python3 or a pre-commit runtime.`,
);
process.exit(1);
}
const venvDir = mkdtempSync(join(tmpdir(), "openclaw-check-workflows-pre-commit-"));
const python = join(venvDir, process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
let postVenvFailure: ReturnType<typeof runChecked> = null;
try {
const venvFailure = runChecked("python3", ["-m", "venv", venvDir]);
if (venvFailure) {
return false;
}
postVenvFailure = runChecked(python, [
"-m",
"pip",
"install",
"--disable-pip-version-check",
`pre-commit==${PRE_COMMIT_VERSION}`,
]);
if (postVenvFailure) {
return false;
}
postVenvFailure = runChecked(python, ["-m", "pre_commit", ...hookArgs]);
return !postVenvFailure;
} finally {
rmSync(venvDir, { force: true, recursive: true });
if (postVenvFailure) {
exitWithFailure(postVenvFailure);
}
}
}
function runPreCommitHook(hook: string, files: string[]): void {
const hookArgs = ["run", "--config", ".pre-commit-config.yaml", hook, "--files", ...files];
if (commandExists("pre-commit")) {
run("pre-commit", hookArgs);
return;
}
if (commandExists("python3", ["-m", "pre_commit", "--version"])) {
run("python3", ["-m", "pre_commit", ...hookArgs]);
return;
}
if (runPreCommitFromTempVenv(hookArgs)) {
return;
}
console.error(
`[check-workflows] missing pre-commit runtime for ${hook}: install pre-commit or Python venv support for pre-commit ${PRE_COMMIT_VERSION}.`,
);
process.exit(1);
}
const workflows = readdirSync(WORKFLOW_DIR)
.filter((file) => file.endsWith(".yml") || file.endsWith(".yaml"))
.toSorted()
.map((file) => join(WORKFLOW_DIR, file));
if (hasPinnedActionlint()) {
run("actionlint", workflows);
} else if (!runGoActionlint(workflows)) {
if (
commandExists("pre-commit") ||
commandExists("python3", ["-m", "pre_commit", "--version"]) ||
commandExists("python3", ["--version"])
) {
runPreCommitHook("actionlint", workflows);
} else {
console.error(
`[check-workflows] missing workflow linter: install actionlint built from ${ACTIONLINT_REVISION}, Go to acquire that revision, or a pre-commit runtime.`,
);
process.exit(1);
}
}
runPreCommitHook("zizmor", workflows);
run("node", ["scripts/generate-ci-git-owner.mts", "--check"]);
run("python3", ["scripts/check-composite-action-input-interpolation.py"]);
run("node", ["scripts/check-no-conflict-markers.mjs"]);