mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(ci): guard workflow template injection
Guard the remaining Windows Testbox workflow ref logging against GitHub Actions template injection by moving `target_ref` through step env before PowerShell reads it. Extend the local workflow check wrapper to run pinned `zizmor` across every workflow file, and keep Workflow Sanity's CI audit explicit with trusted-base pre-commit and zizmor configs for pull-request runs. Thanks @WT-WSL for the original report and patch. Co-authored-by: dev111-actor <captaintobb@outlook.com>
This commit is contained in:
2
.github/workflows/update-migration.yml
vendored
2
.github/workflows/update-migration.yml
vendored
@@ -43,4 +43,4 @@ jobs:
|
||||
published_upgrade_survivor_baselines: ${{ inputs.baselines }}
|
||||
published_upgrade_survivor_scenarios: ${{ inputs.scenarios }}
|
||||
telegram_mode: none
|
||||
secrets: inherit
|
||||
secrets: inherit # zizmor: ignore[secrets-inherit] Maintainer-dispatched package acceptance lane intentionally forwards its declared live-test secret matrix.
|
||||
|
||||
4
.github/workflows/windows-testbox-probe.yml
vendored
4
.github/workflows/windows-testbox-probe.yml
vendored
@@ -61,12 +61,14 @@ jobs:
|
||||
submodules: false
|
||||
|
||||
- name: Probe native Windows
|
||||
env:
|
||||
TARGET_REF: ${{ inputs.target_ref || github.ref }}
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
Write-Host "runner=$env:RUNNER_NAME"
|
||||
Write-Host "machine=$env:COMPUTERNAME"
|
||||
Write-Host "workspace=$env:GITHUB_WORKSPACE"
|
||||
Write-Host "target_ref=${{ inputs.target_ref || github.ref }}"
|
||||
Write-Host "target_ref=$env:TARGET_REF"
|
||||
Write-Host ("os=" + [System.Environment]::OSVersion.VersionString)
|
||||
Write-Host ("arch=" + [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)
|
||||
Write-Host ("powershell=" + $PSVersionTable.PSVersion.ToString())
|
||||
|
||||
68
.github/workflows/workflow-sanity.yml
vendored
68
.github/workflows/workflow-sanity.yml
vendored
@@ -84,6 +84,65 @@ jobs:
|
||||
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
|
||||
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Prepare trusted workflow audit configs
|
||||
if: github.event_name == 'pull_request'
|
||||
env:
|
||||
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
trusted_config="$RUNNER_TEMP/pre-commit-base.yaml"
|
||||
trusted_zizmor_config="$RUNNER_TEMP/zizmor-base.yml"
|
||||
|
||||
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
|
||||
timeout --signal=TERM --kill-after=10s 30s git fetch --no-tags --depth=1 origin \
|
||||
"+${BASE_SHA}:refs/remotes/origin/security-base" ||
|
||||
timeout --signal=TERM --kill-after=10s 30s git fetch --no-tags --depth=1 origin \
|
||||
"+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}"
|
||||
fi
|
||||
|
||||
if git cat-file -e "${BASE_SHA}:.pre-commit-config.yaml" 2>/dev/null; then
|
||||
git show "${BASE_SHA}:.pre-commit-config.yaml" > "$trusted_config"
|
||||
elif git show "refs/remotes/origin/${BASE_REF}:.pre-commit-config.yaml" \
|
||||
> "$trusted_config" 2>/dev/null; then
|
||||
echo "Base SHA ${BASE_SHA} does not expose .pre-commit-config.yaml; using origin/${BASE_REF} instead."
|
||||
else
|
||||
echo "::error title=trusted pre-commit config unavailable::Could not read .pre-commit-config.yaml from ${BASE_SHA} or origin/${BASE_REF}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if git cat-file -e "${BASE_SHA}:.github/zizmor.yml" 2>/dev/null; then
|
||||
git show "${BASE_SHA}:.github/zizmor.yml" > "$trusted_zizmor_config"
|
||||
elif git show "refs/remotes/origin/${BASE_REF}:.github/zizmor.yml" \
|
||||
> "$trusted_zizmor_config" 2>/dev/null; then
|
||||
echo "Base SHA ${BASE_SHA} does not expose .github/zizmor.yml; using origin/${BASE_REF} instead."
|
||||
else
|
||||
echo "::error title=trusted zizmor config unavailable::Could not read .github/zizmor.yml from ${BASE_SHA} or origin/${BASE_REF}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - "$trusted_config" "$trusted_zizmor_config" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
config_path = Path(sys.argv[1])
|
||||
zizmor_config_path = sys.argv[2]
|
||||
text = config_path.read_text()
|
||||
if ".github/zizmor.yml" not in text:
|
||||
raise SystemExit("trusted pre-commit config does not reference .github/zizmor.yml")
|
||||
config_path.write_text(text.replace(".github/zizmor.yml", zizmor_config_path))
|
||||
PY
|
||||
|
||||
echo "PRE_COMMIT_CONFIG_PATH=$trusted_config" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install pre-commit
|
||||
run: python -m pip install --disable-pip-version-check pre-commit==4.2.0
|
||||
|
||||
- name: Install actionlint
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -103,6 +162,15 @@ jobs:
|
||||
- name: Lint workflows
|
||||
run: actionlint
|
||||
|
||||
- name: Audit all workflows with zizmor
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mapfile -t workflow_files < <(
|
||||
find .github/workflows -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) | sort
|
||||
)
|
||||
pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" zizmor --files "${workflow_files[@]}"
|
||||
|
||||
- name: Disallow direct inputs interpolation in composite run blocks
|
||||
run: python3 scripts/check-composite-action-input-interpolation.py
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ OpenClaw CI runs on every push to `main` and every pull request. The `preflight`
|
||||
| Job | Purpose | When it runs |
|
||||
| ---------------------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------- |
|
||||
| `preflight` | Detect docs-only changes, changed scopes, changed extensions, and build the CI manifest | Always on non-draft pushes and PRs |
|
||||
| `security-fast` | Private key detection, workflow audit via `zizmor`, and production lockfile audit | Always on non-draft pushes and PRs |
|
||||
| `security-fast` | Private key detection, changed-workflow audit via `zizmor`, and production lockfile audit | Always on non-draft pushes and PRs |
|
||||
| `check-dependencies` | Production Knip dependency-only pass plus the unused-file allowlist guard | Node-relevant changes |
|
||||
| `build-artifacts` | Build `dist/`, Control UI, built-CLI smoke checks, embedded built-artifact checks, and reusable artifacts | Node-relevant changes |
|
||||
| `checks-fast-core` | Fast Linux correctness lanes such as bundled, protocol, and CI-routing checks | Node-relevant changes |
|
||||
@@ -80,6 +80,7 @@ apply to that PR.
|
||||
Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`. Manual dispatch skips changed-scope detection and makes the preflight manifest act as if every scoped area changed.
|
||||
|
||||
- **CI workflow edits** validate the Node CI graph plus workflow linting, but do not force Windows, Android, or macOS native builds by themselves; those platform lanes stay scoped to platform source changes.
|
||||
- **Workflow Sanity** runs `actionlint`, `zizmor` over all workflow YAML files, the composite-action interpolation guard, and the conflict-marker guard. The PR-scoped `security-fast` job also runs `zizmor` over changed workflow files so workflow security findings fail early in the main CI graph.
|
||||
- **Docs on `main` pushes** are checked by the standalone `Docs` workflow with the same ClawHub docs mirror used by CI, so mixed code+docs pushes do not also queue the CI `check-docs` shard. Pull requests and manual CI still run `check-docs` from CI when docs changed.
|
||||
- **TUI PTY** is a focused workflow for TUI changes. It runs `node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts` on Linux Node 24 for `src/tui/**`, the watch harness, package script, lockfile, and workflow edits. The required lane uses a deterministic `TuiBackend` fixture; the slower `tui --local` smoke is opt-in with `OPENCLAW_TUI_PTY_INCLUDE_LOCAL=1` and mocks only the external model endpoint.
|
||||
- **CI routing-only edits, selected cheap core-test fixture edits, and narrow plugin contract helper/test-routing edits** use a fast Node-only manifest path: `preflight`, security, and a single `checks-fast-core` task. That path skips build artifacts, Node 22 compatibility, channel contracts, full core shards, bundled-plugin shards, and additional guard matrices when the change is limited to the routing or helper surfaces the fast task exercises directly.
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
// Runs local workflow sanity checks.
|
||||
// Uses an installed actionlint when present, otherwise falls back to `go run`
|
||||
// for the pinned version used by CI, then runs repo-specific composite guards.
|
||||
// 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 { join } from "node:path";
|
||||
|
||||
const ACTIONLINT_VERSION = "1.7.11";
|
||||
const WORKFLOW_DIR = ".github/workflows";
|
||||
|
||||
function commandExists(command, args = ["--version"]) {
|
||||
const result = spawnSync(command, args, { stdio: "ignore" });
|
||||
@@ -22,16 +25,49 @@ function run(command, args) {
|
||||
}
|
||||
}
|
||||
|
||||
if (commandExists("actionlint")) {
|
||||
run("actionlint", []);
|
||||
} else if (commandExists("go", ["version"])) {
|
||||
run("go", ["run", `github.com/rhysd/actionlint/cmd/actionlint@v${ACTIONLINT_VERSION}`]);
|
||||
} else {
|
||||
function workflowFiles() {
|
||||
return readdirSync(WORKFLOW_DIR)
|
||||
.filter((file) => file.endsWith(".yml") || file.endsWith(".yaml"))
|
||||
.toSorted()
|
||||
.map((file) => join(WORKFLOW_DIR, file));
|
||||
}
|
||||
|
||||
function runPreCommitHook(hook, files) {
|
||||
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;
|
||||
}
|
||||
|
||||
console.error(
|
||||
`[check-workflows] missing workflow linter: install actionlint or Go ${ACTIONLINT_VERSION} fallback support.`,
|
||||
`[check-workflows] missing pre-commit runtime for ${hook}: install pre-commit or python3 pre_commit.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const workflows = workflowFiles();
|
||||
|
||||
if (commandExists("actionlint")) {
|
||||
run("actionlint", workflows);
|
||||
} else if (commandExists("go", ["version"])) {
|
||||
run("go", ["run", `github.com/rhysd/actionlint/cmd/actionlint@v${ACTIONLINT_VERSION}`]);
|
||||
} else if (
|
||||
commandExists("pre-commit") ||
|
||||
commandExists("python3", ["-m", "pre_commit", "--version"])
|
||||
) {
|
||||
runPreCommitHook("actionlint", workflows);
|
||||
} else {
|
||||
console.error(
|
||||
`[check-workflows] missing workflow linter: install actionlint, Go ${ACTIONLINT_VERSION} fallback support, or pre-commit.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
runPreCommitHook("zizmor", workflows);
|
||||
|
||||
run("python3", ["scripts/check-composite-action-input-interpolation.py"]);
|
||||
run("node", ["scripts/check-no-conflict-markers.mjs"]);
|
||||
|
||||
@@ -18,14 +18,15 @@ describe("check-workflows", () => {
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("missing workflow linter");
|
||||
expect(result.stderr).toContain("install actionlint or Go");
|
||||
expect(result.stderr).toContain("install actionlint, Go");
|
||||
});
|
||||
|
||||
it("uses the pinned go fallback when actionlint is unavailable", () => {
|
||||
it("uses the pinned go fallback and audits all workflows with zizmor", () => {
|
||||
const tempDir = mkdtempSync(path.join(os.tmpdir(), "check-workflows-"));
|
||||
try {
|
||||
const binDir = path.join(tempDir, "bin");
|
||||
const markerPath = path.join(tempDir, "go-run.txt");
|
||||
const preCommitMarkerPath = path.join(tempDir, "pre-commit.txt");
|
||||
mkdirSync(binDir);
|
||||
writeFileSync(
|
||||
path.join(binDir, "go"),
|
||||
@@ -38,6 +39,17 @@ describe("check-workflows", () => {
|
||||
].join("\n"),
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(binDir, "pre-commit"),
|
||||
[
|
||||
"#!/bin/sh",
|
||||
'if [ "$1" = "--version" ]; then exit 0; fi',
|
||||
'printf "%s\\n" "$*" >> "$PRE_COMMIT_MARKER"',
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
for (const command of ["python3", "node"]) {
|
||||
writeFileSync(path.join(binDir, command), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
||||
}
|
||||
@@ -47,6 +59,7 @@ describe("check-workflows", () => {
|
||||
env: {
|
||||
...process.env,
|
||||
GO_FALLBACK_MARKER: markerPath,
|
||||
PRE_COMMIT_MARKER: preCommitMarkerPath,
|
||||
PATH: binDir,
|
||||
},
|
||||
});
|
||||
@@ -55,6 +68,10 @@ describe("check-workflows", () => {
|
||||
expect(readFileSync(markerPath, "utf8")).toContain(
|
||||
"github.com/rhysd/actionlint/cmd/actionlint@v1.7.11",
|
||||
);
|
||||
const preCommitArgs = readFileSync(preCommitMarkerPath, "utf8");
|
||||
expect(preCommitArgs).toContain("run --config .pre-commit-config.yaml zizmor --files");
|
||||
expect(preCommitArgs).toContain(".github/workflows/ci.yml");
|
||||
expect(preCommitArgs).toContain(".github/workflows/windows-testbox-probe.yml");
|
||||
} finally {
|
||||
rmSync(tempDir, { force: true, recursive: true });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user