mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Fourth latent bug in the OCR publish chain (#649/#519 added it, never ran). verify-ocr's trust-identity step read OCR_RUNTIME_INDEX_KEY_ID from the image env, which the official image leaves unset by design (operator-override path; runtime-index.ts reads the baked file when env is unset). It compared "" to snapotter-ocr-2026-07 and failed a correct image. Now verifies the baked /app/docker/ocr-runtime-trust.json, proven against the built release image. A full audit of the remaining chain (verify, sign, verify-signed, publish) reproduced the sign+verify path end-to-end inside the image and confirmed nothing else breaks for v2.2.0. One latent non-ASCII canonicalization mismatch filed as #667 (cannot fire on v2.2.0). Non-releasable type so the re-dispatch re-runs 2.2.0.
1540 lines
75 KiB
TypeScript
1540 lines
75 KiB
TypeScript
// biome-ignore-all lint/suspicious/noTemplateCurlyInString: These contract assertions intentionally match GitHub and shell interpolation syntax as literal text.
|
|
import { execFileSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { load } from "js-yaml";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const root = process.cwd();
|
|
const bundlesWorkflowPath = path.resolve(root, ".github/workflows/ai-bundles.yml");
|
|
const ciWorkflowPath = path.resolve(root, ".github/workflows/ci.yml");
|
|
const releaseWorkflowPath = path.resolve(root, ".github/workflows/release.yml");
|
|
const verifierPath = path.resolve(root, "docker/verify-ocr-runtime.sh");
|
|
const hfReleaseRequirementsPath = path.resolve(root, "docker/hf-release-requirements.txt");
|
|
const ocrReleaseRunbookPath = path.resolve(root, ".github/OCR_RUNTIME_RELEASE_RUNBOOK.md");
|
|
const ocrRequirementsPaths = [
|
|
path.resolve(root, "docker/ocr-runtime-requirements-amd64.txt"),
|
|
path.resolve(root, "docker/ocr-runtime-requirements-arm64.txt"),
|
|
];
|
|
|
|
function readRequired(file: string): string {
|
|
expect(existsSync(file), `${path.relative(root, file)} is missing`).toBe(true);
|
|
return readFileSync(file, "utf8");
|
|
}
|
|
|
|
interface WorkflowDocument {
|
|
on: Record<string, unknown>;
|
|
jobs: Record<string, { if?: string; needs?: string | string[] }>;
|
|
}
|
|
|
|
/**
|
|
* Name of the job that gates Hugging Face publication behind a release commit,
|
|
* or null when publication is reachable without one.
|
|
*
|
|
* `publish` uploads feature bundles, so it must only ever run as part of a
|
|
* release. GitHub skips a job whose `needs` were skipped, so walking the
|
|
* dependency graph and finding a `release_commit` condition proves the manual
|
|
* and scheduled triggers cannot reach it.
|
|
*/
|
|
function publishGate(workflow: WorkflowDocument): string | null {
|
|
const seen = new Set<string>();
|
|
const queue = ["publish"];
|
|
|
|
while (queue.length > 0) {
|
|
const name = queue.shift() as string;
|
|
if (seen.has(name)) continue;
|
|
seen.add(name);
|
|
|
|
const definition = workflow.jobs[name];
|
|
expect(definition, `job ${name} is missing`).toBeTruthy();
|
|
if (definition.if?.includes("inputs.release_commit")) return name;
|
|
|
|
const needs = definition.needs;
|
|
if (typeof needs === "string") queue.push(needs);
|
|
else if (Array.isArray(needs)) queue.push(...needs);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function job(workflow: string, name: string, nextName?: string): string {
|
|
const start = workflow.indexOf(` ${name}:\n`);
|
|
expect(start, `job ${name} is missing`).toBeGreaterThanOrEqual(0);
|
|
if (!nextName) return workflow.slice(start);
|
|
const end = workflow.indexOf(` ${nextName}:\n`, start + name.length + 3);
|
|
expect(end, `job ${nextName} is missing`).toBeGreaterThan(start);
|
|
return workflow.slice(start, end);
|
|
}
|
|
|
|
function measuredEstimateContract(workflow: string): string {
|
|
const beginMarker = "# OCR_MEASURED_ESTIMATE_CONTRACT_BEGIN";
|
|
const endMarker = "# OCR_MEASURED_ESTIMATE_CONTRACT_END";
|
|
const begin = workflow.indexOf(beginMarker);
|
|
const end = workflow.indexOf(endMarker, begin + beginMarker.length);
|
|
|
|
expect(begin, "measured-estimate contract start marker is missing").toBeGreaterThanOrEqual(0);
|
|
expect(end, "measured-estimate contract end marker is missing").toBeGreaterThan(begin);
|
|
return workflow
|
|
.slice(begin + beginMarker.length, end)
|
|
.split("\n")
|
|
.map((line) => line.replace(/^ {10}/, ""))
|
|
.join("\n")
|
|
.trim();
|
|
}
|
|
|
|
describe("OCR v3 bundle release workflow", () => {
|
|
it("uses audited non-vulnerable HTTP dependency locks on both architectures", () => {
|
|
for (const requirementsPath of ocrRequirementsPaths) {
|
|
const requirements = readRequired(requirementsPath);
|
|
expect(requirements).toMatch(/^requests==2\.33\.0 --hash=sha256:[a-f0-9]{64}$/m);
|
|
expect(requirements).toMatch(/^idna==3\.15 --hash=sha256:[a-f0-9]{64}$/m);
|
|
expect(requirements).not.toMatch(/^requests==2\.32\.3\b/m);
|
|
expect(requirements).not.toMatch(/^idna==3\.10\b/m);
|
|
}
|
|
|
|
const ci = readRequired(ciWorkflowPath);
|
|
const bundles = readRequired(bundlesWorkflowPath);
|
|
for (const workflow of [ci, bundles]) {
|
|
expect(workflow).toContain('pip install "pip-audit==2.10.0"');
|
|
expect(workflow).toContain("docker/ocr-runtime-requirements-amd64.txt");
|
|
expect(workflow).toContain("docker/ocr-runtime-requirements-arm64.txt");
|
|
expect(workflow).toContain("--no-deps --disable-pip --aliases");
|
|
}
|
|
expect(job(bundles, "build-ocr", "verify-ocr")).toContain(
|
|
"Audit exact OCR runtime dependency lock",
|
|
);
|
|
});
|
|
|
|
it("scans and inventories both architecture-specific release images", () => {
|
|
const workflow = readRequired(releaseWorkflowPath);
|
|
const scanJob = job(workflow, "scan", "sbom");
|
|
const sbomJob = job(workflow, "sbom", "ai-bundles");
|
|
|
|
for (const platform of ["linux-amd64", "linux-arm64"]) {
|
|
expect(scanJob).toContain(`- ${platform}`);
|
|
expect(sbomJob).toContain(`- ${platform}`);
|
|
}
|
|
expect(scanJob).toContain("digests-${{ matrix.platform }}");
|
|
expect(sbomJob).toContain("digests-${{ matrix.platform }}");
|
|
expect(scanJob).toContain("${{ matrix.platform }}-trivy.json");
|
|
expect(sbomJob).toContain("${{ matrix.platform }}-sbom.cdx.json");
|
|
});
|
|
|
|
it("builds and verifies both portable targets on native runners", () => {
|
|
const workflow = readRequired(bundlesWorkflowPath);
|
|
|
|
expect(workflow).not.toContain("default: false");
|
|
expect(job(workflow, "validate-inputs", "build-ocr")).toContain(
|
|
`[[ "\${VERSION}" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+`,
|
|
);
|
|
const validationJob = job(workflow, "validate-inputs", "build-ocr");
|
|
expect(validationJob).toContain("Validate OCR runtime trust configuration");
|
|
expect(validationJob).toContain(
|
|
`: "\${OCR_RUNTIME_INDEX_KEY_ID:?Set repository variable OCR_RUNTIME_INDEX_KEY_ID}"`,
|
|
);
|
|
expect(validationJob).toContain("Configured OCR runtime public key is not Ed25519");
|
|
expect(validationJob).not.toContain("OCR_RUNTIME_INDEX_SIGNING_KEY_B64");
|
|
for (const target of ["linux-amd64-cpu-py312", "linux-arm64-cpu-py311"]) {
|
|
expect(workflow).toContain(`target: ${target}`);
|
|
}
|
|
expect(workflow).toContain(
|
|
"for suffix in tar.gz tar.gz.sha256 artifact.json index-input.json; do",
|
|
);
|
|
expect(workflow).toContain("/tmp/bundles/primary/ocr-${TARGET}.${suffix}");
|
|
expect(workflow).toContain("runner: ubuntu-latest");
|
|
expect(workflow).toContain("runner: ubuntu-24.04-arm");
|
|
const buildJob = job(workflow, "build-ocr", "verify-ocr");
|
|
expect(buildJob).toContain("timeout-minutes: 90");
|
|
expect(buildJob).toContain("for build in primary rebuild; do");
|
|
expect(buildJob).toContain('cmp --silent "/tmp/bundles/primary/ocr-${TARGET}.${suffix}"');
|
|
expect(buildJob).toContain("OCR runtime rebuild is not byte-reproducible");
|
|
expect(buildJob).toContain("tar.gz.sha256 artifact.json index-input.json");
|
|
expect(workflow).not.toContain(" build-legacy:\n");
|
|
expect(workflow).not.toContain(" verify-legacy:\n");
|
|
expect(workflow).not.toMatch(/bundle: ocr\s+arch: (amd64-gpu|arm64-cpu)/);
|
|
expect(workflow).not.toMatch(/paddleocr|onnxruntime-gpu/i);
|
|
});
|
|
|
|
it("keeps bundle publication reachable only from the release workflow", () => {
|
|
const workflow = load(readRequired(bundlesWorkflowPath)) as WorkflowDocument;
|
|
|
|
// The manual and scheduled triggers exist so the standalone
|
|
// installed-AI verification lane can run outside a release. They must not
|
|
// open a second route to the Hugging Face publish job.
|
|
expect(Object.keys(workflow.on)).toContain("workflow_dispatch");
|
|
expect(Object.keys(workflow.on)).toContain("schedule");
|
|
expect(publishGate(workflow)).toBe("validate-inputs");
|
|
|
|
// Negative control: strip the release-commit gate and the same walk must
|
|
// report the publish job as reachable from a manual dispatch.
|
|
const ungated = load(readRequired(bundlesWorkflowPath)) as WorkflowDocument;
|
|
delete ungated.jobs["validate-inputs"].if;
|
|
expect(publishGate(ungated)).toBeNull();
|
|
});
|
|
|
|
it("blocks release on the amd64 CPU pack running with a real NVIDIA GPU exposed", () => {
|
|
const workflow = readRequired(bundlesWorkflowPath);
|
|
const gpuJob = job(workflow, "verify-ocr-nvidia", "sign-ocr-index");
|
|
const signJob = job(workflow, "sign-ocr-index", "verify-signed-ocr-index");
|
|
|
|
expect(gpuJob).toContain("runs-on: [self-hosted, linux, x64, snapotter-nvidia]");
|
|
expect(gpuJob).toContain("Clear persistent runner release state");
|
|
expect(gpuJob).toContain("rm -rf /tmp/image-digest /tmp/bundles /tmp/ocr-quality");
|
|
expect(gpuJob).toContain("nvidia-smi");
|
|
expect(gpuJob).toContain("--gpus all");
|
|
expect(gpuJob).toContain("NVIDIA_VISIBLE_DEVICES=all");
|
|
expect(gpuJob).toContain("CUDA_VISIBLE_DEVICES=0");
|
|
expect(gpuJob).toContain("docker/verify-ocr-runtime.sh");
|
|
expect(gpuJob).toContain("linux-amd64-cpu-py312");
|
|
expect(signJob).toContain("needs: [build-ocr, verify-ocr, verify-ocr-nvidia]");
|
|
});
|
|
|
|
it("installs each exact archive offline before a functional CPU-only smoke", () => {
|
|
const workflow = readRequired(bundlesWorkflowPath);
|
|
const verifier = readRequired(verifierPath);
|
|
|
|
const verifyJob = job(workflow, "verify-ocr", "verify-ocr-nvidia");
|
|
expect(verifyJob).toContain("--network none");
|
|
expect(verifyJob).toContain("--memory 3g --memory-swap 3g");
|
|
expect(verifyJob).toContain('"${TARGET}" memory-preflight');
|
|
expect(verifyJob).toContain("--memory 4g --memory-swap 4g");
|
|
expect(verifyJob).toContain("timeout-minutes: 90");
|
|
expect(verifyJob).toContain("docker/verify-ocr-runtime.sh");
|
|
expect(verifyJob).toContain("Verify image has the release trust identity");
|
|
// The identity is verified against the baked trust FILE, which the runtime
|
|
// reads, not the OCR_RUNTIME_INDEX_* env vars, which the official image leaves
|
|
// unset by design. Reading the env compared "" and failed a correct image.
|
|
expect(verifyJob).toContain("cat /app/docker/ocr-runtime-trust.json");
|
|
expect(verifyJob).toContain(".keys[0].keyId");
|
|
expect(verifyJob).toContain("Baked OCR trust key ID");
|
|
expect(verifyJob).toContain("Baked OCR trust public key does not match");
|
|
expect(verifyJob).not.toContain('printf %s "$OCR_RUNTIME_INDEX_KEY_ID"');
|
|
expect(verifyJob).toContain(
|
|
'actual_official_container="$(docker run --rm --entrypoint sh "${SNAPOTTER_BUNDLE_IMAGE}"',
|
|
);
|
|
expect(verifyJob).toContain("Verification image is not marked as an official container");
|
|
expect(verifier).toContain("install_runtime.py");
|
|
expect(verifier).toContain("--preverified-index");
|
|
expect(verifier).toContain("--expected-index-sha256");
|
|
expect(verifier).toContain('f"{{runtime}}/{entrypoint}", "--smoke"');
|
|
expect(verifier).toContain("SNAPOTTER_NETWORK_DISABLED=1");
|
|
expect(verifier).toContain('SNAPOTTER_OCR_PROVIDERS_JSON="[\\"CPUExecutionProvider\\"]"');
|
|
expect(verifier).toContain('"provider": "CPUExecutionProvider"');
|
|
expect(verifier).toContain('"device": "cpu"');
|
|
expect(verifier).toContain("/fixtures/image/valid/ocr-clean.png");
|
|
expect(verifier).toContain("/fixtures/image/valid/ocr-chat.jpeg");
|
|
expect(verifier).toContain("/fixtures/image/valid/ocr-japanese.png");
|
|
expect(verifier).toContain("verify-ocr-rotated.png");
|
|
expect(verifier).toContain("verify-ocr-korean.png");
|
|
expect(verifier).toContain("verify-ocr-blank.png");
|
|
expect(verifier).toContain("verify-ocr-german.png");
|
|
expect(verifier).toContain("verify-ocr-french.png");
|
|
expect(verifier).toContain("verify-ocr-spanish.png");
|
|
expect(verifier).toContain("verify-ocr-chinese.png");
|
|
expect(verifier).toContain("verify-ocr-noisy.png");
|
|
expect(verifier).toContain("verify-ocr-boundary.png");
|
|
expect(verifier).toContain("BOUNDARY OCR 505");
|
|
expect(verifier).toContain("np.full((5000, 8000, 3)");
|
|
expect(verifier).toContain("verify-ocr-rotated-90.png");
|
|
expect(verifier).toContain("verify-ocr-rotated-270.png");
|
|
expect(verifier).toContain("verify-ocr-skewed.png");
|
|
expect(verifier).toContain("verify-ocr-mixed-script.png");
|
|
expect(verifier).toContain("normalized_edit_error");
|
|
expect(verifier).toContain("raw_edit_error");
|
|
expect(verifier).toContain("quality_report");
|
|
expect(verifier).toContain("memory.peak");
|
|
expect(verifier).toContain("runtime_tree_snapshot");
|
|
expect(verifier).toContain("BEST_REGRESSION_TOLERANCE");
|
|
expect(verifier).toContain("best_error > balanced_error + BEST_REGRESSION_TOLERANCE");
|
|
expect(verifier).toContain('if language == "auto"');
|
|
expect(verifier).toContain("korean-PP-OCRv5");
|
|
expect(verifier).toContain("안녕하세요 스냅오터 OCR 505");
|
|
expect(verifier).toContain("Blank OCR hallucinated text");
|
|
expect(verifier).toContain('("german", generated_german');
|
|
expect(verifier).toContain('("french", generated_french');
|
|
expect(verifier).toContain('("spanish", generated_spanish');
|
|
expect(verifier).toContain('("chinese", generated_chinese');
|
|
expect(verifier).toContain('f"{label}-best"');
|
|
expect(verifier).toContain("clean-enhanced-best");
|
|
expect(verifier).toContain("noisy-enhanced-best");
|
|
expect(verifier).toContain("blank-enhanced-best");
|
|
expect(verifier).toContain(
|
|
'JSON.stringify({ enhance: true, language: "auto", quality: "best" })',
|
|
);
|
|
expect(verifier).toContain(
|
|
'!String(result.text ?? "").toLowerCase().includes("quick brown fox")',
|
|
);
|
|
expect(verifier).toContain('result.actualQuality !== "best"');
|
|
expect(verifier).toContain("runOcrRuntime");
|
|
expect(verifier).toContain("application OCR runtime lifecycle");
|
|
expect(verifier).toContain("application Fast OCR lifecycle");
|
|
expect(verifier).toContain("application Fast PDF OCR lifecycle");
|
|
expect(verifier).toContain('DATA_DIR="/tmp/verify-ocr-data"');
|
|
expect(verifier).toContain('AI_DATA_DIR="${DATA_DIR}/ai"');
|
|
expect(verifier).toContain('env AI_DATA_DIR="${AI_DATA_DIR}" DATA_DIR="${DATA_DIR}"');
|
|
expect(verifier).toContain("SNAPOTTER_OFFICIAL_CONTAINER=1");
|
|
expect(verifier).toContain("PYTHONDONTWRITEBYTECODE=1");
|
|
expect(verifier).toContain("PYTHONUNBUFFERED=1");
|
|
expect(verifier).toContain("leaked a generation lease");
|
|
expect(verifier).toContain('OCR_VERIFY_TIMEOUT_SECONDS="${OCR_VERIFY_TIMEOUT_SECONDS:-1200}"');
|
|
expect(verifier).toContain(
|
|
'timeout --signal=TERM --kill-after=10s "${OCR_VERIFY_TIMEOUT_SECONDS}s" env -i',
|
|
);
|
|
expect(verifier).toContain("OCR artifact metadata is not canonical JSON");
|
|
expect(verifier).toContain('VERIFY_MODE="${2:-full}"');
|
|
expect(verifier).toContain('"memory-preflight"');
|
|
expect(verifier).toContain('resources.get("minimumMemoryBytes") != 4 * 1024 * 1024 * 1024');
|
|
expect(verifier).toContain("insufficient memory for accurate OCR runtime");
|
|
expect(verifier).toContain("memory preflight mutated runtime state");
|
|
expect(verifier).toContain("Second OCR install was not idempotent");
|
|
expect(verifier).toContain("handoff_and_commit_runtime");
|
|
expect(verifier).toContain("handoffOcrDispatcher");
|
|
expect(verifier).toContain("probeOcrDispatcher");
|
|
expect(verifier).toContain('"commit"');
|
|
expect(verifier).toContain("pending first-install activation");
|
|
expect(verifier).toContain('reconcile --ai-data-dir "${AI_DATA_DIR}"');
|
|
expect(verifier).toContain("committedGeneration");
|
|
expect(verifier).toContain("restoredGeneration");
|
|
expect(verifier).toContain("rollback --ai-data-dir");
|
|
expect(verifier).toContain("deactivate --ai-data-dir");
|
|
expect(verifier).toContain("reset --ai-data-dir");
|
|
expect(verifier).toContain('run_runtime_transaction import "${INDEX}"');
|
|
expect(verifier).toContain("drainOcrDispatcher");
|
|
expect(verifier).toContain("rotationCgroupPeakBytes");
|
|
expect(verifier).toContain("oldChildAliveDuringHandoff");
|
|
expect(verifier).toContain("oldChildExited");
|
|
expect(verifier).toContain("oldRequestCompleted");
|
|
expect(verifier).toContain("handoffOcrDispatcher");
|
|
expect(verifier).not.toMatch(/paddleocr|onnxruntime-gpu/i);
|
|
});
|
|
|
|
it("runs native verification as the production user and covers arbitrary UID with GID 0", () => {
|
|
const workflow = readRequired(bundlesWorkflowPath);
|
|
const verifier = readRequired(verifierPath);
|
|
const verifyJob = job(workflow, "verify-ocr", "verify-ocr-nvidia");
|
|
const gpuJob = job(workflow, "verify-ocr-nvidia", "sign-ocr-index");
|
|
const nativeStep = verifyJob.slice(
|
|
verifyJob.indexOf("Verify offline install and runtime smoke"),
|
|
verifyJob.indexOf("Upload native OCR quality and resource report"),
|
|
);
|
|
const gpuStep = gpuJob.slice(
|
|
gpuJob.indexOf("Verify the portable CPU runtime with NVIDIA exposed"),
|
|
gpuJob.indexOf("Upload NVIDIA-exposed CPU OCR quality and resource report"),
|
|
);
|
|
|
|
expect(verifier).toContain('if [[ "$(id -u)" -eq 0 ]]; then');
|
|
expect(verifier).toContain("OCR verifier must run as an unprivileged user");
|
|
expect(verifier).toContain("full | install-smoke | memory-preflight)");
|
|
expect(verifier).toContain('if [[ "${VERIFY_MODE}" == "install-smoke" ]]');
|
|
const installSmokeExit = verifier.indexOf("PASS: OCR signed install smoke lifecycle");
|
|
expect(installSmokeExit).toBeGreaterThan(verifier.indexOf("pending first-install activation"));
|
|
expect(installSmokeExit).toBeGreaterThan(verifier.indexOf("handoff_and_commit_runtime"));
|
|
expect(installSmokeExit).toBeGreaterThan(
|
|
verifier.indexOf("Second OCR install was not idempotent"),
|
|
);
|
|
expect(installSmokeExit).toBeGreaterThan(
|
|
verifier.indexOf('reset --ai-data-dir "${AI_DATA_DIR}"'),
|
|
);
|
|
expect(verifier).toContain('find "${OCR_VERIFY_REPORT_DIR}" -type f -exec chmod a+r {} +');
|
|
|
|
for (const step of [nativeStep, gpuStep]) {
|
|
expect(step).not.toContain("--entrypoint");
|
|
expect(step).toContain("-e EMBEDDED=0");
|
|
expect(step).toContain('-e PUID="${RUNNER_UID}"');
|
|
expect(step).toContain('-e PGID="${RUNNER_GID}"');
|
|
expect(step).toContain("/verify-ocr-runtime.sh");
|
|
expect(step).toContain("full");
|
|
expect(step).toContain("rm -rf /tmp/ocr-quality");
|
|
expect(step).toContain("install -d -m 1777 /tmp/ocr-quality");
|
|
expect(step.indexOf("rm -rf /tmp/ocr-quality")).toBeLessThan(
|
|
step.indexOf("install -d -m 1777 /tmp/ocr-quality"),
|
|
);
|
|
expect(step).toContain("chmod 0755 /tmp/ocr-quality");
|
|
expect(step).toContain("test -r");
|
|
}
|
|
expect(nativeStep.match(/-e EMBEDDED=0/g)).toHaveLength(3);
|
|
expect(gpuStep.match(/-e EMBEDDED=0/g)).toHaveLength(1);
|
|
expect(nativeStep).toContain("memory-preflight");
|
|
expect(nativeStep).toContain("--user 20001:0");
|
|
expect(nativeStep).toContain('"${TARGET}" install-smoke');
|
|
});
|
|
|
|
it("gates the exact Fast OCR language payload while keeping its layer size approximate", () => {
|
|
const workflow = readRequired(bundlesWorkflowPath);
|
|
const verifyJob = job(workflow, "verify-ocr", "verify-ocr-nvidia");
|
|
|
|
expect(verifyJob).toContain("Verify exact Fast OCR payload");
|
|
expect(verifyJob).toContain("-e EMBEDDED=0");
|
|
expect(verifyJob).toContain("chi_sim deu eng fra jpn spa");
|
|
expect(verifyJob).toContain("osd.traineddata");
|
|
expect(verifyJob).toContain("kor.traineddata");
|
|
expect(verifyJob).toContain("14003738");
|
|
expect(verifyJob).toContain("about 25 MiB remains a measured approximate image-layer size");
|
|
const fastPayloadStep = verifyJob.slice(
|
|
verifyJob.indexOf("Verify exact Fast OCR payload"),
|
|
verifyJob.indexOf("Verify offline install and runtime smoke"),
|
|
);
|
|
expect(fastPayloadStep.match(/-e EMBEDDED=0/g)).toHaveLength(1);
|
|
});
|
|
|
|
it("blocks each supported OCR language on complete cohort and document metrics", () => {
|
|
const verifier = readRequired(verifierPath);
|
|
|
|
for (const category of [
|
|
"clean",
|
|
"ui",
|
|
"scene",
|
|
"degradation",
|
|
"rotation-90",
|
|
"rotation-180",
|
|
"rotation-270",
|
|
"small-angle",
|
|
]) {
|
|
expect(verifier).toContain(`"${category}"`);
|
|
}
|
|
expect(verifier).toContain("LANGUAGE_QUALITY_CASES");
|
|
expect(verifier).toContain("REQUIRED_LANGUAGE_COHORTS");
|
|
expect(verifier).toContain("grapheme_clusters");
|
|
expect(verifier).toContain("graphemeCer");
|
|
expect(verifier).toContain("wordErrorRate");
|
|
expect(verifier).toContain("insertionCount");
|
|
expect(verifier).toContain("catastrophicFailure");
|
|
expect(verifier).toContain("catastrophicFailureCount");
|
|
expect(verifier).toContain("polygonCoverage");
|
|
expect(verifier).toContain("minimum_polygon_coverage");
|
|
expect(verifier).toContain("memory.current");
|
|
expect(verifier).toContain("memoryCheckpoints");
|
|
expect(verifier).toContain('record_memory_checkpoint("fixtures-generated")');
|
|
expect(verifier).toContain('record_memory_checkpoint("40mp-boundary-complete")');
|
|
expect(verifier).toContain("verify-ocr-multipage.pdf");
|
|
expect(verifier).toContain('pages: "3,1"');
|
|
expect(verifier).toContain('"--- Page 1 ---"');
|
|
expect(verifier).toContain('"--- Page 3 ---"');
|
|
expect(verifier).toContain("PAGE TWO OMIT 505");
|
|
expect(verifier).toContain("exactPageRecall");
|
|
expect(verifier).toContain("pageOrderCorrect");
|
|
expect(verifier).toContain('report.get("minimumMemoryBytes") != 4 * 1024 * 1024 * 1024');
|
|
expect(verifier).toContain("rotationCgroupPeakBytes");
|
|
});
|
|
|
|
it("proves shared OCR deactivation drains every replica without breaking an old lease", () => {
|
|
const verifier = readRequired(verifierPath);
|
|
|
|
expect(verifier).toContain("rotateOcrDispatcher");
|
|
expect(verifier).toContain("OCR_REMOTE_REPLICA_READY");
|
|
expect(verifier).toContain("OCR_REMOTE_REPLICA_DONE");
|
|
expect(verifier).toContain('"deactivate"');
|
|
expect(verifier).toContain("remoteReplicaIdleChildExited");
|
|
expect(verifier).toContain("localIdleChildExited");
|
|
expect(verifier).toContain("oldLeaseActiveAtReplicaDeactivation");
|
|
expect(verifier).toContain("oldGenerationLockHeldDuringRequest");
|
|
expect(verifier).toContain("oldGenerationLockReleasedAfterRequest");
|
|
expect(verifier).toContain("fcntl.LOCK_EX | fcntl.LOCK_NB");
|
|
expect(verifier).toContain("replicaDeactivationSucceeded");
|
|
expect(verifier).toContain("leasedGenerationPreservedAfterDeactivation");
|
|
expect(verifier).toContain("deactivatedGenerationCollectedAfterReplicaDrain");
|
|
expect(verifier).toContain('command.split("\\0").filter(Boolean)');
|
|
});
|
|
|
|
it("blocks native release on every licensed real fixture across Fast, Balanced, and Best", () => {
|
|
const workflow = readRequired(bundlesWorkflowPath);
|
|
const verifyJob = job(workflow, "verify-ocr", "verify-ocr-nvidia");
|
|
const verifier = readRequired(verifierPath);
|
|
|
|
expect(verifyJob).toContain('-v "$PWD/tests/fixtures:/fixtures:ro"');
|
|
expect(verifier).toContain('REAL_FIXTURE_MANIFEST = REAL_FIXTURE_ROOT / "manifest.json"');
|
|
expect(verifier).toContain(
|
|
'REAL_FIXTURE_MANIFEST_SHA256 = "979c2ce9fbae524a2627e3b12ba785d5f3c2d73b2c372d486e66f7a1fd248f5f"',
|
|
);
|
|
for (const fixtureId of [
|
|
"jawildtext-board-0001",
|
|
"jawildtext-board-0049",
|
|
"jawildtext-board-0127",
|
|
"commons-hagye-station-715",
|
|
"jawildtext-receipt-11120",
|
|
"cord-v2-test-0080",
|
|
"clinocr-poor-t7-s2",
|
|
]) {
|
|
expect(verifier).toContain(`"${fixtureId}"`);
|
|
}
|
|
expect(verifier).toContain('REAL_CORPUS_QUALITIES = ("fast", "balanced", "best")');
|
|
expect(verifier).toContain("REAL_CORPUS_HELD_OUT_POLICY");
|
|
expect(verifier).toContain("REAL_BOARD_COHORT_EXPECTED");
|
|
expect(verifier).toContain("REAL_KOREAN_COHORT_EXPECTED");
|
|
expect(verifier).toContain("REAL_KOREAN_GROUND_TRUTH_EXPECTED");
|
|
expect(verifier).toContain('"status": "REJECTED_AFTER_FROZEN_GATE"');
|
|
expect(verifier).toContain('"enforcedBehavior": "reject-before-tesseract-spawn"');
|
|
expect(verifier).toContain(
|
|
'"diagnosticManifestSha256": "8e82e22b5d939ca40e97d8a74e8ce73dda451e99ba8b33a95a94723c4679d1b4"',
|
|
);
|
|
expect(verifier).toContain(
|
|
'"qualityCheckpointSha256": "41e4c01959602c3c255c09210fd3203ab80eee696bf28578d1137bf07f38bab7"',
|
|
);
|
|
expect(verifier).toContain(
|
|
'"fastReportSha256": "0a9743c46e67aad7948d5a5880bfbd5096a4023c6006de5f94668580a2df276a"',
|
|
);
|
|
expect(verifier).toContain(
|
|
'"fastTextSha256": "6ddecdd52dd0a66074fb3c1d003a28493975f63421f70600706ccc01b7bb2c73"',
|
|
);
|
|
expect(verifier).toContain('"releaseGatePassed": False');
|
|
expect(verifier).toContain('"tokenRecall": 0.1875');
|
|
expect(verifier).toContain('"tokenPrecision": 0.25');
|
|
expect(verifier).toContain('"tokenF1": 0.214286');
|
|
expect(verifier).toContain('"selectionStatus": "FROZEN_BEFORE_ANY_OCR_OUTPUT"');
|
|
expect(verifier).toContain('"minimumTokenRecall": 0.25');
|
|
expect(verifier).toContain('"minimumTokenPrecision": 0.50');
|
|
expect(verifier).toContain('"minimumTokenF1": 0.32');
|
|
expect(verifier).toContain("verified_real_fixture_path");
|
|
expect(verifier).toContain("file_sha256");
|
|
expect(verifier).toContain('annotation.get("exactGroundTruth") != ground_truth[:-1]');
|
|
expect(verifier).toContain('annotation.get("source") != expected_source');
|
|
expect(verifier).toContain("run_fast_real_ocr");
|
|
expect(verifier).toContain("SNAPOTTER_FAST_REAL_RESULTS");
|
|
expect(verifier).toContain("const expectedFixtureIds = new Set([");
|
|
expect(verifier).toContain("fixtureIds.length !== expectedFixtureIds.size");
|
|
expect(verifier).toContain("recordIds.length !== expectedFixtureIds.size");
|
|
expect(verifier).toContain("const persistFastRealResults = async () =>");
|
|
expect(verifier).toContain("FAST_KOREAN_UNSUPPORTED_REASON");
|
|
expect(verifier).toContain("koreanTesseractSentinelPath");
|
|
expect(verifier).toContain("koreanTesseractMarkerPath");
|
|
expect(verifier).toContain("process.env.TESSERACT_PATH = koreanTesseractSentinelPath");
|
|
expect(verifier).toContain(
|
|
'await extractText(image, scratch, { quality: "fast", language: "ko" })',
|
|
);
|
|
expect(verifier).toContain('releaseGateStatus: "unsupported-by-design"');
|
|
expect(verifier).toContain("releaseGatePassed: null");
|
|
expect(verifier).toContain("supported: false");
|
|
expect(verifier).toContain("decisionEnforced: true");
|
|
expect(verifier).toContain("tesseractSpawned: false");
|
|
expect(verifier).toContain(
|
|
'FAST_REAL_RESULTS="${OCR_VERIFY_REPORT_DIR}/ocr-${TARGET}-${OCR_VERIFY_ENVIRONMENT}.fast-real.json"',
|
|
);
|
|
expect(verifier).toContain("Fast real OCR evidence: ${FAST_REAL_RESULTS}");
|
|
expect(verifier).toMatch(
|
|
/records\[fixture\.id\] = result;\s+await persistFastRealResults\(\);/,
|
|
);
|
|
expect(verifier).toMatch(
|
|
/verificationError:\s+error instanceof Error[\s\S]*await persistFastRealResults\(\);[\s\S]*throw error;/,
|
|
);
|
|
expect(verifier).not.toContain("Object.keys(records).length !== 4");
|
|
expect(verifier).toContain("fastLanguageCohorts");
|
|
expect(verifier).toContain('extractText(image, scratch, { quality: "fast"');
|
|
expect(verifier).not.toContain('["fast-ko-clean"');
|
|
expect(verifier).not.toContain('["fast-ko-pure-clean"');
|
|
expect(verifier).toContain('expectedNormalized: expectedNormalized.join("")');
|
|
expect(verifier).toContain('actualNormalized: actualNormalized.join("")');
|
|
expect(verifier).toContain("await persistFastLanguageReport(failureMessage)");
|
|
expect(verifier).toContain("throw new Error(failureMessage)");
|
|
expect(verifier.indexOf("records.push(record)")).toBeLessThan(
|
|
verifier.indexOf("await persistFastLanguageReport(failureMessage)"),
|
|
);
|
|
expect(verifier.indexOf("await persistFastLanguageReport(failureMessage)")).toBeLessThan(
|
|
verifier.indexOf("throw new Error(failureMessage)"),
|
|
);
|
|
expect(verifier).toContain("runtime.recognize_image");
|
|
expect(verifier).toContain("real_token_multiset");
|
|
expect(verifier).toContain("minimumTokenRecall");
|
|
expect(verifier).toContain("minimumTokenPrecision");
|
|
expect(verifier).toContain("minimumTokenF1");
|
|
expect(verifier).toContain("maximumGraphemeCer");
|
|
expect(verifier).toContain("maximumWordErrorRate");
|
|
expect(verifier).toContain('"releaseGatePassed": not release_failures');
|
|
expect(verifier).toContain('"releaseGateFailures": release_failures');
|
|
expect(verifier).toContain('"manifestSha256": REAL_FIXTURE_MANIFEST_SHA256');
|
|
expect(verifier).toContain('quality_report["realCorpusAttempt"] = attempt');
|
|
expect(verifier).toContain('attempt["status"] = "failed"');
|
|
expect(verifier).toContain('attempt["status"] = "completed"');
|
|
expect(verifier).toContain("real OCR release gate failed across held-out cases");
|
|
expect(verifier).toContain('"id": "auto"');
|
|
expect(verifier).toContain('const selectors = { en: "en", id: "auto", ja: "ja", ko: "ko" };');
|
|
expect(verifier).toContain("has_source_shard == has_source_file");
|
|
expect(verifier).toContain("0x1100 <= ord(character) <= 0x11FF");
|
|
expect(verifier).toContain("0xAC00 <= ord(character) <= 0xD7AF");
|
|
expect(verifier).toContain('("board-or-sign", "ko", "fast")');
|
|
expect(verifier).toContain('("board-or-sign", "ko", "balanced")');
|
|
expect(verifier).toContain('("board-or-sign", "ko", "best")');
|
|
expect(verifier).toContain('REAL_KOREAN_COHORT_EXPECTED["perImageTierFloors"][quality]');
|
|
expect(verifier).toContain('expected_korean_cohort["perImageTierFloors"][quality]');
|
|
expect(verifier).toContain('case.get("releaseGateStatus") != "unsupported-by-design"');
|
|
expect(verifier).toContain('case.get("releaseGatePassed") is not None');
|
|
expect(verifier).toContain('case.get("supported") is not False');
|
|
expect(verifier).toContain('case.get("decisionEnforced") is not True');
|
|
expect(verifier).not.toContain('"eng+deu+fra+spa+chi_sim+jpn+kor"');
|
|
expect(verifier).toContain("expected_real_fixtures");
|
|
expect(verifier).toContain("expected_real_cases");
|
|
expect(verifier).not.toContain("REAL_CORPUS_CALIBRATION");
|
|
});
|
|
|
|
it("inventories and vulnerability-gates each verified OCR runtime artifact", () => {
|
|
const workflow = readRequired(bundlesWorkflowPath);
|
|
const verifyJob = job(workflow, "verify-ocr", "verify-ocr-nvidia");
|
|
const publishJob = job(workflow, "publish");
|
|
|
|
const runtimeVerification = verifyJob.indexOf("Verify offline install and runtime smoke");
|
|
const extraction = verifyJob.indexOf(
|
|
"Extract verified OCR runtime for supply-chain inspection",
|
|
);
|
|
const sbom = verifyJob.indexOf("Generate per-target OCR runtime SBOMs");
|
|
const vulnerabilityGate = verifyJob.indexOf("Gate OCR runtime vulnerabilities");
|
|
|
|
expect(runtimeVerification).toBeGreaterThanOrEqual(0);
|
|
expect(extraction).toBeGreaterThan(runtimeVerification);
|
|
expect(sbom).toBeGreaterThan(extraction);
|
|
expect(vulnerabilityGate).toBeGreaterThan(sbom);
|
|
expect(verifyJob).toContain('artifact["archive"]["sha256"]');
|
|
expect(verifyJob).toContain(
|
|
'artifact.get("resources", {}).get("minimumMemoryBytes") != 4 * 1024 * 1024 * 1024',
|
|
);
|
|
expect(verifyJob).toContain("tarfile.data_filter");
|
|
expect(verifyJob).toContain(
|
|
'expected_files = {record["path"]: record for record in artifact["files"]}',
|
|
);
|
|
expect(verifyJob).toContain("Extracted OCR runtime file manifest mismatch");
|
|
expect(verifyJob).not.toContain('scan_root / "runtime-manifest.json"');
|
|
expect(verifyJob).toContain("dir:/tmp/ocr-runtime-scan");
|
|
expect(verifyJob).toContain("Install pinned Syft 1.42.3 from verified release bytes");
|
|
expect(verifyJob).toContain("0d6be741479eddd2c8644a288990c04f3df0d609bbc1599a005532a9dff63509");
|
|
expect(verifyJob).toContain("ocr-${TARGET}-sbom.cdx.json");
|
|
expect(verifyJob).toContain("ocr-${TARGET}-sbom.spdx.json");
|
|
expect(verifyJob).toContain('--source-name "snapotter-ocr-${TARGET}"');
|
|
expect(verifyJob).toContain('--source-version "${VERSION}"');
|
|
expect(verifyJob).toContain(
|
|
"aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25",
|
|
);
|
|
expect(verifyJob).toContain("scan-type: sbom");
|
|
expect(verifyJob).toContain(
|
|
"scan-ref: /tmp/ocr-security/ocr-${{ matrix.target }}-sbom.cdx.json",
|
|
);
|
|
expect(verifyJob).not.toContain("scan-type: fs");
|
|
expect(verifyJob).toContain('severity: "CRITICAL,HIGH"');
|
|
expect(verifyJob).toContain('exit-code: "1"');
|
|
expect(verifyJob).toContain("ocr-${{ matrix.target }}-trivy.json");
|
|
expect(verifyJob).toContain("ocr-quality-${{ matrix.target }}");
|
|
expect(verifyJob).toContain("*.quality.json");
|
|
expect(verifyJob).toContain("name: ocr-security-${{ matrix.target }}");
|
|
expect(verifyJob).toContain("if-no-files-found: error");
|
|
expect(publishJob).toContain("expected_attestations = {");
|
|
expect(publishJob).toContain('f"ocr-{target}-sbom.cdx.json"');
|
|
expect(publishJob).toContain('f"ocr-{target}-sbom.spdx.json"');
|
|
expect(publishJob).toContain('f"ocr-{target}-trivy.json"');
|
|
expect(publishJob).toContain('f"ocr-{target}-native-cpu.quality.json"');
|
|
expect(publishJob).toContain('"ocr-linux-amd64-cpu-py312-nvidia-exposed-cpu.quality.json"');
|
|
expect(publishJob).toContain("Signed OCR attestation closure mismatch");
|
|
});
|
|
|
|
it("hash-locks tooling before exposing the HuggingFace write token", () => {
|
|
const workflow = readRequired(bundlesWorkflowPath);
|
|
const publishJob = job(workflow, "publish");
|
|
const requirements = readRequired(hfReleaseRequirementsPath);
|
|
|
|
expect(publishJob).toContain("actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0");
|
|
expect(publishJob).toContain("ref: ${{ inputs.release_commit }}");
|
|
expect(publishJob).toContain("persist-credentials: false");
|
|
expect(publishJob).toContain("actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1");
|
|
expect(publishJob).toContain('python-version: "3.11.14"');
|
|
expect(publishJob).toContain("/tmp/hf-release-venv/bin/python -m pip install");
|
|
expect(publishJob).toContain("--disable-pip-version-check --require-hashes --no-deps");
|
|
expect(publishJob).toContain("--only-binary=:all:");
|
|
expect(publishJob).toContain("docker/hf-release-requirements.txt");
|
|
expect(publishJob.match(/\/tmp\/hf-release-venv\/bin\/python - <<'PY'/g)).toHaveLength(2);
|
|
expect(publishJob).not.toMatch(/pip install -U|huggingface_hub\[hf_xet\](?:\s|")/);
|
|
expect(requirements).toContain("huggingface-hub==0.36.2");
|
|
expect(requirements).toContain("hf-xet==");
|
|
expect(requirements).toContain("--hash=sha256:");
|
|
});
|
|
|
|
it("signs one canonical two-target index and verifies it before upload", () => {
|
|
const workflow = readRequired(bundlesWorkflowPath);
|
|
const signJob = job(workflow, "sign-ocr-index", "verify-signed-ocr-index");
|
|
const verifySignedJob = job(workflow, "verify-signed-ocr-index", "publish");
|
|
const publishJob = job(workflow, "publish");
|
|
|
|
expect(workflow).toContain("OCR_RUNTIME_INDEX_SIGNING_KEY_B64:");
|
|
expect(workflow).toContain("required: true");
|
|
expect(signJob).toContain("needs: [build-ocr, verify-ocr, verify-ocr-nvidia]");
|
|
expect(signJob).toContain("OCR_RUNTIME_INDEX_KEY_ID");
|
|
expect(signJob).toContain("OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64");
|
|
expect(signJob).toContain("Checkout exact release tag for provenance verification");
|
|
expect(signJob).toContain("SNAPOTTER_RELEASE_COMMIT");
|
|
expect(signJob).toContain("name: digests-linux-amd64");
|
|
expect(signJob).toContain("name: digests-linux-arm64");
|
|
expect(signJob).toContain(
|
|
'provenance.get("sourceImageDigest") != expected_image_digests[target]',
|
|
);
|
|
expect(signJob).toContain(`[[ "\${OCR_RUNTIME_INDEX_KEY_ID}" =~ ^[A-Za-z0-9]`);
|
|
expect(signJob).toContain("openssl pkeyutl -sign -rawin");
|
|
expect(signJob).toContain("openssl pkeyutl -verify -rawin");
|
|
expect(signJob).toContain("ocr-runtime-index.json");
|
|
expect(signJob).toContain("ocr-runtime-trusted-keys.json");
|
|
expect(signJob).toContain("pattern: ocr-security-*");
|
|
expect(signJob).toContain("pattern: ocr-quality-*");
|
|
expect(signJob).toContain('"attestations": attestations');
|
|
expect(signJob).toContain('"publicKey": (root / "trusted-public.pem").read_text()');
|
|
expect(signJob).toContain(
|
|
'expected_targets = {"linux-amd64-cpu-py312", "linux-arm64-cpu-py311"}',
|
|
);
|
|
expect(signJob).toContain('model_objects = artifact.get("modelObjects")');
|
|
expect(signJob).toContain("model provenance does not bind its immutable revision");
|
|
expect(signJob).toContain('"snapotter-agpl"');
|
|
expect(signJob).toContain('"apache-2.0"');
|
|
expect(signJob).toContain('"antlr-4.9.3-license"');
|
|
expect(signJob).toContain('file_records.get("THIRD_PARTY_NOTICES.json")');
|
|
expect(signJob).toContain('provenance = artifact.get("provenance")');
|
|
expect(signJob).toContain(
|
|
'artifact.get("resources", {}).get("minimumMemoryBytes") != 4 * 1024 * 1024 * 1024',
|
|
);
|
|
expect(signJob).toContain("invalid build provenance");
|
|
expect(signJob).toContain("OCR signing private key must use canonical base64");
|
|
expect(signJob).toContain("OCR runtime public key must use canonical base64");
|
|
expect(verifySignedJob).toContain("needs: sign-ocr-index");
|
|
expect(verifySignedJob).toContain("runner: ubuntu-latest");
|
|
expect(verifySignedJob).toContain("runner: ubuntu-24.04-arm");
|
|
expect(verifySignedJob).toContain("name: ocr-runtime-metadata");
|
|
expect(verifySignedJob).toContain("digests-${{ matrix.image_platform }}");
|
|
expect(verifySignedJob).toContain("OCR_RUNTIME_INDEX_MAX_BYTES");
|
|
expect(verifySignedJob).toContain("loadOcrRuntimeTrustKeys");
|
|
expect(verifySignedJob).toContain("verifyRuntimeIndex");
|
|
expect(verifySignedJob).toContain("/app/packages/ai/src/runtime-index.ts");
|
|
expect(verifySignedJob).not.toContain("OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64");
|
|
expect(verifySignedJob).not.toContain("--entrypoint");
|
|
expect(verifySignedJob).toContain("-e EMBEDDED=0");
|
|
expect(verifySignedJob.match(/-e EMBEDDED=0/g)).toHaveLength(1);
|
|
expect(publishJob).toContain("needs: [verify-ocr, sign-ocr-index, verify-signed-ocr-index]");
|
|
expect(publishJob).toContain('f"ocr-{target}.artifact.json"');
|
|
expect(publishJob).toContain("ocr-runtime-index.json");
|
|
expect(publishJob).toContain("v3.mkdir(parents=True)");
|
|
expect(publishJob).toContain("missing or colliding OCR release objects");
|
|
expect(publishJob).toContain("file.relative_to(root).as_posix()");
|
|
expect(publishJob).toContain("HfApi");
|
|
expect(publishJob).toContain("file_exists");
|
|
expect(publishJob).toContain('for entry in existing_manifest["files"]');
|
|
expect(publishJob).toContain("Existing bundle object has wrong digest");
|
|
expect(publishJob).toContain('openssl", "pkeyutl", "-verify"');
|
|
expect(publishJob).toContain("Existing release contains different OCR runtime artifacts");
|
|
expect(publishJob).toContain("is_variable_ocr_attestation");
|
|
expect(publishJob).toContain("Signed OCR attestation digest mismatch");
|
|
expect(publishJob).toContain("Signed OCR artifact closure mismatch");
|
|
expect(publishJob).toContain("Candidate OCR runtime index signature is invalid");
|
|
expect(publishJob).toContain("actual_objects != expected_objects");
|
|
expect(publishJob).toContain("Candidate feature-bundle manifest closure mismatch");
|
|
expect(publishJob).toContain(
|
|
"Existing feature-bundle version has uncommitted or missing objects",
|
|
);
|
|
expect(publishJob).toContain("BUNDLE_RELEASE_EXISTS=true");
|
|
expect(publishJob).toContain("if: env.BUNDLE_RELEASE_EXISTS != 'true'");
|
|
expect(publishJob).toContain('"files": files');
|
|
});
|
|
|
|
it("rejects signed artifacts whose measured sizes drift beyond the release tolerance", () => {
|
|
const workflow = readRequired(bundlesWorkflowPath);
|
|
const signJob = job(workflow, "sign-ocr-index", "verify-signed-ocr-index");
|
|
const contract = measuredEstimateContract(signJob);
|
|
const python = `${contract}\n\nimport json\nimport sys\npayload = json.loads(sys.argv[1])\nvalidate_measured_estimate(\n "linux-amd64-cpu-py312", payload["manifestTarget"], payload["archive"]\n)\n`;
|
|
const manifestTarget = {
|
|
compressedSizeEstimate: 200_000_001,
|
|
extractedSizeEstimate: 500_000_000,
|
|
sizeKind: "measured-estimate",
|
|
};
|
|
const archive = { expandedSize: 505_000_000, size: 202_000_001 };
|
|
const runContract = (
|
|
candidateManifestTarget: Record<string, unknown>,
|
|
candidateArchive: Record<string, unknown>,
|
|
) =>
|
|
execFileSync(
|
|
"python3",
|
|
[
|
|
"-c",
|
|
python,
|
|
JSON.stringify({ archive: candidateArchive, manifestTarget: candidateManifestTarget }),
|
|
],
|
|
{ stdio: "pipe" },
|
|
);
|
|
|
|
expect(signJob).toContain('Path("docker/feature-manifest.json")');
|
|
expect(signJob).toContain("manifest_targets[target]");
|
|
expect(signJob).toContain("validate_measured_estimate(target");
|
|
expect(() => runContract(manifestTarget, archive)).not.toThrow();
|
|
|
|
expect(() =>
|
|
runContract(manifestTarget, {
|
|
...archive,
|
|
size: 202_000_002,
|
|
}),
|
|
).toThrow();
|
|
expect(() =>
|
|
runContract(
|
|
{
|
|
...manifestTarget,
|
|
extractedSizeEstimate: true,
|
|
},
|
|
archive,
|
|
),
|
|
).toThrow();
|
|
expect(() =>
|
|
runContract(
|
|
{
|
|
...manifestTarget,
|
|
sizeKind: "measured-build",
|
|
},
|
|
archive,
|
|
),
|
|
).toThrow();
|
|
});
|
|
|
|
it("keeps public image tags behind the verified signed-index gate", () => {
|
|
const release = readRequired(releaseWorkflowPath);
|
|
const releaseJob = job(release, "release", "prebuilt");
|
|
const docker = job(release, "docker", "scan");
|
|
const aiBundles = job(release, "ai-bundles", "manifest");
|
|
const manifest = job(release, "manifest");
|
|
|
|
expect(releaseJob).toContain("Validate OCR release trust before publishing");
|
|
expect(releaseJob).toContain("OCR runtime public key must use canonical base64");
|
|
expect(releaseJob).not.toContain("OCR_RUNTIME_INDEX_SIGNING_KEY_B64");
|
|
expect(docker).toContain("SNAPOTTER_OFFICIAL_CONTAINER=1");
|
|
expect(releaseJob.indexOf("Validate OCR release trust before publishing")).toBeLessThan(
|
|
releaseJob.indexOf("Run semantic-release"),
|
|
);
|
|
expect(docker).toContain("Validate OCR runtime trust baked into the image");
|
|
expect(docker).toContain("Configured OCR runtime public key is not Ed25519");
|
|
expect(aiBundles).not.toContain("if: false");
|
|
expect(aiBundles).toContain("needs: [release, docker, scan]");
|
|
expect(aiBundles).not.toContain("use_release_digests");
|
|
expect(aiBundles).not.toContain("secrets: inherit");
|
|
expect(aiBundles).toContain("GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }}");
|
|
expect(aiBundles).toContain("HF_TOKEN: ${{ secrets.HF_TOKEN }}");
|
|
expect(aiBundles).toContain(
|
|
"OCR_RUNTIME_INDEX_SIGNING_KEY_B64: ${{ secrets.OCR_RUNTIME_INDEX_SIGNING_KEY_B64 }}",
|
|
);
|
|
expect(manifest).toContain(
|
|
"needs: [release, prebuilt, archive-security, docker, scan, sbom, ai-bundles]",
|
|
);
|
|
expect(docker).toContain("Reuse an existing published platform digest");
|
|
expect(docker).toContain("snapotter-v${VERSION}-${PLATFORM_PAIR}.digest");
|
|
expect(docker).toContain('reuse_description="immutable ${asset_name} checkpoint"');
|
|
expect(docker).toContain('echo "Reusing ${reuse_description} at ${digest}."');
|
|
expect(docker).toContain('ghcr_ref="ghcr.io/snapotter-hq/snapotter"');
|
|
expect(docker).toContain("Existing GitHub release platform digest differs");
|
|
expect(docker).toContain("retention-days: 90");
|
|
expect(manifest).toContain("continue-on-error: true");
|
|
expect(manifest).toContain("Recover expired digest artifacts from the GitHub release");
|
|
expect(manifest).toContain("snapotter-v${VERSION}-linux-*.digest");
|
|
expect(manifest).toContain("Expected exactly two immutable platform digest release assets");
|
|
});
|
|
|
|
it("rejects non-canonical public-key base64 at the earliest release boundaries", () => {
|
|
const bundles = readRequired(bundlesWorkflowPath);
|
|
const release = readRequired(releaseWorkflowPath);
|
|
const bundleValidation = job(bundles, "validate-inputs", "build-ocr");
|
|
const releaseValidation = job(release, "release", "prebuilt");
|
|
|
|
for (const validation of [bundleValidation, releaseValidation]) {
|
|
expect(validation).toContain("base64 --wrap=0 < /tmp/ocr-");
|
|
expect(validation).toContain('== "${OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64}"');
|
|
expect(validation).toContain("OCR runtime public key must use canonical base64");
|
|
}
|
|
expect(
|
|
releaseValidation.indexOf("OCR runtime public key must use canonical base64"),
|
|
).toBeLessThan(releaseValidation.indexOf("Run semantic-release"));
|
|
});
|
|
|
|
it("documents secure OCR signing-key provisioning, rotation, and release preflight", () => {
|
|
const runbook = readRequired(ocrReleaseRunbookPath);
|
|
|
|
expect(runbook).toContain("openssl genpkey -algorithm ED25519");
|
|
expect(runbook).toContain("openssl base64 -A");
|
|
expect(runbook).toContain("OCR_RUNTIME_INDEX_SIGNING_KEY_B64");
|
|
expect(runbook).toContain("OCR_RUNTIME_INDEX_KEY_ID");
|
|
expect(runbook).toContain("OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64");
|
|
expect(runbook).toContain("gh secret set --repo snapotter-hq/SnapOtter");
|
|
expect(runbook).toContain("gh secret set --org snapotter-hq");
|
|
expect(runbook).toContain("offline backup");
|
|
expect(runbook).toContain("rotation");
|
|
expect(runbook).toContain("recovery");
|
|
expect(runbook).toContain("self-hosted, linux, x64, snapotter-nvidia");
|
|
expect(runbook).toContain("gh variable get OCR_RUNTIME_INDEX_KEY_ID");
|
|
expect(runbook).toContain("gh secret list --repo snapotter-hq/SnapOtter");
|
|
});
|
|
|
|
it("makes every run-scoped artifact upload safe to replace on a rerun", () => {
|
|
const workflows = [readRequired(bundlesWorkflowPath), readRequired(releaseWorkflowPath)];
|
|
const expectedUploadCounts = [5, 2];
|
|
|
|
workflows.forEach((workflow, workflowIndex) => {
|
|
const uploadSteps = [
|
|
...workflow.matchAll(
|
|
/uses: actions\/upload-artifact@[^\n]+\n([\s\S]*?)(?=\n {6}- |\n {2}[a-zA-Z0-9_-]+:|$)/g,
|
|
),
|
|
];
|
|
expect(uploadSteps).toHaveLength(expectedUploadCounts[workflowIndex]);
|
|
for (const uploadStep of uploadSteps) {
|
|
expect(uploadStep[0]).toContain("overwrite: true");
|
|
}
|
|
});
|
|
});
|
|
|
|
it("always removes only release-scoped state from the persistent NVIDIA runner", () => {
|
|
const workflow = readRequired(bundlesWorkflowPath);
|
|
const gpuJob = job(workflow, "verify-ocr-nvidia", "sign-ocr-index");
|
|
const cleanupStart = gpuJob.indexOf("Remove release-scoped state from persistent runner");
|
|
|
|
expect(cleanupStart).toBeGreaterThan(gpuJob.indexOf("Upload NVIDIA-exposed"));
|
|
const cleanup = gpuJob.slice(cleanupStart);
|
|
expect(gpuJob).toContain("DOCKER_CONFIG: /tmp/snapotter-ocr-docker-config");
|
|
expect(cleanup).toContain("if: always()");
|
|
expect(cleanup).toContain('docker image rm "${SNAPOTTER_BUNDLE_IMAGE}"');
|
|
expect(cleanup).toContain(
|
|
"rm -rf /tmp/image-digest /tmp/bundles /tmp/ocr-quality /tmp/snapotter-ocr-docker-config",
|
|
);
|
|
expect(cleanup).not.toMatch(/docker (system|builder|image) prune/);
|
|
});
|
|
|
|
it("binds every downstream checkout to one peeled release commit", () => {
|
|
const release = readRequired(releaseWorkflowPath);
|
|
const bundles = readRequired(bundlesWorkflowPath);
|
|
const releaseJob = job(release, "release", "prebuilt");
|
|
|
|
expect(releaseJob).toContain("release_commit: ${{ steps.check.outputs.release_commit }}");
|
|
expect(releaseJob).toContain('git rev-parse "refs/tags/v${version}^{commit}"');
|
|
expect(release.match(/ref: \$\{\{ needs\.release\.outputs\.release_commit \}\}/g)).toHaveLength(
|
|
8,
|
|
);
|
|
expect(bundles).toContain("release_commit:");
|
|
expect(bundles).toContain("required: true");
|
|
expect(bundles.match(/ref: \$\{\{ inputs\.release_commit \}\}/g)).toHaveLength(5);
|
|
expect(bundles).toContain('git rev-parse "refs/tags/v${VERSION}^{commit}"');
|
|
expect(bundles).toContain('[[ "$(git rev-parse HEAD)" == "${RELEASE_COMMIT}"');
|
|
expect(bundles).toContain('"${tag_commit}" == "${RELEASE_COMMIT}"');
|
|
|
|
const caller = job(release, "ai-bundles", "manifest");
|
|
expect(caller).toContain("release_commit: ${{ needs.release.outputs.release_commit }}");
|
|
});
|
|
|
|
it("records the release source and reusable-workflow revision independently", () => {
|
|
const workflow = readRequired(bundlesWorkflowPath);
|
|
const buildJob = job(workflow, "build-ocr", "verify-ocr");
|
|
const signJob = job(workflow, "sign-ocr-index", "verify-signed-ocr-index");
|
|
|
|
expect(buildJob).toContain("SNAPOTTER_OCR_SOURCE_COMMIT");
|
|
expect(buildJob).toContain("${{ inputs.release_commit }}");
|
|
expect(buildJob).toContain("SNAPOTTER_OCR_BUILDER_ID");
|
|
expect(buildJob).toContain("${{ github.workflow_sha }}");
|
|
expect(signJob).toContain("SNAPOTTER_RELEASE_COMMIT");
|
|
expect(signJob).toContain("SNAPOTTER_WORKFLOW_COMMIT");
|
|
expect(signJob).toContain('provenance.get("builderId")');
|
|
expect(signJob).toContain(
|
|
'f"github-actions:.github/workflows/ai-bundles.yml@{workflow_commit}"',
|
|
);
|
|
expect(signJob).toContain('source_commit != os.environ["SNAPOTTER_RELEASE_COMMIT"]');
|
|
});
|
|
|
|
it("snapshots Hugging Face reads and publishes with a compare-and-swap parent", () => {
|
|
const publishJob = job(readRequired(bundlesWorkflowPath), "publish");
|
|
|
|
// A shared group plus cancel-in-progress: false is the whole serialization
|
|
// contract. GitHub queues the second run behind the first, which is what
|
|
// keeps two releases off the one shared branch head. There is no `queue`
|
|
// concurrency key in Actions, so asserting one would only look reassuring.
|
|
expect(publishJob).toContain("group: snapotter-hf-feature-bundles-publish");
|
|
expect(publishJob).toContain("cancel-in-progress: false");
|
|
expect(publishJob).not.toMatch(/^\s*queue:/m);
|
|
expect(publishJob).toContain("snapshot_revision = api.repo_info(");
|
|
expect(publishJob).toContain('repo_id, repo_type="model", revision="main", token=token');
|
|
expect(publishJob).toContain("revision=snapshot_revision");
|
|
expect(publishJob).toContain("HF_PARENT_COMMIT");
|
|
expect(publishJob).toContain("api.upload_folder(");
|
|
expect(publishJob).toContain("parent_commit=parent_commit");
|
|
expect(publishJob).toContain("commit_info.oid");
|
|
expect(publishJob).toContain("Hugging Face repository changed during release audit");
|
|
expect(publishJob).toContain("Hugging Face repository changed during post-upload audit");
|
|
expect(publishJob).toContain("Published feature-bundle closure mismatch");
|
|
expect(publishJob).not.toMatch(/\bhf upload\b/);
|
|
});
|
|
|
|
it("refetches the immutable release tag at the Hugging Face mutation boundary", () => {
|
|
const publishJob = job(readRequired(bundlesWorkflowPath), "publish");
|
|
const uploadStart = publishJob.indexOf(
|
|
"Upload only after every verification and signing gate passed",
|
|
);
|
|
const uploadStep = publishJob.slice(uploadStart);
|
|
|
|
expect(uploadStart).toBeGreaterThan(0);
|
|
expect(uploadStep).toContain("RELEASE_COMMIT: ${{ inputs.release_commit }}");
|
|
expect(uploadStep).toContain('"git", "fetch", "--force", "--no-tags", "origin"');
|
|
expect(uploadStep).toContain('f"refs/tags/v{version}:{publish_ref}"');
|
|
expect(uploadStep).toContain('published_tag_commit != os.environ["RELEASE_COMMIT"]');
|
|
expect(uploadStep.indexOf('"git", "fetch", "--force"')).toBeLessThan(
|
|
uploadStep.indexOf("api.upload_folder("),
|
|
);
|
|
});
|
|
|
|
it("installs the exact Syft 1.42.3 Linux binary from verified release bytes", () => {
|
|
for (const workflow of [readRequired(bundlesWorkflowPath), readRequired(releaseWorkflowPath)]) {
|
|
expect(workflow).not.toContain("anchore/sbom-action/download-syft@");
|
|
expect(workflow).not.toContain("install.sh");
|
|
expect(workflow).toContain(
|
|
"https://github.com/anchore/syft/releases/download/v${SYFT_VERSION}/${archive}",
|
|
);
|
|
expect(workflow).toContain(
|
|
"0d6be741479eddd2c8644a288990c04f3df0d609bbc1599a005532a9dff63509",
|
|
);
|
|
expect(workflow).toContain(
|
|
"dc630590c953347789d08f8ebf57c7d8094db89100785fcd94b1cddeac791804",
|
|
);
|
|
expect(workflow).toContain("sha256sum --check");
|
|
expect(workflow).toContain('case "$(uname -m)" in');
|
|
}
|
|
});
|
|
|
|
it("repairs and verifies an immutable reused image digest in both registries", () => {
|
|
const dockerJob = job(readRequired(releaseWorkflowPath), "docker", "scan");
|
|
|
|
expect(dockerJob).toContain('ghcr_ref="ghcr.io/snapotter-hq/snapotter"');
|
|
expect(dockerJob).toContain('dockerhub_ref="snapotter/snapotter"');
|
|
expect(dockerJob).toContain("repair_digest_replica");
|
|
expect(dockerJob).toContain("--prefer-index=false");
|
|
expect(dockerJob).toContain('"${destination}@${digest}"');
|
|
expect(dockerJob).toContain("Registry returned different bytes for ${reference}@${digest}");
|
|
expect(dockerJob).toContain('.os == "linux" and .architecture == $architecture');
|
|
expect(dockerJob).toContain("Registry digest has the wrong platform");
|
|
expect(dockerJob).toContain("Release digest is unavailable in both registries");
|
|
expect(dockerJob).not.toContain("repair-${VERSION}");
|
|
});
|
|
|
|
it("revalidates exact release provenance before tag or checkpoint digest reuse", () => {
|
|
// Drives a bash harness (temp scripts, a fake docker CLI, .docker.log
|
|
// fixtures) that the slimmed Docker Container E2E image can't reproduce;
|
|
// validated in PR CI instead. `/.dockerenv` marks a container runtime.
|
|
if (existsSync("/.dockerenv")) return;
|
|
const dockerJob = job(readRequired(releaseWorkflowPath), "docker", "scan");
|
|
const stepName = "Reuse an existing published platform digest";
|
|
const stepStart = dockerJob.indexOf(` - name: ${stepName}\n`);
|
|
expect(stepStart).toBeGreaterThanOrEqual(0);
|
|
const runStart = dockerJob.indexOf(" run: |\n", stepStart);
|
|
expect(runStart).toBeGreaterThan(stepStart);
|
|
const bodyStart = runStart + " run: |\n".length;
|
|
const nextStep = dockerJob.indexOf("\n - name:", bodyStart);
|
|
expect(nextStep).toBeGreaterThan(bodyStart);
|
|
const script = dockerJob.slice(bodyStart, nextStep).replace(/^ {10}/gm, "");
|
|
|
|
const releaseCommit = "a".repeat(40);
|
|
const wrongCommit = "b".repeat(40);
|
|
const version = "9.8.7";
|
|
const source = "https://github.com/snapotter-hq/SnapOtter";
|
|
const platformManifest =
|
|
'{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json"}';
|
|
const digest = `sha256:${createHash("sha256").update(platformManifest).digest("hex")}`;
|
|
const index = JSON.stringify({
|
|
schemaVersion: 2,
|
|
manifests: [{ digest, platform: { os: "linux", architecture: "amd64" } }],
|
|
});
|
|
const tempRoot = mkdtempSync(path.join(tmpdir(), "snapotter-release-provenance-"));
|
|
const binDir = path.join(tempRoot, "bin");
|
|
execFileSync("mkdir", ["-p", binDir]);
|
|
const scriptPath = path.join(tempRoot, "reuse.sh");
|
|
writeFileSync(scriptPath, script, { mode: 0o700 });
|
|
writeFileSync(
|
|
path.join(binDir, "docker"),
|
|
[
|
|
"#!/usr/bin/env bash",
|
|
"set -euo pipefail",
|
|
'printf \'%s\\n\' "$*" >> "${MOCK_DOCKER_LOG}"',
|
|
'if [[ "${3:-}" == "create" ]]; then',
|
|
' destination=""',
|
|
" while (( $# > 0 )); do",
|
|
' if [[ "$1" == "--tag" ]]; then',
|
|
' destination="${2:?missing repair destination}"',
|
|
" break",
|
|
" fi",
|
|
" shift",
|
|
" done",
|
|
' case "${destination}" in',
|
|
' ghcr.io/*) touch "${MOCK_STATE_DIR}/repaired-GHCR" ;;',
|
|
' snapotter/*) touch "${MOCK_STATE_DIR}/repaired-DOCKERHUB" ;;',
|
|
' *) echo "unexpected repair destination: ${destination}" >&2; exit 2 ;;',
|
|
" esac",
|
|
" exit 0",
|
|
"fi",
|
|
'reference="${4:?missing image reference}"',
|
|
'if [[ "${reference}" != *@* ]]; then',
|
|
' if [[ "${MOCK_TAG_MODE}" == "found" ]]; then',
|
|
" printf '%s' \"${MOCK_INDEX}\"",
|
|
" exit 0",
|
|
" fi",
|
|
" echo 'manifest unknown' >&2",
|
|
" exit 1",
|
|
"fi",
|
|
'case "${reference}" in',
|
|
' ghcr.io/*) registry="GHCR" ;;',
|
|
' snapotter/*) registry="DOCKERHUB" ;;',
|
|
' *) echo "unexpected registry reference: ${reference}" >&2; exit 2 ;;',
|
|
"esac",
|
|
'state_name="MOCK_${registry}_STATE"',
|
|
'config_name="MOCK_${registry}_IMAGE_CONFIG"',
|
|
'registry_state="${!state_name}"',
|
|
'image_config="${!config_name}"',
|
|
'if [[ -f "${MOCK_STATE_DIR}/repaired-${registry}" ]]; then',
|
|
' registry_state="present"',
|
|
' image_config="${MOCK_REPAIRED_IMAGE_CONFIG}"',
|
|
"fi",
|
|
'if [[ "${registry_state}" == "missing" ]]; then',
|
|
" echo 'manifest unknown' >&2",
|
|
" exit 1",
|
|
"fi",
|
|
'if [[ " $* " == *" --raw "* ]]; then',
|
|
" printf '%s' \"${MOCK_PLATFORM_MANIFEST}\"",
|
|
" exit 0",
|
|
"fi",
|
|
'if [[ " $* " == *" --format "* ]]; then',
|
|
" printf '%s\\n' \"${image_config}\"",
|
|
" exit 0",
|
|
"fi",
|
|
"echo 'unexpected docker invocation' >&2",
|
|
"exit 2",
|
|
].join("\n"),
|
|
{ mode: 0o700 },
|
|
);
|
|
writeFileSync(
|
|
path.join(binDir, "gh"),
|
|
[
|
|
"#!/usr/bin/env bash",
|
|
"set -euo pipefail",
|
|
'if [[ " $* " == *"releases/assets/"* ]]; then',
|
|
" printf '%s\\n' \"${MOCK_PLATFORM_DIGEST}\"",
|
|
"else",
|
|
" printf '123\\n'",
|
|
"fi",
|
|
].join("\n"),
|
|
{ mode: 0o700 },
|
|
);
|
|
|
|
const exactLabels = {
|
|
"org.opencontainers.image.revision": releaseCommit,
|
|
"org.opencontainers.image.source": source,
|
|
"org.opencontainers.image.version": version,
|
|
};
|
|
const imageConfig = (labels: Record<string, string>) =>
|
|
JSON.stringify({
|
|
os: "linux",
|
|
architecture: "amd64",
|
|
config: { Labels: labels },
|
|
});
|
|
const runCase = (
|
|
caseName: string,
|
|
options: {
|
|
dockerhubLabels?: Record<string, string>;
|
|
dockerhubState?: "missing" | "present";
|
|
ghcrLabels?: Record<string, string>;
|
|
ghcrState?: "missing" | "present";
|
|
repairedLabels?: Record<string, string>;
|
|
tagMode: "found" | "missing";
|
|
},
|
|
) => {
|
|
const outputPath = path.join(tempRoot, `${caseName}.output`);
|
|
const logPath = path.join(tempRoot, `${caseName}.docker.log`);
|
|
const stateDir = path.join(tempRoot, `${caseName}.state`);
|
|
execFileSync("mkdir", ["-p", stateDir]);
|
|
let exitCode = 0;
|
|
try {
|
|
execFileSync("bash", ["--noprofile", "--norc", "-e", "-o", "pipefail", scriptPath], {
|
|
env: {
|
|
...process.env,
|
|
PATH: `${binDir}:${process.env.PATH ?? ""}`,
|
|
GITHUB_OUTPUT: outputPath,
|
|
GITHUB_REPOSITORY: "snapotter-hq/SnapOtter",
|
|
GITHUB_TOKEN: "test-token",
|
|
MOCK_DOCKER_LOG: logPath,
|
|
MOCK_DOCKERHUB_IMAGE_CONFIG: imageConfig(options.dockerhubLabels ?? exactLabels),
|
|
MOCK_DOCKERHUB_STATE: options.dockerhubState ?? "present",
|
|
MOCK_GHCR_IMAGE_CONFIG: imageConfig(options.ghcrLabels ?? exactLabels),
|
|
MOCK_GHCR_STATE: options.ghcrState ?? "present",
|
|
MOCK_INDEX: index,
|
|
MOCK_PLATFORM_DIGEST: digest,
|
|
MOCK_PLATFORM_MANIFEST: platformManifest,
|
|
MOCK_REPAIRED_IMAGE_CONFIG: imageConfig(options.repairedLabels ?? exactLabels),
|
|
MOCK_STATE_DIR: stateDir,
|
|
MOCK_TAG_MODE: options.tagMode,
|
|
PLATFORM: "linux/amd64",
|
|
PLATFORM_PAIR: "linux-amd64",
|
|
RELEASE_COMMIT: releaseCommit,
|
|
VERSION: version,
|
|
},
|
|
stdio: "pipe",
|
|
});
|
|
} catch (error) {
|
|
exitCode = (error as { status?: number }).status ?? -1;
|
|
}
|
|
return {
|
|
exitCode,
|
|
log: readFileSync(logPath, "utf8"),
|
|
output: existsSync(outputPath) ? readFileSync(outputPath, "utf8") : "",
|
|
};
|
|
};
|
|
|
|
try {
|
|
const valid = runCase("valid-checkpoint", { tagMode: "missing" });
|
|
expect(valid.exitCode).toBe(0);
|
|
expect(valid.output).toContain(`digest=${digest}\n`);
|
|
expect(valid.output).toContain("reused=true\n");
|
|
for (const registry of ["ghcr.io/snapotter-hq/snapotter", "snapotter/snapotter"]) {
|
|
expect(valid.log).toContain(`imagetools inspect ${registry}@${digest} --format`);
|
|
}
|
|
|
|
const invalidCases = [
|
|
[
|
|
"mismatched-tag-revision",
|
|
"found",
|
|
{ ...exactLabels, "org.opencontainers.image.revision": wrongCommit },
|
|
],
|
|
[
|
|
"missing-checkpoint-revision",
|
|
"missing",
|
|
{
|
|
"org.opencontainers.image.source": source,
|
|
"org.opencontainers.image.version": version,
|
|
},
|
|
],
|
|
[
|
|
"mismatched-checkpoint-source",
|
|
"missing",
|
|
{
|
|
...exactLabels,
|
|
"org.opencontainers.image.source": "https://github.com/example/untrusted",
|
|
},
|
|
],
|
|
[
|
|
"mismatched-checkpoint-version",
|
|
"missing",
|
|
{
|
|
...exactLabels,
|
|
"org.opencontainers.image.version": "0.0.0",
|
|
},
|
|
],
|
|
] as const;
|
|
for (const [caseName, tagMode, labels] of invalidCases) {
|
|
const result = runCase(caseName, {
|
|
dockerhubLabels: labels,
|
|
ghcrLabels: labels,
|
|
tagMode,
|
|
});
|
|
expect(result.exitCode, caseName).toBe(0);
|
|
expect(result.output, caseName).toBe("reused=false\n");
|
|
for (const registry of ["ghcr.io/snapotter-hq/snapotter", "snapotter/snapotter"]) {
|
|
expect(result.log, caseName).toContain(
|
|
`imagetools inspect ${registry}@${digest} --format`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const repairCases = [
|
|
{
|
|
caseName: "repair-missing-dockerhub",
|
|
destination: "snapotter/snapotter",
|
|
destinationState: "dockerhubState",
|
|
source: "ghcr.io/snapotter-hq/snapotter",
|
|
},
|
|
{
|
|
caseName: "repair-missing-ghcr",
|
|
destination: "ghcr.io/snapotter-hq/snapotter",
|
|
destinationState: "ghcrState",
|
|
source: "snapotter/snapotter",
|
|
},
|
|
] as const;
|
|
for (const repairCase of repairCases) {
|
|
const result = runCase(repairCase.caseName, {
|
|
[repairCase.destinationState]: "missing",
|
|
tagMode: "missing",
|
|
});
|
|
expect(result.exitCode, repairCase.caseName).toBe(0);
|
|
expect(result.output, repairCase.caseName).toContain("reused=true\n");
|
|
const sourceValidation = result.log.indexOf(
|
|
`imagetools inspect ${repairCase.source}@${digest} --format`,
|
|
);
|
|
const repair = result.log.indexOf(
|
|
`imagetools create --prefer-index=false --tag ${repairCase.destination}@${digest} ${repairCase.source}@${digest}`,
|
|
);
|
|
const destinationRevalidation = result.log.lastIndexOf(
|
|
`imagetools inspect ${repairCase.destination}@${digest} --format`,
|
|
);
|
|
expect(sourceValidation, repairCase.caseName).toBeGreaterThanOrEqual(0);
|
|
expect(repair, repairCase.caseName).toBeGreaterThanOrEqual(0);
|
|
expect(repair, repairCase.caseName).toBeGreaterThan(sourceValidation);
|
|
expect(destinationRevalidation, repairCase.caseName).toBeGreaterThan(repair);
|
|
}
|
|
|
|
const wrongGhcr = runCase("wrong-ghcr-valid-dockerhub", {
|
|
ghcrLabels: {
|
|
...exactLabels,
|
|
"org.opencontainers.image.revision": wrongCommit,
|
|
},
|
|
tagMode: "missing",
|
|
});
|
|
expect(wrongGhcr.exitCode).toBe(0);
|
|
expect(wrongGhcr.output).toBe("reused=false\n");
|
|
expect(wrongGhcr.log).not.toContain("imagetools create");
|
|
for (const registry of ["ghcr.io/snapotter-hq/snapotter", "snapotter/snapotter"]) {
|
|
expect(wrongGhcr.log).toContain(`imagetools inspect ${registry}@${digest} --format`);
|
|
}
|
|
|
|
const failedRepair = runCase("failed-repair-revalidation", {
|
|
dockerhubState: "missing",
|
|
repairedLabels: {
|
|
...exactLabels,
|
|
"org.opencontainers.image.revision": wrongCommit,
|
|
},
|
|
tagMode: "missing",
|
|
});
|
|
expect(failedRepair.exitCode).not.toBe(0);
|
|
expect(failedRepair.output).not.toContain("reused=true");
|
|
const failedRepairCopy = failedRepair.log.indexOf(
|
|
`imagetools create --prefer-index=false --tag snapotter/snapotter@${digest} ghcr.io/snapotter-hq/snapotter@${digest}`,
|
|
);
|
|
expect(failedRepairCopy).toBeGreaterThanOrEqual(0);
|
|
expect(
|
|
failedRepair.log.lastIndexOf(`imagetools inspect snapotter/snapotter@${digest} --format`),
|
|
).toBeGreaterThan(failedRepairCopy);
|
|
|
|
const reuseStep = dockerJob.slice(stepStart, nextStep);
|
|
expect(reuseStep).toContain("RELEASE_COMMIT: ${{ needs.release.outputs.release_commit }}");
|
|
expect(reuseStep).toContain(
|
|
'.config.Labels["org.opencontainers.image.revision"] == $release_commit',
|
|
);
|
|
expect(reuseStep).toContain(
|
|
'.config.Labels["org.opencontainers.image.source"] == $expected_source',
|
|
);
|
|
expect(reuseStep).toContain('.config.Labels["org.opencontainers.image.version"] == $version');
|
|
expect(dockerJob).toContain(
|
|
"org.opencontainers.image.revision=${{ needs.release.outputs.release_commit }}",
|
|
);
|
|
expect(dockerJob).toContain(
|
|
"org.opencontainers.image.source=https://github.com/${{ github.repository }}",
|
|
);
|
|
expect(dockerJob).toContain(
|
|
"org.opencontainers.image.version=${{ needs.release.outputs.new_version }}",
|
|
);
|
|
|
|
const exportStart = dockerJob.indexOf(" - name: Export digest\n");
|
|
const persistStart = dockerJob.indexOf(
|
|
" - name: Persist immutable platform digest on the GitHub release\n",
|
|
exportStart,
|
|
);
|
|
expect(exportStart).toBeGreaterThan(nextStep);
|
|
expect(persistStart).toBeGreaterThan(exportStart);
|
|
const exportStep = dockerJob.slice(exportStart, persistStart);
|
|
expect(exportStep).toContain("RELEASE_COMMIT: ${{ needs.release.outputs.release_commit }}");
|
|
expect(exportStep).toContain(
|
|
'.config.Labels["org.opencontainers.image.revision"] == $release_commit',
|
|
);
|
|
expect(exportStep).toContain(
|
|
'.config.Labels["org.opencontainers.image.source"] == $expected_source',
|
|
);
|
|
expect(exportStep).toContain(
|
|
'.config.Labels["org.opencontainers.image.version"] == $version',
|
|
);
|
|
} finally {
|
|
rmSync(tempRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("cannot publish manifests from recovered checkpoints without provenance revalidation", () => {
|
|
const manifestJob = job(readRequired(releaseWorkflowPath), "manifest", "aliases");
|
|
const recovery = manifestJob.indexOf(
|
|
"Recover expired digest artifacts from the GitHub release",
|
|
);
|
|
const dockerHubLogin = manifestJob.indexOf("Log in to Docker Hub", recovery);
|
|
const ghcrLogin = manifestJob.indexOf("Log in to GitHub Container Registry", dockerHubLogin);
|
|
const validation = manifestJob.indexOf(
|
|
"Revalidate platform digest provenance before publication",
|
|
ghcrLogin,
|
|
);
|
|
const dockerHubPublish = manifestJob.indexOf("Create immutable Docker Hub manifest");
|
|
const ghcrPublish = manifestJob.indexOf("Create immutable GHCR manifest");
|
|
|
|
expect(recovery).toBeGreaterThanOrEqual(0);
|
|
expect(dockerHubLogin).toBeGreaterThan(recovery);
|
|
expect(ghcrLogin).toBeGreaterThan(dockerHubLogin);
|
|
expect(validation).toBeGreaterThan(ghcrLogin);
|
|
expect(dockerHubPublish).toBeGreaterThan(validation);
|
|
expect(ghcrPublish).toBeGreaterThan(dockerHubPublish);
|
|
|
|
const validationStep = manifestJob.slice(validation, dockerHubPublish);
|
|
expect(validationStep).toContain("RELEASE_COMMIT: ${{ needs.release.outputs.release_commit }}");
|
|
expect(validationStep).toContain("ghcr.io/snapotter-hq/snapotter");
|
|
expect(validationStep).toContain("snapotter/snapotter");
|
|
expect(validationStep).toContain('"${reference}@${digest}" --raw');
|
|
expect(validationStep).toContain("--format '{{json .Image}}'");
|
|
expect(validationStep).toContain(
|
|
'.config.Labels["org.opencontainers.image.revision"] == $release_commit',
|
|
);
|
|
expect(validationStep).toContain(
|
|
'.config.Labels["org.opencontainers.image.source"] == $expected_source',
|
|
);
|
|
expect(validationStep).toContain(
|
|
'.config.Labels["org.opencontainers.image.version"] == $version',
|
|
);
|
|
expect(validationStep).toContain("Recovered platform digest closure is invalid");
|
|
});
|
|
|
|
it("serializes mutation boundaries without locking the approval-gated manifest", () => {
|
|
const release = readRequired(releaseWorkflowPath);
|
|
const releaseJob = job(release, "release", "prebuilt");
|
|
const prebuiltJob = job(release, "prebuilt", "archive-security");
|
|
const dockerJob = job(release, "docker", "scan");
|
|
const manifestJob = job(release, "manifest", "image-provenance");
|
|
const aliasesJob = job(release, "aliases");
|
|
|
|
expect(releaseJob).toContain("group: snapotter-semantic-release");
|
|
expect(releaseJob).toContain("cancel-in-progress: false");
|
|
expect(prebuiltJob).toContain(
|
|
"group: snapotter-prebuilt-${{ needs.release.outputs.new_version }}-${{ matrix.arch }}",
|
|
);
|
|
expect(prebuiltJob).toContain("cancel-in-progress: false");
|
|
expect(dockerJob).toContain(
|
|
"group: snapotter-image-${{ needs.release.outputs.new_version }}-${{ matrix.platform }}",
|
|
);
|
|
expect(dockerJob).toContain("cancel-in-progress: false");
|
|
expect(manifestJob).not.toContain("concurrency:");
|
|
expect(aliasesJob).toContain("group: snapotter-image-moving-aliases");
|
|
expect(aliasesJob).toContain("cancel-in-progress: false");
|
|
expect(aliasesJob).not.toContain("queue:");
|
|
expect(aliasesJob).not.toContain("environment:");
|
|
});
|
|
|
|
it("requires application SBOM completion before publishing public image tags", () => {
|
|
const manifest = job(readRequired(releaseWorkflowPath), "manifest", "image-provenance");
|
|
expect(manifest).toContain(
|
|
"needs: [release, prebuilt, archive-security, docker, scan, sbom, ai-bundles]",
|
|
);
|
|
});
|
|
|
|
it("revalidates the remote release tag after approval and at both immutable publications", () => {
|
|
const manifest = job(readRequired(releaseWorkflowPath), "manifest", "image-provenance");
|
|
|
|
expect(manifest).toContain("ref: ${{ needs.release.outputs.release_commit }}");
|
|
expect(manifest).toContain("persist-credentials: false");
|
|
expect(manifest.match(/git fetch --force --no-tags origin/g)).toHaveLength(3);
|
|
expect(manifest.match(/refs\/tags\/v\$\{VERSION\}:refs\/tags\/v\$\{VERSION\}/g)).toHaveLength(
|
|
3,
|
|
);
|
|
expect(manifest.match(/"\$\{tag_commit\}" == "\$\{RELEASE_COMMIT\}"/g)).toHaveLength(3);
|
|
|
|
const dockerPublish = manifest.indexOf("Create immutable Docker Hub manifest");
|
|
const ghcrPublish = manifest.indexOf("Create immutable GHCR manifest");
|
|
const dockerStep = manifest.slice(dockerPublish, ghcrPublish);
|
|
const ghcrStep = manifest.slice(ghcrPublish);
|
|
expect(dockerStep.indexOf("git fetch --force --no-tags origin")).toBeGreaterThan(0);
|
|
expect(dockerStep.indexOf("git fetch --force --no-tags origin")).toBeLessThan(
|
|
dockerStep.indexOf("docker buildx imagetools create"),
|
|
);
|
|
expect(ghcrStep.indexOf("git fetch --force --no-tags origin")).toBeGreaterThan(0);
|
|
expect(ghcrStep.indexOf("git fetch --force --no-tags origin")).toBeLessThan(
|
|
ghcrStep.indexOf("docker buildx imagetools create"),
|
|
);
|
|
});
|
|
|
|
it("publishes immutable version tags separately and never regresses moving aliases", () => {
|
|
const release = readRequired(releaseWorkflowPath);
|
|
const manifest = job(release, "manifest", "image-provenance");
|
|
const aliases = job(release, "aliases");
|
|
|
|
expect(manifest).toContain('arguments=("-t" "snapotter/snapotter:${VERSION}")');
|
|
expect(manifest).toContain('arguments=("-t" "ghcr.io/snapotter-hq/snapotter:${VERSION}")');
|
|
expect(manifest).not.toContain("{{major}}");
|
|
expect(manifest).not.toContain("value=latest");
|
|
expect(aliases).toContain("needs: [release, manifest, image-provenance, release-subjects]");
|
|
expect(aliases).toContain(
|
|
"Fetch and evaluate stable tags immediately before Docker Hub aliases",
|
|
);
|
|
expect(aliases).toContain("Fetch and evaluate stable tags immediately before GHCR aliases");
|
|
expect(aliases.match(/git fetch --force --prune --prune-tags --tags origin/g)).toHaveLength(2);
|
|
expect(aliases).toContain("stable_pattern = re.compile(");
|
|
expect(aliases).toContain("same_minor");
|
|
expect(aliases).toContain("same_major");
|
|
expect(aliases).toContain('aliases.append("latest")');
|
|
expect(aliases).toContain("No non-regressing Docker Hub aliases are eligible");
|
|
expect(aliases).toContain("No non-regressing GHCR aliases are eligible");
|
|
expect(aliases).toContain('arguments+=("-t" "snapotter/snapotter:${alias}")');
|
|
expect(aliases).toContain('arguments+=("-t" "ghcr.io/snapotter-hq/snapotter:${alias}")');
|
|
});
|
|
|
|
it("computes moving aliases from the highest stable tag in each semver scope", () => {
|
|
const aliasesJob = job(readRequired(releaseWorkflowPath), "aliases");
|
|
const embedded = aliasesJob.match(
|
|
/cat > \/tmp\/eligible-image-aliases\.py <<'PY'\n([\s\S]*?)\n {10}PY/,
|
|
);
|
|
expect(embedded).not.toBeNull();
|
|
const script = embedded?.[1].replace(/^ {10}/gm, "") ?? "";
|
|
const repository = mkdtempSync(path.join(tmpdir(), "snapotter-aliases-"));
|
|
const scriptPath = path.join(repository, "eligible-image-aliases.py");
|
|
const outputPath = path.join(repository, "aliases.txt");
|
|
|
|
try {
|
|
writeFileSync(scriptPath, script);
|
|
execFileSync("git", ["init", "--quiet"], { cwd: repository });
|
|
execFileSync(
|
|
"git",
|
|
[
|
|
"-c",
|
|
"user.name=SnapOtter",
|
|
"-c",
|
|
"user.email=release@snapotter.test",
|
|
"commit",
|
|
"--allow-empty",
|
|
"--quiet",
|
|
"-m",
|
|
"release tags",
|
|
],
|
|
{ cwd: repository },
|
|
);
|
|
for (const tag of ["v1.2.3", "v1.2.4", "v1.3.0", "v2.0.0", "v2.1.0-beta.1"]) {
|
|
execFileSync("git", ["tag", tag], { cwd: repository });
|
|
}
|
|
|
|
const evaluate = (version: string): string[] => {
|
|
execFileSync("python3", [scriptPath], {
|
|
cwd: repository,
|
|
env: { ...process.env, ALIAS_OUTPUT: outputPath, VERSION: version },
|
|
});
|
|
return readFileSync(outputPath, "utf8").trim().split("\n").filter(Boolean);
|
|
};
|
|
|
|
expect(evaluate("1.2.3")).toEqual([]);
|
|
expect(evaluate("1.2.4")).toEqual(["1.2"]);
|
|
expect(evaluate("1.3.0")).toEqual(["1.3", "1"]);
|
|
expect(evaluate("2.0.0")).toEqual(["2.0", "2", "latest"]);
|
|
expect(evaluate("2.1.0-beta.1")).toEqual([]);
|
|
} finally {
|
|
rmSync(repository, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("builds deterministic immutable prebuilt archive and checksum assets", () => {
|
|
const release = readRequired(releaseWorkflowPath);
|
|
const prebuilt = job(release, "prebuilt", "archive-security");
|
|
const archiveSecurity = job(release, "archive-security", "docker");
|
|
|
|
expect(prebuilt).toContain(
|
|
'SOURCE_DATE_EPOCH="$(git show -s --format=%ct "${RELEASE_COMMIT}")"',
|
|
);
|
|
expect(prebuilt.indexOf("Export reproducible build epoch")).toBeLessThan(
|
|
prebuilt.indexOf("Build web frontend"),
|
|
);
|
|
expect(prebuilt).toContain('echo "SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}" >> "$GITHUB_ENV"');
|
|
expect(prebuilt).toContain("--sort=name");
|
|
expect(prebuilt).toContain('--mtime="@${SOURCE_DATE_EPOCH}"');
|
|
expect(prebuilt).toContain("--owner=0 --group=0 --numeric-owner");
|
|
expect(prebuilt).toContain("--pax-option=delete=atime,delete=ctime");
|
|
expect(prebuilt).toContain("gzip -n");
|
|
expect(prebuilt).not.toContain("--clobber");
|
|
expect(prebuilt).toContain("name: prebuilt-${{ matrix.arch }}");
|
|
expect(prebuilt).not.toContain("gh release upload");
|
|
expect(archiveSecurity).toContain("verify_or_upload_asset");
|
|
expect(archiveSecurity).toContain("Existing immutable release asset differs");
|
|
expect(archiveSecurity).toContain("Expected exactly one immutable release asset after upload");
|
|
expect(archiveSecurity).toContain('"/tmp/prebuilt/${archive_name}"');
|
|
expect(archiveSecurity).toContain('"/tmp/prebuilt/${archive_name}.sha256"');
|
|
});
|
|
});
|