import { describe, expect, mock, test } from "bun:test"; mock.module("cloudflare:workers", () => ({ env: {}, waitUntil: () => undefined, WorkflowEntrypoint: class {}, DurableObject: class {}, WorkerEntrypoint: class {}, })); const { parseImageRef, isDigestPinned, imageTag } = await import("./images"); const { splitArgs } = await import("./compile-action"); const { parseActionManifest } = await import("./manifest"); const { compileWorkflow } = await import("../compile"); const { buildJobScript } = await import("../script"); const { parseWorkflow } = await import("../spec"); const { withDockerRunner, cloudRunnerSize } = await import("../runner-sizes"); const { runnerMeter } = await import("../../metering/ci"); const DIGEST = `sha256:${"c".repeat(64)}`; const LUACHECK = `name: Luacheck inputs: args: default: "." runs: using: docker image: docker://ghcr.io/lunarmodules/luacheck:v1.2.0 entrypoint: sh args: - -c - luacheck \${{ inputs.args }} `; const image = { ref: "ghcr.io/lunarmodules/luacheck:v1.2.0", registry: "ghcr.io", repository: "lunarmodules/luacheck", digest: DIGEST, mediaType: "application/vnd.oci.image.manifest.v1+json", manifestSize: 10, config: { digest: `sha256:${"d".repeat(64)}`, size: 5, mediaType: "" }, layers: [{ digest: `sha256:${"e".repeat(64)}`, size: 7, mediaType: "" }], }; function compileLuacheck(runsOn = "") { const parsed = parseWorkflow( `on: push\njobs:\n lint:\n${runsOn} steps:\n - uses: lunarmodules/luacheck@v1\n with:\n args: "--no-color src"\n`, ); if (!parsed.ok) throw new Error(parsed.error); const [job] = compileWorkflow(parsed.spec, { event: { event_name: "push" }, inputs: {}, vars: {}, actions: { "lunarmodules/luacheck@v1": { uses: "lunarmodules/luacheck@v1", local: false, repository: "lunarmodules/luacheck", ref: "v1", sha: "1".repeat(40), path: "", archive: { key: `lunarmodules/luacheck@${"1".repeat(40)}`, sha256: "a".repeat(64) }, manifest: parseActionManifest(LUACHECK, "lunarmodules/luacheck@v1"), image, }, }, }); return job; } describe("image references", () => { test("defaults to Docker Hub and library/, keeps registries, tags and digests", () => { expect(parseImageRef("alpine")).toEqual({ registry: "registry-1.docker.io", repository: "library/alpine", reference: "latest", }); expect(parseImageRef("ghcr.io/lunarmodules/luacheck:v1.2.0")).toEqual({ registry: "ghcr.io", repository: "lunarmodules/luacheck", reference: "v1.2.0", }); expect(parseImageRef(`docker.io/node@${DIGEST}`).reference).toBe(DIGEST); expect(parseImageRef("localhost:5000/x:1").registry).toBe("localhost:5000"); expect(() => parseImageRef("UPPER/case")).toThrow("valid image"); expect(() => parseImageRef("alpine@sha256:short")).toThrow("digest"); expect(isDigestPinned(`alpine@${DIGEST}`)).toBe(true); expect(isDigestPinned("alpine:3")).toBe(false); expect(imageTag(DIGEST)).toBe(`gitfor-actions/image:${"c".repeat(64)}`); }); test("with.args splits like a shell and keeps expressions whole", () => { expect(splitArgs(`-c "echo hi" 'a b' \${{ inputs.x y }}`)).toEqual(["-c", "echo hi", "a b", "${{ inputs.x y }}"]); expect(splitArgs(" ")).toEqual([]); expect(splitArgs('""')).toEqual([""]); }); }); describe("Docker actions", () => { test("parse registry and Dockerfile images", () => { const manifest = parseActionManifest(LUACHECK, "x"); expect(manifest.runtime).toBe("docker"); expect(manifest.docker?.image).toEqual({ kind: "registry", ref: "ghcr.io/lunarmodules/luacheck:v1.2.0" }); expect(manifest.docker?.entrypoint).toBe("sh"); const built = parseActionManifest("runs:\n using: docker\n image: Dockerfile\n env:\n A: b\n", "x"); expect(built.docker?.image).toEqual({ kind: "dockerfile", path: "Dockerfile" }); expect(built.docker?.env).toEqual([["A", "b"]]); }); test("a job with a Docker action moves to the Docker twin of its size, billed as the base size", () => { expect(compileLuacheck().runsOn).toEqual(["cloudflare-docker"]); expect(compileLuacheck(" runs-on: cloudflare-large\n").runsOn).toEqual(["cloudflare-large-docker"]); expect(compileLuacheck(" runs-on: [self-hosted, linux]\n").runsOn).toEqual(["self-hosted", "linux"]); expect(withDockerRunner(["ubuntu-latest-8-cores"])).toEqual(["cloudflare-xlarge-docker"]); expect(cloudRunnerSize(["cloudflare-large-docker"])?.weight).toBe(cloudRunnerSize(["cloudflare-large"])?.weight); expect(runnerMeter(["cloudflare-docker"])).toBe("compute.runner"); expect(runnerMeter(["cloudflare-xlarge-docker"])).toBe("compute.runner_xlarge"); }); test("inputs reach the image's args and the script runs it by digest", async () => { const job = compileLuacheck(); const docker = job.steps[0].action?.docker; expect(docker?.entrypoint).toBe("sh"); expect(docker?.args).toEqual([ [{ kind: "text", value: "-c" }], [ { kind: "text", value: "luacheck " }, { kind: "text", value: "--no-color src" }, ], ]); const script = buildJobScript(job); expect(script).toContain(`__gitfor_docker_run 0 pull ${DIGEST}`); expect(script).toContain('"INPUT_ARGS=--no-color src"'); const check = Bun.spawnSync(["bash", "-n"], { stdin: new TextEncoder().encode(script) }); expect(check.exitCode).toBe(0); }); test("the generated script loads the image through the forge and runs it with the step's inputs", async () => { const job = compileLuacheck(); const dir = (await Bun.$`mktemp -d`.text()).trim(); await Bun.$`mkdir -p ${dir}/bin && git init -q -b main ${dir}/origin && git -C ${dir}/origin commit -q --allow-empty -m a`; const sha = (await Bun.$`git -C ${dir}/origin rev-parse HEAD`.text()).trim(); await Bun.write( `${dir}/bin/docker`, `#!/usr/bin/env bash case "$1" in info) exit 0 ;; image) [ -f "${dir}/loaded" ] ;; load) cat > "${dir}/loaded" ;; rm) exit 0 ;; run) echo "RUN $*"; echo "INPUT_ARGS=$INPUT_ARGS" ;; esac `, ); await Bun.$`chmod +x ${dir}/bin/docker`; let requested = ""; const server = Bun.serve({ port: 0, fetch: (request) => { requested = new URL(request.url).searchParams.get("digest") ?? ""; return new Response("image-tar"); }, }); await Bun.write(`${dir}/job.sh`, buildJobScript({ ...job, steps: job.steps })); const out = await Bun.$`bash ${dir}/job.sh` .env({ ...process.env, PATH: `${dir}/bin:${process.env.PATH}`, GITFOR_WORKSPACE: `${dir}/ws`, GITFOR_SHA: sha, GITFOR_REF: "refs/heads/main", GITFOR_CLONE_URL: `file://${dir}/origin`, GITFOR_API_URL: `http://localhost:${server.port}`, GITFOR_JOB_TOKEN: "t", GITFOR_REPOSITORY: "me/r", }) .nothrow() .text(); server.stop(); const loaded = await Bun.file(`${dir}/loaded`).text(); await Bun.$`rm -rf ${dir}`; expect(requested).toBe(DIGEST); expect(loaded).toBe("image-tar"); expect(out).toContain("--network host"); expect(out).toContain(`--entrypoint sh ${imageTag(DIGEST)} -c luacheck --no-color src`); expect(out).toContain("INPUT_ARGS=--no-color src"); expect(out).toContain("-e GITHUB_WORKSPACE=/github/workspace"); }); });