mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-14 11:56:06 +00:00
* feat(tooling): enforce indexed access checks in root tests * style(tooling): clarify scoped package guard
472 lines
14 KiB
TypeScript
472 lines
14 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, resolve } from "node:path";
|
|
import { expectDefined } from "@openclaw/normalization-core";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const HELPER = resolve("scripts/docker/shared-image-artifact.sh");
|
|
const TARGET_SHA = "a".repeat(40);
|
|
const WORKFLOW_SHA = "b".repeat(40);
|
|
const PACKAGE_SHA256 = "c".repeat(64);
|
|
const ARTIFACT_DIGEST = "d".repeat(64);
|
|
const ARTIFACT_ID = "789";
|
|
const ARTIFACT_NAME = "docker-e2e-shared-images-release-aabbccddeeff-123456-2";
|
|
const ARTIFACT_RUN_ATTEMPT = "2";
|
|
const ARTIFACT_RUN_ID = "123456";
|
|
const IMAGE_REFS = ["openclaw-docker-e2e-bare:pkg-test", "openclaw-docker-e2e-functional:pkg-test"];
|
|
|
|
function imageId(ref: string): string {
|
|
return `sha256:${createHash("sha256").update(ref).digest("hex")}`;
|
|
}
|
|
|
|
function writeExecutable(path: string, contents: string): void {
|
|
writeFileSync(path, contents);
|
|
chmodSync(path, 0o755);
|
|
}
|
|
|
|
function runHelper(params: {
|
|
artifactDir: string;
|
|
command: "load" | "pack";
|
|
env: NodeJS.ProcessEnv;
|
|
imageRefs?: string[];
|
|
}) {
|
|
return spawnSync(
|
|
"bash",
|
|
[
|
|
HELPER,
|
|
params.command,
|
|
params.artifactDir,
|
|
"docker-e2e",
|
|
TARGET_SHA,
|
|
WORKFLOW_SHA,
|
|
...(params.imageRefs ?? IMAGE_REFS),
|
|
],
|
|
{
|
|
encoding: "utf8",
|
|
env: params.env,
|
|
},
|
|
);
|
|
}
|
|
|
|
function verifyUploadedArtifact(
|
|
fixture: ReturnType<typeof createFixture>,
|
|
params: {
|
|
artifactDigest?: string;
|
|
artifactName?: string;
|
|
env?: NodeJS.ProcessEnv;
|
|
runAttempt?: string;
|
|
runId?: string;
|
|
} = {},
|
|
) {
|
|
return spawnSync(
|
|
"bash",
|
|
[
|
|
HELPER,
|
|
"verify-upload",
|
|
"Docker E2E image",
|
|
ARTIFACT_ID,
|
|
params.artifactName ?? ARTIFACT_NAME,
|
|
params.artifactDigest ?? ARTIFACT_DIGEST,
|
|
params.runId ?? ARTIFACT_RUN_ID,
|
|
params.runAttempt ?? ARTIFACT_RUN_ATTEMPT,
|
|
],
|
|
{
|
|
encoding: "utf8",
|
|
env: { ...fixture.env, ...params.env },
|
|
},
|
|
);
|
|
}
|
|
|
|
function createFixture() {
|
|
const root = mkdtempSync(join(tmpdir(), "openclaw-shared-image-artifact-"));
|
|
const bin = join(root, "bin");
|
|
const artifactDir = join(root, "artifact");
|
|
const dockerLog = join(root, "docker.log");
|
|
const ghLog = join(root, "gh.log");
|
|
mkdirSync(bin);
|
|
writeFileSync(dockerLog, "");
|
|
writeFileSync(ghLog, "");
|
|
|
|
writeExecutable(
|
|
join(bin, "docker"),
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
printf '%s\\n' "$*" >> "$FAKE_DOCKER_LOG"
|
|
|
|
image_id() {
|
|
printf '%s' "$1" | sha256sum | awk '{print "sha256:" $1}'
|
|
}
|
|
|
|
if [[ "$1" == "image" && "$2" == "inspect" ]]; then
|
|
ref="\${5:?image ref required}"
|
|
id="$(image_id "$ref")"
|
|
if [[ "\${FAKE_DOCKER_FORCE_ID_MISMATCH:-0}" == "1" ]]; then
|
|
id="sha256:$(printf 'mismatch:%s' "$ref" | sha256sum | awk '{print $1}')"
|
|
fi
|
|
printf '%s\\n' "$id"
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "$1" == "image" && "$2" == "save" ]]; then
|
|
shift 2
|
|
output=""
|
|
refs=()
|
|
while [[ "$#" -gt 0 ]]; do
|
|
case "$1" in
|
|
--output)
|
|
output="\${2:?output path required}"
|
|
shift 2
|
|
;;
|
|
*)
|
|
refs+=("$1")
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
: > "$output"
|
|
for ref in "\${refs[@]}"; do
|
|
printf '%s\\t%s\\n' "$ref" "$(image_id "$ref")" >> "$output"
|
|
done
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "$1" == "image" && "$2" == "load" ]]; then
|
|
cat >/dev/null
|
|
exit 0
|
|
fi
|
|
|
|
echo "unexpected docker invocation: $*" >&2
|
|
exit 2
|
|
`,
|
|
);
|
|
|
|
writeExecutable(
|
|
join(bin, "zstd"),
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
if [[ "$1" == "-t" ]]; then
|
|
test -s "$2"
|
|
exit 0
|
|
fi
|
|
if [[ "$1" == "-d" && "$2" == "--stdout" ]]; then
|
|
cat "$3"
|
|
exit 0
|
|
fi
|
|
|
|
source_path=""
|
|
output_path=""
|
|
while [[ "$#" -gt 0 ]]; do
|
|
case "$1" in
|
|
-f)
|
|
source_path="\${2:?source path required}"
|
|
shift 2
|
|
;;
|
|
-o)
|
|
output_path="\${2:?output path required}"
|
|
shift 2
|
|
;;
|
|
*)
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
cp "$source_path" "$output_path"
|
|
`,
|
|
);
|
|
|
|
writeExecutable(
|
|
join(bin, "gh"),
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
printf '%s\\n' "$*" >> "$FAKE_GH_LOG"
|
|
[[ "$1" == "api" ]] || {
|
|
echo "unexpected gh invocation: $*" >&2
|
|
exit 2
|
|
}
|
|
path="$2"
|
|
case "$path" in
|
|
"repos/\${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")
|
|
printf '{"id":%s,"name":"%s","expired":%s,"digest":"sha256:%s","workflow_run":{"id":%s}}\\n' \
|
|
"$FAKE_ARTIFACT_ID" "$FAKE_ARTIFACT_NAME" "$FAKE_ARTIFACT_EXPIRED" \
|
|
"$FAKE_ARTIFACT_DIGEST" "$FAKE_ARTIFACT_RUN_ID"
|
|
;;
|
|
"repos/\${GITHUB_REPOSITORY}/actions/runs/${ARTIFACT_RUN_ID}/attempts/${ARTIFACT_RUN_ATTEMPT}")
|
|
printf '{"id":%s,"run_attempt":%s}\\n' \
|
|
"$FAKE_ATTEMPT_RUN_ID" "$FAKE_ARTIFACT_RUN_ATTEMPT"
|
|
;;
|
|
*)
|
|
echo "unexpected gh api path: $path" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
`,
|
|
);
|
|
|
|
const env = {
|
|
...process.env,
|
|
FAKE_ARTIFACT_DIGEST: ARTIFACT_DIGEST,
|
|
FAKE_ARTIFACT_EXPIRED: "false",
|
|
FAKE_ARTIFACT_ID: ARTIFACT_ID,
|
|
FAKE_ARTIFACT_NAME: ARTIFACT_NAME,
|
|
FAKE_ARTIFACT_RUN_ATTEMPT: ARTIFACT_RUN_ATTEMPT,
|
|
FAKE_ARTIFACT_RUN_ID: ARTIFACT_RUN_ID,
|
|
FAKE_ATTEMPT_RUN_ID: ARTIFACT_RUN_ID,
|
|
FAKE_DOCKER_LOG: dockerLog,
|
|
FAKE_GH_LOG: ghLog,
|
|
GH_TOKEN: "test-token",
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
GITHUB_RUN_ATTEMPT: "2",
|
|
GITHUB_RUN_ID: "123456",
|
|
PATH: `${bin}:${process.env.PATH ?? ""}`,
|
|
RUNNER_TEMP: root,
|
|
OPENCLAW_SHARED_IMAGE_PACKAGE_SHA256: PACKAGE_SHA256,
|
|
};
|
|
return { artifactDir, dockerLog, env, ghLog, root };
|
|
}
|
|
|
|
function expectedArchiveEnv(fixture: ReturnType<typeof createFixture>): NodeJS.ProcessEnv {
|
|
const manifest = JSON.parse(
|
|
readFileSync(join(fixture.artifactDir, "shared-image-artifact.json"), "utf8"),
|
|
);
|
|
return {
|
|
...fixture.env,
|
|
OPENCLAW_SHARED_IMAGE_ARCHIVE_SHA256: manifest.archive.sha256,
|
|
OPENCLAW_SHARED_IMAGE_RUN_ATTEMPT: String(manifest.runAttempt),
|
|
OPENCLAW_SHARED_IMAGE_RUN_ID: String(manifest.runId),
|
|
};
|
|
}
|
|
|
|
describe("shared Docker image artifacts", () => {
|
|
it("binds uploaded artifacts to the exact service tuple and producer attempt", () => {
|
|
const fixture = createFixture();
|
|
try {
|
|
const verified = verifyUploadedArtifact(fixture);
|
|
expect(verified.status, `${verified.stdout}\n${verified.stderr}`).toBe(0);
|
|
expect(readFileSync(fixture.ghLog, "utf8")).toContain(
|
|
`api repos/openclaw/openclaw/actions/artifacts/${ARTIFACT_ID}`,
|
|
);
|
|
expect(readFileSync(fixture.ghLog, "utf8")).toContain(
|
|
`api repos/openclaw/openclaw/actions/runs/${ARTIFACT_RUN_ID}/attempts/${ARTIFACT_RUN_ATTEMPT}`,
|
|
);
|
|
|
|
const digestMismatch = verifyUploadedArtifact(fixture, {
|
|
artifactDigest: "e".repeat(64),
|
|
});
|
|
expect(digestMismatch.status).not.toBe(0);
|
|
expect(digestMismatch.stderr).toContain(
|
|
"artifact identity does not match the immutable producer tuple",
|
|
);
|
|
|
|
const attemptMismatch = verifyUploadedArtifact(fixture, {
|
|
env: { FAKE_ARTIFACT_RUN_ATTEMPT: "3" },
|
|
});
|
|
expect(attemptMismatch.status).not.toBe(0);
|
|
expect(attemptMismatch.stderr).toContain(
|
|
"producer run attempt does not match the immutable tuple",
|
|
);
|
|
} finally {
|
|
rmSync(fixture.root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("packs provenance-bound images and verifies them before loading", () => {
|
|
const fixture = createFixture();
|
|
try {
|
|
const packed = runHelper({
|
|
artifactDir: fixture.artifactDir,
|
|
command: "pack",
|
|
env: fixture.env,
|
|
});
|
|
expect(packed.status, `${packed.stdout}\n${packed.stderr}`).toBe(0);
|
|
|
|
const archive = readFileSync(join(fixture.artifactDir, "shared-images.tar.zst"));
|
|
const manifest = JSON.parse(
|
|
readFileSync(join(fixture.artifactDir, "shared-image-artifact.json"), "utf8"),
|
|
);
|
|
expect(manifest).toEqual({
|
|
archive: {
|
|
filename: "shared-images.tar.zst",
|
|
format: "docker-tar+zstd",
|
|
sha256: createHash("sha256").update(archive).digest("hex"),
|
|
sizeBytes: archive.length,
|
|
},
|
|
conclusion: "success",
|
|
images: IMAGE_REFS.map((ref) => ({ id: imageId(ref), ref })),
|
|
kind: "docker-e2e",
|
|
packageSha256: PACKAGE_SHA256,
|
|
packageSourceSha: TARGET_SHA,
|
|
runAttempt: 2,
|
|
runId: 123456,
|
|
schema: "openclaw.shared-docker-image-artifact/v1",
|
|
schemaVersion: 1,
|
|
targetSha: TARGET_SHA,
|
|
workflowSha: WORKFLOW_SHA,
|
|
});
|
|
|
|
writeFileSync(fixture.dockerLog, "");
|
|
const loaded = runHelper({
|
|
artifactDir: fixture.artifactDir,
|
|
command: "load",
|
|
env: { ...expectedArchiveEnv(fixture), GITHUB_RUN_ATTEMPT: "3" },
|
|
});
|
|
expect(loaded.status, `${loaded.stdout}\n${loaded.stderr}`).toBe(0);
|
|
const calls = readFileSync(fixture.dockerLog, "utf8");
|
|
expect(calls).toContain("image load");
|
|
for (const ref of IMAGE_REFS) {
|
|
expect(calls).toContain(`image inspect --format {{.Id}} ${ref}`);
|
|
}
|
|
} finally {
|
|
rmSync(fixture.root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("fails before loading when provenance or archive bytes differ", () => {
|
|
const fixture = createFixture();
|
|
try {
|
|
const packed = runHelper({
|
|
artifactDir: fixture.artifactDir,
|
|
command: "pack",
|
|
env: fixture.env,
|
|
});
|
|
expect(packed.status, packed.stderr).toBe(0);
|
|
|
|
for (const variant of [
|
|
{
|
|
env: {
|
|
...expectedArchiveEnv(fixture),
|
|
OPENCLAW_SHARED_IMAGE_RUN_ID: "654321",
|
|
},
|
|
imageRefs: IMAGE_REFS,
|
|
expected: "run ID",
|
|
},
|
|
{
|
|
env: expectedArchiveEnv(fixture),
|
|
imageRefs: [
|
|
expectDefined(IMAGE_REFS[0], "first shared Docker image reference"),
|
|
"openclaw-docker-e2e-functional:wrong",
|
|
],
|
|
expected: "image ref 1",
|
|
},
|
|
{
|
|
env: {
|
|
...expectedArchiveEnv(fixture),
|
|
OPENCLAW_SHARED_IMAGE_PACKAGE_SHA256: "d".repeat(64),
|
|
},
|
|
imageRefs: IMAGE_REFS,
|
|
expected: "package SHA-256",
|
|
},
|
|
]) {
|
|
writeFileSync(fixture.dockerLog, "");
|
|
const result = runHelper({
|
|
artifactDir: fixture.artifactDir,
|
|
command: "load",
|
|
env: variant.env,
|
|
imageRefs: variant.imageRefs,
|
|
});
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain(variant.expected);
|
|
expect(readFileSync(fixture.dockerLog, "utf8")).not.toContain("image load");
|
|
}
|
|
|
|
const archivePath = join(fixture.artifactDir, "shared-images.tar.zst");
|
|
writeFileSync(archivePath, `${readFileSync(archivePath, "utf8")}tampered`);
|
|
writeFileSync(fixture.dockerLog, "");
|
|
const tampered = runHelper({
|
|
artifactDir: fixture.artifactDir,
|
|
command: "load",
|
|
env: expectedArchiveEnv(fixture),
|
|
});
|
|
expect(tampered.status).not.toBe(0);
|
|
expect(tampered.stderr).toContain("archive SHA-256 mismatch");
|
|
expect(readFileSync(fixture.dockerLog, "utf8")).not.toContain("image load");
|
|
} finally {
|
|
rmSync(fixture.root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("rejects unsafe pack destinations and loaded image ID drift", () => {
|
|
const fixture = createFixture();
|
|
try {
|
|
const unsafe = runHelper({
|
|
artifactDir: fixture.root,
|
|
command: "pack",
|
|
env: fixture.env,
|
|
});
|
|
expect(unsafe.status).not.toBe(0);
|
|
expect(unsafe.stderr).toContain("artifact directory must be a child of RUNNER_TEMP");
|
|
|
|
const packed = runHelper({
|
|
artifactDir: fixture.artifactDir,
|
|
command: "pack",
|
|
env: fixture.env,
|
|
});
|
|
expect(packed.status, packed.stderr).toBe(0);
|
|
|
|
writeFileSync(fixture.dockerLog, "");
|
|
const mismatch = runHelper({
|
|
artifactDir: fixture.artifactDir,
|
|
command: "load",
|
|
env: { ...expectedArchiveEnv(fixture), FAKE_DOCKER_FORCE_ID_MISMATCH: "1" },
|
|
});
|
|
expect(mismatch.status).not.toBe(0);
|
|
expect(mismatch.stderr).toContain("loaded ID mismatch");
|
|
expect(readFileSync(fixture.dockerLog, "utf8")).toContain("image load");
|
|
} finally {
|
|
rmSync(fixture.root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("requires an external expected archive digest before loading", () => {
|
|
const fixture = createFixture();
|
|
try {
|
|
const packed = runHelper({
|
|
artifactDir: fixture.artifactDir,
|
|
command: "pack",
|
|
env: fixture.env,
|
|
});
|
|
expect(packed.status, packed.stderr).toBe(0);
|
|
|
|
const missingRunEnv = expectedArchiveEnv(fixture);
|
|
delete missingRunEnv.OPENCLAW_SHARED_IMAGE_RUN_ID;
|
|
delete missingRunEnv.OPENCLAW_SHARED_IMAGE_RUN_ATTEMPT;
|
|
const missingRun = runHelper({
|
|
artifactDir: fixture.artifactDir,
|
|
command: "load",
|
|
env: missingRunEnv,
|
|
});
|
|
expect(missingRun.status).not.toBe(0);
|
|
expect(missingRun.stderr).toContain("OPENCLAW_SHARED_IMAGE_RUN_ID");
|
|
expect(readFileSync(fixture.dockerLog, "utf8")).not.toContain("image load");
|
|
|
|
const missing = runHelper({
|
|
artifactDir: fixture.artifactDir,
|
|
command: "load",
|
|
env: {
|
|
...fixture.env,
|
|
OPENCLAW_SHARED_IMAGE_RUN_ATTEMPT: "2",
|
|
OPENCLAW_SHARED_IMAGE_RUN_ID: "123456",
|
|
},
|
|
});
|
|
expect(missing.status).not.toBe(0);
|
|
expect(missing.stderr).toContain("expected shared image archive SHA-256");
|
|
expect(readFileSync(fixture.dockerLog, "utf8")).not.toContain("image load");
|
|
|
|
const mismatched = runHelper({
|
|
artifactDir: fixture.artifactDir,
|
|
command: "load",
|
|
env: {
|
|
...expectedArchiveEnv(fixture),
|
|
OPENCLAW_SHARED_IMAGE_ARCHIVE_SHA256: "d".repeat(64),
|
|
},
|
|
});
|
|
expect(mismatched.status).not.toBe(0);
|
|
expect(mismatched.stderr).toContain("expected archive sha256");
|
|
expect(readFileSync(fixture.dockerLog, "utf8")).not.toContain("image load");
|
|
} finally {
|
|
rmSync(fixture.root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
});
|