Files
openclaw/scripts/e2e/lib/docker-artifact-proof/write-identities.ts
Dallin Romney 66b4dcf184 chore(docker): execute Compose and package artifact proofs (#101045)
* test(docker): prove Compose and package artifacts

* test(docker): use bounded container cleanup

* test(docker): bound package proof launch

* test(qa): keep Docker artifact proofs release-only

* test(docker): simplify artifact proof names

* test(docker): satisfy identity writer lint
2026-07-06 13:05:34 -07:00

158 lines
4.9 KiB
TypeScript

import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
type ContainerOption = {
name: string;
role: string;
};
type WriterOptions = {
containers: ContainerOption[];
details: Map<string, Record<string, string>>;
image?: string;
output?: string;
package?: string;
scenario?: string;
};
type RequiredWriterOptions = WriterOptions & {
image: string;
output: string;
package: string;
scenario: string;
};
type DockerInspect = {
Id: string;
Image?: string;
Name?: string;
RepoDigests?: string[];
State?: { Status?: string };
};
type PackageManifest = {
name?: unknown;
version?: unknown;
};
function readValue(args: string[], index: number, option: string): string {
const value = args[index + 1];
if (!value || value.startsWith("--")) {
throw new Error(`${option} requires a value`);
}
return value;
}
function parseArgs(args: string[]): RequiredWriterOptions {
const options: WriterOptions = { containers: [], details: new Map() };
for (let index = 0; index < args.length; index += 1) {
const option = args[index];
if (!option) {
continue;
}
const value = readValue(args, index, option);
index += 1;
if (option === "--scenario") {
options.scenario = value;
} else if (option === "--output") {
options.output = value;
} else if (option === "--image") {
options.image = value;
} else if (option === "--package") {
options.package = value;
} else if (option === "--container") {
const separator = value.indexOf("=");
if (separator < 1 || separator === value.length - 1) {
throw new Error("--container must use role=name");
}
options.containers.push({
role: value.slice(0, separator),
name: value.slice(separator + 1),
});
} else if (option === "--detail") {
const roleSeparator = value.indexOf(":");
const valueSeparator = value.indexOf("=", roleSeparator + 1);
if (roleSeparator < 1 || valueSeparator < roleSeparator + 2) {
throw new Error("--detail must use role:key=value");
}
const role = value.slice(0, roleSeparator);
const key = value.slice(roleSeparator + 1, valueSeparator);
const detailValue = value.slice(valueSeparator + 1);
const roleDetails = options.details.get(role) ?? {};
roleDetails[key] = detailValue;
options.details.set(role, roleDetails);
} else {
throw new Error(`unknown option: ${option}`);
}
}
for (const required of ["scenario", "output", "image", "package"] as const) {
if (!options[required]) {
throw new Error(`--${required} is required`);
}
}
if (options.containers.length === 0) {
throw new Error("at least one --container is required");
}
return options as RequiredWriterOptions;
}
function run(command: string, args: string[]): string {
return execFileSync(command, args, { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }).trim();
}
function inspectDockerObject(reference: string): DockerInspect {
const [result] = JSON.parse(run("docker", ["inspect", reference])) as DockerInspect[];
if (!result) {
throw new Error(`docker inspect returned no result for ${reference}`);
}
return result;
}
function readPackageIdentity(packagePath: string) {
const packageJson = JSON.parse(
run("tar", ["-xOf", packagePath, "package/package.json"]),
) as PackageManifest;
if (typeof packageJson.name !== "string" || typeof packageJson.version !== "string") {
throw new Error("package artifact manifest is missing name or version");
}
const bytes = fs.readFileSync(packagePath);
return {
fileName: path.basename(packagePath),
name: packageJson.name,
sha256: createHash("sha256").update(bytes).digest("hex"),
sizeBytes: bytes.byteLength,
version: packageJson.version,
};
}
const options = parseArgs(process.argv.slice(2));
const image = inspectDockerObject(options.image);
const containers = options.containers.map(({ name, role }) => {
const container = inspectDockerObject(name);
return {
details: options.details.get(role) ?? {},
id: container.Id,
imageId: container.Image,
name: (container.Name ?? name).replace(/^\//u, ""),
role,
state: container.State?.Status,
};
});
const identity = {
containers,
image: {
id: image.Id,
reference: options.image,
repoDigests: image.RepoDigests ?? [],
},
package: readPackageIdentity(options.package),
scenarioId: options.scenario,
};
fs.mkdirSync(path.dirname(options.output), { recursive: true });
fs.writeFileSync(options.output, `${JSON.stringify(identity, null, 2)}\n`);
console.log(
`artifact identities: package=${identity.package.name}@${identity.package.version} sha256=${identity.package.sha256} image=${identity.image.id} containers=${containers.map((container) => `${container.role}:${container.id}`).join(",")}`,
);