import { readFileSync } from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { parse } from "yaml"; const workflowText = readFileSync( path.resolve(import.meta.dirname, "../../../.github/workflows/publish-server-image.yml"), "utf8", ); interface WorkflowJob { if?: string; needs?: string; "runs-on"?: string; strategy?: { matrix?: { arch?: string[]; include?: Array> } }; } const workflow = parse(workflowText) as { jobs: { validate: WorkflowJob; build: WorkflowJob; publish: WorkflowJob }; }; describe("server image publish workflow", () => { it("builds amd64 natively on the Linux runner and keeps arm64 paused, not emulated", () => { expect(workflowText).not.toContain("setup-qemu-action"); expect(workflowText).toContain("arm64 is paused"); const build = workflow.jobs.build; expect(build["runs-on"]).toContain("matrix.runner"); expect(build.strategy?.matrix?.arch).toEqual(["amd64"]); const runners = Object.fromEntries( (build.strategy?.matrix?.include ?? []) .filter((entry) => entry.arch !== undefined) .map((entry) => [entry.arch, entry.runner]), ); expect(runners).toEqual({ amd64: ["self-hosted", "evoo-ci", "Linux"] }); }); it("publishes one verified manifest per image after the build", () => { const publish = workflow.jobs.publish; expect(publish.needs).toEqual(["changes", "build"]); expect(workflowText).toContain("push-by-digest=true"); expect(workflowText).toContain("docker buildx imagetools create"); expect(workflowText).toContain(`if [[ "\${#sources[@]}" -ne 1 ]]`); expect(workflowText).toContain("for want in linux/amd64; do"); expect(workflowText).toContain("actions/attest-build-provenance@"); expect(workflowText).not.toContain("github.repository_owner_type"); expect(workflowText).toContain(`gh api "repos/\${GITHUB_REPOSITORY}"`); // `gh api --jq` already prints raw strings; `--jq -r` takes `-r` as the filter and fails. expect(workflowText).not.toContain("--jq -r"); expect(workflowText).toContain(".owner.type"); expect(workflowText).toContain("if: steps.attestation.outputs.enabled == 'true'"); expect(workflowText).toContain('[[ "$owner_type" == "User" && "$private" == "true" ]]'); }); it("stores digest markers under RUNNER_TEMP so self-hosted runners do not leak stale files", () => { expect(workflowText).not.toContain("/tmp/digests"); expect(workflowText).toContain("RUNNER_TEMP"); }); it("passes build digests to publish through GHCR handoff tags, not Actions artifact storage", () => { expect(workflowText).not.toContain("actions/upload-artifact@"); expect(workflowText).not.toContain("actions/download-artifact@"); expect(workflowText).toContain("Tag digest for publish job handoff"); expect(workflowText).toContain("ci-${GITHUB_RUN_ID}-"); }); it("isolates Docker registry auth per job and deletes handoff tags without imagetools rm", () => { expect(workflowText).toContain("DOCKER_CONFIG: ${{ runner.temp }}/docker-config"); expect(workflowText).toContain("Prepare isolated Docker config directory"); expect(workflowText).not.toContain("imagetools rm"); expect(workflowText).toContain("Delete CI handoff tags"); expect(workflowText).toContain("/v2/${repository}/manifests/${tag}"); }); it("names every image under the lowercase repository namespace", () => { expect(workflowText).not.toContain(`ghcr.io/\${{ github.repository }}`); const references = workflowText.match(/ghcr\.io\/\$\{\{ steps\.ns\.outputs\.repo \}\}\//g) ?? []; expect(references.length).toBeGreaterThanOrEqual(6); expect(workflowText).toContain(`repo=\${GITHUB_REPOSITORY,,}`); }); it("keeps pull requests read-only and every action pinned to a commit", () => { const validate = workflow.jobs.validate; const build = workflow.jobs.build; const publish = workflow.jobs.publish; expect(validate.if).toBe("github.event_name == 'pull_request'"); expect(build.if).toBe( "github.event_name != 'pull_request' && needs.changes.outputs.publish == 'true'", ); expect(publish.if).toBe( "github.event_name != 'pull_request' && needs.changes.outputs.publish == 'true'", ); expect(workflowText).toContain("cancel-in-progress: ${{ github.event_name == 'pull_request' }}"); expect(workflowText).toContain("publish-server-image-build-${{ github.ref }}"); expect(workflowText).toContain("publish-server-image-publish-${{ github.ref == 'refs/heads/main' && 'main' || github.ref }}"); expect(workflowText).toContain("push: false"); for (const match of workflowText.matchAll(/uses:\s+([^\s#]+)/g)) { expect(match[1], match[1]).toMatch(/@[0-9a-f]{40}$/); } }); });