Files
ttstd-infinite-canvas/web/test/ci-image-release.test.mjs

110 lines
5.0 KiB
JavaScript

import { afterEach, describe, expect, test } from "bun:test";
import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
const scripts = fileURLToPath(new URL("../../.github/scripts/", import.meta.url));
const temporaryDirectories = [];
const sha = "a".repeat(40);
const digest = "b".repeat(64);
afterEach(() => {
for (const directory of temporaryDirectories.splice(0)) rmSync(directory, { recursive: true, force: true });
});
function fixture(overrides = {}) {
const root = mkdtempSync(path.join(os.tmpdir(), "canvas-ci-image-"));
temporaryDirectories.push(root);
const bin = path.join(root, "bin");
const digests = path.join(root, "digests");
mkdirSync(bin);
mkdirSync(digests);
writeFileSync(path.join(digests, digest), "");
writeFileSync(path.join(bin, "docker"), '#!/bin/sh\nif [ "$3" = inspect ]; then\n if [ -n "$MOCK_ERROR" ]; then printf "%s\\n" "$MOCK_ERROR" >&2; exit 1; fi\n printf "%s\\n" "$MOCK_DIGEST"\nelse\n printf "%s\\n" "$@" > "$MOCK_CALL"\nfi\n', {
mode: 0o755,
});
writeFileSync(path.join(bin, "gh"), '#!/bin/sh\nif [ -n "$MOCK_GH_ERROR" ]; then exit 1; fi\nprintf "%s\\n" "$MOCK_MAIN"\n', { mode: 0o755 });
const output = path.join(root, "output");
writeFileSync(output, "");
const env = {
...process.env,
PATH: `${bin}:${process.env.PATH}`,
GITHUB_OUTPUT: output,
GITHUB_SHA: sha,
GITHUB_REF: "refs/heads/main",
GITHUB_REPOSITORY: "fixture/repo",
IMAGE: "ghcr.io/fixture/image",
VERSION_TAG: "1.5.5",
EXPECTED_DIGESTS: "1",
DIGEST_DIR: digests,
MOCK_DIGEST: `sha256:${digest}`,
MOCK_ERROR: "",
MOCK_GH_ERROR: "",
MOCK_MAIN: sha,
MOCK_CALL: path.join(root, "docker-call"),
...overrides,
};
return {
run: (script, ...args) => spawnSync("bash", [path.join(scripts, script), ...args], { env, encoding: "utf8" }),
output: () => readFileSync(output, "utf8"),
call: () => (existsSync(env.MOCK_CALL) ? readFileSync(env.MOCK_CALL, "utf8") : ""),
};
}
describe("commit image reuse", () => {
test("reuses the exact existing digest", () => {
const repo = fixture();
expect(repo.run("resolve-image.sh", "ghcr.io/fixture/image:sha-test").status).toBe(0);
expect(repo.output()).toBe(`found=true\ndigest=sha256:${digest}\n`);
});
test("only a missing manifest permits a rebuild", () => {
const repo = fixture({ MOCK_ERROR: "ERROR: ghcr.io/fixture/image:sha-test: not found" });
expect(repo.run("resolve-image.sh", "ghcr.io/fixture/image:sha-test").status).toBe(0);
expect(repo.output()).toBe("found=false\n");
});
for (const error of ["ERROR: unauthorized", "ERROR: 429 Too Many Requests", "ERROR: dial tcp: i/o timeout", "ERROR: certificate file: not found: authentication failed"]) {
test(`fails closed on ${error}`, () => {
const repo = fixture({ MOCK_ERROR: error });
expect(repo.run("resolve-image.sh", "ghcr.io/fixture/image:sha-test").status).not.toBe(0);
expect(repo.output()).toBe("");
});
}
test("rejects malformed successful responses", () => {
const repo = fixture({ MOCK_DIGEST: "<html>error</html>" });
expect(repo.run("resolve-image.sh", "ghcr.io/fixture/image:sha-test").status).not.toBe(0);
});
});
describe("digest promotion", () => {
test("current main promotes latest and SHA tags using digests", () => {
const repo = fixture();
expect(repo.run("promote-image.sh").status).toBe(0);
expect(repo.call()).toContain("ghcr.io/fixture/image:latest");
expect(repo.call()).toContain(`ghcr.io/fixture/image@sha256:${digest}`);
expect(repo.call()).toContain(`ghcr.io/fixture/image:sha-${sha}`);
expect(repo.call()).not.toContain("ghcr.io/fixture/image:1.5.5");
});
test("stale main cannot roll latest back", () => {
const repo = fixture({ MOCK_MAIN: "c".repeat(40) });
expect(repo.run("promote-image.sh").status).toBe(0);
expect(repo.call()).not.toContain(":latest");
});
test("tag release promotes the exact version without touching latest", () => {
const repo = fixture({ GITHUB_REF: "refs/tags/v1.5.5" });
expect(repo.run("promote-image.sh").status).toBe(0);
expect(repo.call()).toContain("ghcr.io/fixture/image:1.5.5");
expect(repo.call()).not.toContain(":latest");
});
test("missing architecture blocks every tag update", () => {
const repo = fixture({ EXPECTED_DIGESTS: "2" });
expect(repo.run("promote-image.sh").status).not.toBe(0);
expect(repo.call()).toBe("");
});
test("failure to resolve main blocks promotion", () => {
const repo = fixture({ MOCK_GH_ERROR: "1" });
expect(repo.run("promote-image.sh").status).not.toBe(0);
expect(repo.call()).toBe("");
});
});