mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
211 lines
6.2 KiB
TypeScript
211 lines
6.2 KiB
TypeScript
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { spawnSync } from "node:child_process";
|
|
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
const e2eRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
const workspaceRoot = dirname(e2eRoot);
|
|
const script = join(workspaceRoot, ".github", "scripts", "release", "stable-docker.py");
|
|
const fixtures: string[] = [];
|
|
const commit = "0123456789abcdef0123456789abcdef01234567";
|
|
const targetDigest = `sha256:${"a".repeat(64)}`;
|
|
|
|
async function jsonFile(value: unknown): Promise<string> {
|
|
const root = await mkdtemp(join(tmpdir(), "od-stable-docker-"));
|
|
fixtures.push(root);
|
|
const path = join(root, "fixture.json");
|
|
await writeFile(path, `${JSON.stringify(value)}\n`, "utf8");
|
|
return path;
|
|
}
|
|
|
|
function run(args: string[]) {
|
|
return spawnSync("python3", [script, ...args], {
|
|
cwd: workspaceRoot,
|
|
encoding: "utf8",
|
|
});
|
|
}
|
|
|
|
function outputs(stdout: string): Record<string, string> {
|
|
return Object.fromEntries(
|
|
stdout.trim().split("\n").filter(Boolean).map((line) => {
|
|
const index = line.indexOf("=");
|
|
return [line.slice(0, index), line.slice(index + 1)];
|
|
}),
|
|
);
|
|
}
|
|
|
|
function metadata(version = "1.2.3") {
|
|
return {
|
|
channel: "stable",
|
|
dryRun: false,
|
|
releaseState: "complete",
|
|
releaseVersion: version,
|
|
stableVersion: version,
|
|
versionTag: `open-design-v${version}`,
|
|
github: {
|
|
branch: `release/v${version}`,
|
|
commit,
|
|
repository: "nexu-io/open-design",
|
|
runAttempt: 2,
|
|
runId: 12345,
|
|
workflow: "release-stable",
|
|
},
|
|
r2: {
|
|
versionMetadataUrl: `https://releases.example/stable/versions/${version}/metadata.json`,
|
|
versionPrefix: `stable/versions/${version}`,
|
|
},
|
|
};
|
|
}
|
|
|
|
function inspection(options: {
|
|
digest?: string;
|
|
platforms?: string[];
|
|
revision?: string;
|
|
version?: string;
|
|
} = {}) {
|
|
const revision = options.revision ?? commit;
|
|
const version = options.version ?? "1.2.3";
|
|
const platforms = options.platforms ?? ["linux/amd64", "linux/arm64"];
|
|
return {
|
|
manifest: { digest: options.digest ?? targetDigest },
|
|
image: Object.fromEntries(platforms.map((platform) => [
|
|
platform,
|
|
{
|
|
config: {
|
|
Labels: {
|
|
"org.opencontainers.image.revision": revision,
|
|
"org.opencontainers.image.version": version,
|
|
},
|
|
},
|
|
},
|
|
])),
|
|
};
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(fixtures.splice(0).map((path) => rm(path, { recursive: true, force: true })));
|
|
});
|
|
|
|
describe("stable Docker reconciliation contract", () => {
|
|
it("resolves an exact immutable stable identity", async () => {
|
|
const path = await jsonFile(metadata());
|
|
const result = run([
|
|
"resolve",
|
|
"--metadata", path,
|
|
"--repository", "nexu-io/open-design",
|
|
"--release-version", "1.2.3",
|
|
"--origin-run-id", "12345",
|
|
"--origin-run-attempt", "2",
|
|
]);
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(outputs(result.stdout)).toMatchObject({
|
|
commit,
|
|
is_target_version: "true",
|
|
release_run_id: "12345",
|
|
release_version: "1.2.3",
|
|
version_tag: "open-design-v1.2.3",
|
|
});
|
|
});
|
|
|
|
it("allows latest metadata to identify a newer authoritative version", async () => {
|
|
const path = await jsonFile(metadata("1.2.4"));
|
|
const result = run([
|
|
"resolve",
|
|
"--metadata", path,
|
|
"--repository", "nexu-io/open-design",
|
|
"--release-version", "1.2.3",
|
|
"--allow-version-mismatch",
|
|
]);
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(outputs(result.stdout)).toMatchObject({
|
|
is_target_version: "false",
|
|
release_version: "1.2.4",
|
|
});
|
|
});
|
|
|
|
it("fails closed when dispatch and metadata identity disagree", async () => {
|
|
const value = metadata();
|
|
value.github.commit = "f".repeat(40);
|
|
value.github.repository = "someone/else";
|
|
const path = await jsonFile(value);
|
|
const result = run([
|
|
"resolve",
|
|
"--metadata", path,
|
|
"--repository", "nexu-io/open-design",
|
|
"--release-version", "1.2.3",
|
|
"--origin-run-id", "12345",
|
|
]);
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("metadata.github.repository");
|
|
});
|
|
|
|
it("returns no-op for a complete matching multi-arch version image", async () => {
|
|
const path = await jsonFile(inspection());
|
|
const result = run([
|
|
"image-state",
|
|
"--inspection", path,
|
|
"--expected-revision", commit,
|
|
"--expected-version", "1.2.3",
|
|
]);
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(outputs(result.stdout)).toEqual({
|
|
digest: targetDigest,
|
|
missing_platforms: "",
|
|
state: "complete",
|
|
});
|
|
});
|
|
|
|
it("repairs a matching version image with a missing platform", async () => {
|
|
const path = await jsonFile(inspection({ platforms: ["linux/amd64"] }));
|
|
const result = run([
|
|
"image-state",
|
|
"--inspection", path,
|
|
"--expected-revision", commit,
|
|
"--expected-version", "1.2.3",
|
|
]);
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(outputs(result.stdout)).toMatchObject({
|
|
missing_platforms: "linux/arm64",
|
|
state: "repair",
|
|
});
|
|
});
|
|
|
|
it("refuses to overwrite an existing version tag with another identity", async () => {
|
|
const path = await jsonFile(inspection({ revision: "f".repeat(40) }));
|
|
const result = run([
|
|
"image-state",
|
|
"--inspection", path,
|
|
"--expected-revision", commit,
|
|
"--expected-version", "1.2.3",
|
|
]);
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("existing version tag identity conflict");
|
|
});
|
|
|
|
it.each([
|
|
[targetDigest, "1.2.3", "noop"],
|
|
[`sha256:${"b".repeat(64)}`, "1.2.2", "promote"],
|
|
[`sha256:${"c".repeat(64)}`, "1.2.4", "skip-newer"],
|
|
])("plans latest digest %s at version %s as %s", async (digest, version, action) => {
|
|
const path = await jsonFile(inspection({ digest, version }));
|
|
const result = run([
|
|
"latest-action",
|
|
"--inspection", path,
|
|
"--target-digest", targetDigest,
|
|
"--target-version", "1.2.3",
|
|
]);
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(outputs(result.stdout).action).toBe(action);
|
|
});
|
|
});
|