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_baselines: ${{ inputs.baselines }}
|
||||||
published_upgrade_survivor_scenarios: ${{ inputs.scenarios }}
|
published_upgrade_survivor_scenarios: ${{ inputs.scenarios }}
|
||||||
telegram_mode: none
|
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
|
submodules: false
|
||||||
|
|
||||||
- name: Probe native Windows
|
- name: Probe native Windows
|
||||||
|
env:
|
||||||
|
TARGET_REF: ${{ inputs.target_ref || github.ref }}
|
||||||
run: |
|
run: |
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
Write-Host "runner=$env:RUNNER_NAME"
|
Write-Host "runner=$env:RUNNER_NAME"
|
||||||
Write-Host "machine=$env:COMPUTERNAME"
|
Write-Host "machine=$env:COMPUTERNAME"
|
||||||
Write-Host "workspace=$env:GITHUB_WORKSPACE"
|
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 ("os=" + [System.Environment]::OSVersion.VersionString)
|
||||||
Write-Host ("arch=" + [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)
|
Write-Host ("arch=" + [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)
|
||||||
Write-Host ("powershell=" + $PSVersionTable.PSVersion.ToString())
|
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"
|
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
|
||||||
git -C "$GITHUB_WORKSPACE" checkout --detach 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
|
- name: Install actionlint
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@@ -103,6 +162,15 @@ jobs:
|
|||||||
- name: Lint workflows
|
- name: Lint workflows
|
||||||
run: actionlint
|
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
|
- name: Disallow direct inputs interpolation in composite run blocks
|
||||||
run: python3 scripts/check-composite-action-input-interpolation.py
|
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 |
|
| 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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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.
|
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.
|
- **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.
|
- **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.
|
- **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.
|
- **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
|
#!/usr/bin/env node
|
||||||
// Runs local workflow sanity checks.
|
// Runs local workflow sanity checks.
|
||||||
// Uses an installed actionlint when present, otherwise falls back to `go run`
|
// Uses installed tools when present, otherwise falls back to pinned hooks where
|
||||||
// for the pinned version used by CI, then runs repo-specific composite guards.
|
// possible, then runs repo-specific workflow guards.
|
||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { readdirSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
const ACTIONLINT_VERSION = "1.7.11";
|
const ACTIONLINT_VERSION = "1.7.11";
|
||||||
|
const WORKFLOW_DIR = ".github/workflows";
|
||||||
|
|
||||||
function commandExists(command, args = ["--version"]) {
|
function commandExists(command, args = ["--version"]) {
|
||||||
const result = spawnSync(command, args, { stdio: "ignore" });
|
const result = spawnSync(command, args, { stdio: "ignore" });
|
||||||
@@ -22,16 +25,49 @@ function run(command, args) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commandExists("actionlint")) {
|
function workflowFiles() {
|
||||||
run("actionlint", []);
|
return readdirSync(WORKFLOW_DIR)
|
||||||
} else if (commandExists("go", ["version"])) {
|
.filter((file) => file.endsWith(".yml") || file.endsWith(".yaml"))
|
||||||
run("go", ["run", `github.com/rhysd/actionlint/cmd/actionlint@v${ACTIONLINT_VERSION}`]);
|
.toSorted()
|
||||||
} else {
|
.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(
|
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);
|
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("python3", ["scripts/check-composite-action-input-interpolation.py"]);
|
||||||
run("node", ["scripts/check-no-conflict-markers.mjs"]);
|
run("node", ["scripts/check-no-conflict-markers.mjs"]);
|
||||||
|
|||||||
@@ -18,14 +18,15 @@ describe("check-workflows", () => {
|
|||||||
|
|
||||||
expect(result.status).toBe(1);
|
expect(result.status).toBe(1);
|
||||||
expect(result.stderr).toContain("missing workflow linter");
|
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-"));
|
const tempDir = mkdtempSync(path.join(os.tmpdir(), "check-workflows-"));
|
||||||
try {
|
try {
|
||||||
const binDir = path.join(tempDir, "bin");
|
const binDir = path.join(tempDir, "bin");
|
||||||
const markerPath = path.join(tempDir, "go-run.txt");
|
const markerPath = path.join(tempDir, "go-run.txt");
|
||||||
|
const preCommitMarkerPath = path.join(tempDir, "pre-commit.txt");
|
||||||
mkdirSync(binDir);
|
mkdirSync(binDir);
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
path.join(binDir, "go"),
|
path.join(binDir, "go"),
|
||||||
@@ -38,6 +39,17 @@ describe("check-workflows", () => {
|
|||||||
].join("\n"),
|
].join("\n"),
|
||||||
{ mode: 0o755 },
|
{ 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"]) {
|
for (const command of ["python3", "node"]) {
|
||||||
writeFileSync(path.join(binDir, command), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
writeFileSync(path.join(binDir, command), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
||||||
}
|
}
|
||||||
@@ -47,6 +59,7 @@ describe("check-workflows", () => {
|
|||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
GO_FALLBACK_MARKER: markerPath,
|
GO_FALLBACK_MARKER: markerPath,
|
||||||
|
PRE_COMMIT_MARKER: preCommitMarkerPath,
|
||||||
PATH: binDir,
|
PATH: binDir,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -55,6 +68,10 @@ describe("check-workflows", () => {
|
|||||||
expect(readFileSync(markerPath, "utf8")).toContain(
|
expect(readFileSync(markerPath, "utf8")).toContain(
|
||||||
"github.com/rhysd/actionlint/cmd/actionlint@v1.7.11",
|
"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 {
|
} finally {
|
||||||
rmSync(tempDir, { force: true, recursive: true });
|
rmSync(tempDir, { force: true, recursive: true });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user