Every image publish on main failed at "Resolve registry attestation eligibility" with "accepts 1 arg(s), received 2". The step ran `gh api ... --jq -r '.owner.type'`, but gh api has no -r option: --jq took "-r" as its filter and the real filter became a second positional argument. gh api --jq already prints strings without quotes, so the filter is now passed directly. The images were built and their manifests pushed; only the attestation decision failed. The workflow test now rejects `--jq -r`. Run with the repository's values, the fixed step reports a personal account and a private repository and skips attestation, as the guard intends. Claude-Session: https://claude.ai/code/session_01EyQw3AEaHYXegvgvTPAsi1 Co-authored-by: Claude <noreply@anthropic.com>
79 lines
3.4 KiB
TypeScript
79 lines
3.4 KiB
TypeScript
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<Record<string, string>> } };
|
|
}
|
|
|
|
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).toBe("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("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'");
|
|
expect(publish.if).toBe("github.event_name != 'pull_request'");
|
|
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}$/);
|
|
}
|
|
});
|
|
});
|