Files
openclaw/test/scripts/shared-image-artifact.test.ts
Peter Steinberger e0f45bfbf0 feat(tooling): enforce indexed access checks in root tests (#105223)
* feat(tooling): enforce indexed access checks in root tests

* style(tooling): clarify scoped package guard
2026-07-12 10:42:07 +01:00

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 });
}
});
});