ci(testbox): provision pinned TruffleHog (#112482)

* ci(testbox): provision pinned TruffleHog

* fix(ci): preserve existing TruffleHog bin directory

* test(ci): cover TruffleHog target routing
This commit is contained in:
Patrick Erichsen
2026-07-21 20:14:58 -07:00
committed by GitHub
parent d3430cae80
commit d4a9bbbe87
8 changed files with 319 additions and 4 deletions
@@ -11,6 +11,10 @@ inputs:
description: Whether to install Bun alongside Node.
required: false
default: "true"
install-trufflehog:
description: Whether to install the pinned TruffleHog binary for a reusable test environment.
required: false
default: "false"
install-deps:
description: Whether to run pnpm install after environment setup.
required: false
@@ -97,6 +101,11 @@ runs:
node-version: ${{ inputs.node-version }}
use-actions-cache: ${{ inputs.use-actions-cache }}
- name: Setup TruffleHog
if: inputs.install-trufflehog == 'true'
shell: bash
run: bash scripts/install-trufflehog.sh
- name: Validate sticky pnpm layout
if: inputs.sticky-disk == 'true'
shell: bash
@@ -91,6 +91,7 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
install-trufflehog: "true"
- name: Resolve release dist cache seeds
id: dist-cache-seeds
@@ -105,6 +105,7 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
install-trufflehog: "true"
- name: Prepare Testbox shell
shell: bash
run: |
+1
View File
@@ -97,6 +97,7 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
install-trufflehog: "true"
- name: Prepare Testbox shell
shell: bash
run: |
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env bash
set -euo pipefail
trufflehog_version="3.95.9"
trufflehog_bin_dir="${OPENCLAW_TRUFFLEHOG_BIN_DIR:-/usr/local/bin}"
log() {
printf 'trufflehog-install: %s\n' "$*" >&2
}
run_as_root() {
local writable_ancestor="$trufflehog_bin_dir"
while [[ ! -e "$writable_ancestor" && "$writable_ancestor" != "/" ]]; do
writable_ancestor="$(dirname "$writable_ancestor")"
done
if [[ "$(id -u)" -eq 0 || -w "$trufflehog_bin_dir" || -w "$writable_ancestor" ]]; then
"$@"
return
fi
if ! command -v sudo >/dev/null 2>&1; then
log "sudo is required to install into $trufflehog_bin_dir"
return 1
fi
sudo "$@"
}
ensure_trufflehog_bin_dir() {
if [[ -d "$trufflehog_bin_dir" ]]; then
return 0
fi
if [[ -e "$trufflehog_bin_dir" ]]; then
log "install path exists but is not a directory: $trufflehog_bin_dir"
return 1
fi
run_as_root install -d -m 0755 "$trufflehog_bin_dir"
}
trufflehog_arch() {
case "$1" in
x86_64 | amd64) printf '%s\n' "amd64" ;;
aarch64 | arm64) printf '%s\n' "arm64" ;;
*)
log "unsupported Linux architecture: $1"
return 1
;;
esac
}
trufflehog_sha256() {
case "$1" in
amd64) printf '%s\n' "f6d1106b85107d79527ed7a5b98b592beadd8b770dc3c9e8c1ad99e1b2cf127e" ;;
arm64) printf '%s\n' "9d9c2ec4ea36a089a9c5aaafe1969d176013ddf9f44d68e8cd75291aed8c83ed" ;;
*)
log "unsupported TruffleHog architecture: $1"
return 1
;;
esac
}
trufflehog_binary_ready() {
local binary="$1"
[[ -x "$binary" ]] &&
"$binary" --no-update --version 2>/dev/null |
awk -v version="$trufflehog_version" '
{
for (field = 1; field <= NF; field++) {
if ($field == version) {
found = 1
}
}
}
END { exit found ? 0 : 1 }
'
}
install_trufflehog() {
local arch archive candidate checksum target tmp_dir url
if [[ "$(uname -s)" != "Linux" ]]; then
log "this installer supports Linux Testbox environments only"
return 1
fi
target="$trufflehog_bin_dir/trufflehog"
if trufflehog_binary_ready "$target"; then
log "TruffleHog $trufflehog_version is already installed"
return 0
fi
arch="$(trufflehog_arch "$(uname -m)")"
checksum="$(trufflehog_sha256 "$arch")"
archive="trufflehog_${trufflehog_version}_linux_${arch}.tar.gz"
url="https://github.com/trufflesecurity/trufflehog/releases/download/v${trufflehog_version}/${archive}"
tmp_dir="$(mktemp -d)"
if ! curl -fsSL --retry 3 --output "$tmp_dir/$archive" "$url" ||
! (
cd "$tmp_dir"
printf '%s %s\n' "$checksum" "$archive" | sha256sum -c -
) ||
! tar --no-same-owner -xzf "$tmp_dir/$archive" -C "$tmp_dir" trufflehog; then
rm -rf "$tmp_dir"
return 1
fi
ensure_trufflehog_bin_dir
candidate="$(run_as_root mktemp "${target}.tmp.XXXXXX")"
if ! run_as_root install -m 0755 "$tmp_dir/trufflehog" "$candidate" ||
! trufflehog_binary_ready "$candidate" ||
! run_as_root mv -f "$candidate" "$target"; then
run_as_root rm -f "$candidate" || true
rm -rf "$tmp_dir"
return 1
fi
rm -rf "$tmp_dir"
log "installed TruffleHog $trufflehog_version at $target"
}
if [[ "${OPENCLAW_TRUFFLEHOG_SOURCE_ONLY:-0}" == "1" ]]; then
return 0 2>/dev/null || exit 0
fi
install_trufflehog
+13 -4
View File
@@ -474,15 +474,19 @@ const GITHUB_YAML_PINNING_GUARD_TEST_TARGETS = ["test/scripts/ci-workflow-guards
const GITHUB_WORKFLOW_OWNER_TEST_TARGETS = new Map([
[
".github/workflows/ci-build-artifacts-testbox.yml",
["test/scripts/package-acceptance-workflow.test.ts"],
["test/scripts/install-trufflehog.test.ts", "test/scripts/package-acceptance-workflow.test.ts"],
],
[
".github/workflows/ci-check-arm-testbox.yml",
["test/scripts/package-acceptance-workflow.test.ts"],
["test/scripts/install-trufflehog.test.ts", "test/scripts/package-acceptance-workflow.test.ts"],
],
[
".github/workflows/ci-check-testbox.yml",
["test/scripts/changed-lanes.test.ts", "test/scripts/package-acceptance-workflow.test.ts"],
[
"test/scripts/changed-lanes.test.ts",
"test/scripts/install-trufflehog.test.ts",
"test/scripts/package-acceptance-workflow.test.ts",
],
],
[
".github/workflows/ci.yml",
@@ -713,7 +717,11 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
[".github/actions/ensure-base-commit/action.yml", ["test/scripts/ci-workflow-guards.test.ts"]],
[
".github/actions/setup-node-env/action.yml",
["test/scripts/package-acceptance-workflow.test.ts", "test/scripts/ci-workflow-guards.test.ts"],
[
"test/scripts/install-trufflehog.test.ts",
"test/scripts/package-acceptance-workflow.test.ts",
"test/scripts/ci-workflow-guards.test.ts",
],
],
[
".github/actions/setup-node-env/dependency-fingerprint.mjs",
@@ -1102,6 +1110,7 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
],
["scripts/github/resolve-openclaw-ref.sh", ["test/scripts/resolve-openclaw-ref.test.ts"]],
["scripts/ci-hydrate-testbox-env.sh", ["test/scripts/ci-hydrate-testbox-env.test.ts"]],
["scripts/install-trufflehog.sh", ["test/scripts/install-trufflehog.test.ts"]],
[
"scripts/github/run-openclaw-cross-os-release-checks.sh",
["test/scripts/openclaw-cross-os-release-workflow.test.ts"],
+155
View File
@@ -0,0 +1,155 @@
import { execFileSync } from "node:child_process";
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js";
const SCRIPT = "scripts/install-trufflehog.sh";
const tempDirs = new Set<string>();
afterEach(() => {
cleanupTempDirs(tempDirs);
});
function runBash(command: string, env: NodeJS.ProcessEnv = {}): string {
return execFileSync("/bin/bash", ["--noprofile", "--norc", "-c", command], {
cwd: process.cwd(),
encoding: "utf8",
env: {
...process.env,
OPENCLAW_TRUFFLEHOG_SOURCE_ONLY: "1",
...env,
},
stdio: ["ignore", "pipe", "pipe"],
});
}
describe("scripts/install-trufflehog.sh", () => {
it("is an opt-in shared environment setup capability", () => {
const action = readFileSync(".github/actions/setup-node-env/action.yml", "utf8");
expect(action).toContain("install-trufflehog:");
expect(action).toContain("if: inputs.install-trufflehog == 'true'");
expect(action).toContain("run: bash scripts/install-trufflehog.sh");
});
it("is enabled during every Linux Testbox hydration before handoff", () => {
for (const workflow of [
".github/workflows/ci-check-testbox.yml",
".github/workflows/ci-check-arm-testbox.yml",
".github/workflows/ci-build-artifacts-testbox.yml",
]) {
const text = readFileSync(workflow, "utf8");
const install = text.indexOf('install-trufflehog: "true"');
const handoff = text.indexOf("uses: useblacksmith/run-testbox@");
expect(install, `${workflow} must provision TruffleHog`).toBeGreaterThanOrEqual(0);
expect(handoff, `${workflow} must hand off to run-testbox`).toBeGreaterThan(install);
}
});
it("pins the reviewed Linux checksums for both Testbox architectures", () => {
const output = runBash(
[
`source ${SCRIPT}`,
"printf 'amd64=%s\\n' \"$(trufflehog_sha256 amd64)\"",
"printf 'arm64=%s\\n' \"$(trufflehog_sha256 arm64)\"",
].join("\n"),
);
expect(output).toContain(
"amd64=f6d1106b85107d79527ed7a5b98b592beadd8b770dc3c9e8c1ad99e1b2cf127e",
);
expect(output).toContain(
"arm64=9d9c2ec4ea36a089a9c5aaafe1969d176013ddf9f44d68e8cd75291aed8c83ed",
);
});
it("does not download TruffleHog again when the pinned version is installed", () => {
const root = makeTempDir(tempDirs, "openclaw-trufflehog-install-");
const binDir = join(root, "bin");
const downloadMarker = join(root, "downloaded");
mkdirSync(binDir);
const trufflehog = join(binDir, "trufflehog");
writeFileSync(trufflehog, "#!/bin/sh\nprintf 'trufflehog 3.95.9\\n'\n");
chmodSync(trufflehog, 0o755);
const fakeCurl = join(binDir, "curl");
writeFileSync(
fakeCurl,
`#!/bin/sh\nprintf downloaded >${JSON.stringify(downloadMarker)}\nexit 99\n`,
);
chmodSync(fakeCurl, 0o755);
const fakeUname = join(binDir, "uname");
writeFileSync(
fakeUname,
'#!/bin/sh\nif [ "$1" = "-s" ]; then printf "Linux\\n"; else printf "x86_64\\n"; fi\n',
);
chmodSync(fakeUname, 0o755);
runBash(`source ${SCRIPT}\ninstall_trufflehog`, {
OPENCLAW_TRUFFLEHOG_BIN_DIR: binDir,
PATH: `${binDir}:${process.env.PATH ?? ""}`,
});
expect(existsSync(downloadMarker)).toBe(false);
expect(readFileSync(trufflehog, "utf8")).toContain("3.95.9");
});
it("creates a missing user-writable install directory without sudo", () => {
const root = makeTempDir(tempDirs, "openclaw-trufflehog-user-bin-");
const binDir = join(root, "nested", "bin");
const fakeBin = join(root, "fake-bin");
const sudoMarker = join(root, "sudo-used");
mkdirSync(fakeBin);
const fakeSudo = join(fakeBin, "sudo");
writeFileSync(fakeSudo, `#!/bin/sh\nprintf used >${JSON.stringify(sudoMarker)}\nexit 99\n`);
chmodSync(fakeSudo, 0o755);
runBash(`source ${SCRIPT}\nrun_as_root mkdir -p "$OPENCLAW_TRUFFLEHOG_BIN_DIR"`, {
OPENCLAW_TRUFFLEHOG_BIN_DIR: binDir,
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
});
expect(existsSync(binDir)).toBe(true);
expect(existsSync(sudoMarker)).toBe(false);
});
it("does not change permissions on an existing writable install directory", () => {
const root = makeTempDir(tempDirs, "openclaw-trufflehog-existing-bin-");
const binDir = join(root, "bin");
const fakeBin = join(root, "fake-bin");
const installMarker = join(root, "install-used");
mkdirSync(binDir);
mkdirSync(fakeBin);
const fakeInstall = join(fakeBin, "install");
writeFileSync(
fakeInstall,
`#!/bin/sh\nprintf used >${JSON.stringify(installMarker)}\nexit 99\n`,
);
chmodSync(fakeInstall, 0o755);
runBash(`source ${SCRIPT}\nensure_trufflehog_bin_dir`, {
OPENCLAW_TRUFFLEHOG_BIN_DIR: binDir,
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
});
expect(existsSync(installMarker)).toBe(false);
});
it("verifies the archive before extraction and replaces the binary atomically", () => {
const script = readFileSync(SCRIPT, "utf8");
expect(script).toContain('"$binary" --no-update --version');
const download = script.indexOf('curl -fsSL --retry 3 --output "$tmp_dir/$archive" "$url"');
const verify = script.indexOf("sha256sum -c -");
const extract = script.indexOf(
'tar --no-same-owner -xzf "$tmp_dir/$archive" -C "$tmp_dir" trufflehog',
);
const validate = script.indexOf('trufflehog_binary_ready "$candidate"');
const replace = script.indexOf('mv -f "$candidate" "$target"');
expect(download).toBeGreaterThanOrEqual(0);
expect(verify).toBeGreaterThan(download);
expect(extract).toBeGreaterThan(verify);
expect(validate).toBeGreaterThan(extract);
expect(replace).toBeGreaterThan(validate);
});
});
+15
View File
@@ -362,6 +362,10 @@ describe("scripts/test-projects changed-target routing", () => {
mode: "targets",
targets: ["test/scripts/check-file-utils.test.ts"],
});
expect(resolveChangedTestTargetPlan(["scripts/install-trufflehog.sh"])).toEqual({
mode: "targets",
targets: ["test/scripts/install-trufflehog.test.ts"],
});
});
it("routes nested scripts through conventional owner tests", () => {
@@ -1369,6 +1373,7 @@ describe("scripts/test-projects changed-target routing", () => {
"test/scripts/ci-workflow-guards.test.ts",
"test/scripts/package-acceptance-workflow.test.ts",
"test/scripts/changed-lanes.test.ts",
"test/scripts/install-trufflehog.test.ts",
],
],
[
@@ -1376,6 +1381,15 @@ describe("scripts/test-projects changed-target routing", () => {
[
"test/scripts/ci-workflow-guards.test.ts",
"test/scripts/package-acceptance-workflow.test.ts",
"test/scripts/install-trufflehog.test.ts",
],
],
[
".github/workflows/ci-build-artifacts-testbox.yml",
[
"test/scripts/install-trufflehog.test.ts",
"test/scripts/package-acceptance-workflow.test.ts",
"test/scripts/ci-workflow-guards.test.ts",
],
],
[
@@ -1978,6 +1992,7 @@ describe("scripts/test-projects changed-target routing", () => {
[
".github/actions/setup-node-env/action.yml",
[
"test/scripts/install-trufflehog.test.ts",
"test/scripts/package-acceptance-workflow.test.ts",
"test/scripts/ci-workflow-guards.test.ts",
],