import { expect, mock, test } from "bun:test"; mock.module("cloudflare:workers", () => ({ env: {}, waitUntil: () => undefined, WorkflowEntrypoint: class {}, DurableObject: class {}, WorkerEntrypoint: class {}, })); const { buildArchive } = await import("./archive"); const { parseActionManifest } = await import("./manifest"); const { compileWorkflow } = await import("../compile"); const { buildJobScript } = await import("../script"); const { parseWorkflow } = await import("../spec"); const { buildPack, makeBlob, makeCommit, makeTree } = await import("../../git/objects"); const { parsePack } = await import("../../git/pack"); async function runJob(tamper: boolean) { const yml = "inputs:\n version:\n default: stable\nruns:\n using: node20\n main: index.js\n post: post.js\n"; const main = `const fs=require('fs');console.log('main version='+process.env.INPUT_VERSION+' action='+process.env.GITHUB_ACTION_REPOSITORY);fs.appendFileSync(process.env.GITHUB_OUTPUT,'executable=/usr/bin/vim\\n');fs.appendFileSync(process.env.GITHUB_STATE,'saved=yes\\n');console.log('::add-mask::topsecret');`; const post = `console.log('post state='+process.env.STATE_saved)`; const blobs = [await makeBlob(yml), await makeBlob(main), await makeBlob(post)]; const root = await makeTree([ { mode: "100644", name: "action.yml", sha: blobs[0].sha }, { mode: "100644", name: "index.js", sha: blobs[1].sha }, { mode: "100644", name: "post.js", sha: blobs[2].sha }, ]); const sig = { name: "a", email: "a@x", timestamp: 1, timezone: "+0000" }; const commit = await makeCommit({ tree: root.sha, parents: [], author: sig, committer: sig, message: "x" }); const parsed = await parsePack(await buildPack([...blobs, root, commit]), { maxObjects: 100, maxObjectBytes: 1 << 20, maxTotalBytes: 1 << 22, }); const { tgz } = await buildArchive(parsed, commit.sha); const sum = tamper ? "0".repeat(64) : new Bun.CryptoHasher("sha256").update(tgz).digest("hex"); const key = `o/act@${commit.sha}`; const server = Bun.serve({ port: 0, fetch: (req) => new URL(req.url).searchParams.get("key") === key ? new Response(tgz) : new Response("no", { status: 404 }), }); const wf = parseWorkflow( 'on: push\njobs:\n a:\n steps:\n - id: vim\n uses: o/act@v1\n with:\n version: nightly\n - run: echo "out=${{ steps.vim.outputs.executable }}"\n', ); if (!wf.ok) throw new Error(wf.error); const [job] = compileWorkflow(wf.spec, { event: { event_name: "push" }, inputs: {}, vars: {}, actions: { "o/act@v1": { uses: "o/act@v1", local: false, repository: "o/act", ref: "v1", sha: commit.sha, path: "", archive: { key, sha256: sum }, manifest: parseActionManifest(yml, "o/act@v1"), }, }, }); const dir = (await Bun.$`mktemp -d`.text()).trim(); await Bun.$`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}/job.sh`, buildJobScript(job)); const out = await Bun.$`bash ${dir}/job.sh` .env({ ...process.env, 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(); await Bun.$`rm -rf ${dir}`; return out; } /** Runs a generated job script for real (bash, git, node, curl): download, verify, main, outputs, state, post. */ test("runs a JavaScript action end to end", async () => { const out = await runJob(false); expect(out).toContain("main version=nightly action=o/act"); expect(out).toContain("out=/usr/bin/vim"); expect(out).toContain("post state=yes"); }); test("refuses an archive whose sha256 doesn't match the recorded one", async () => { const out = await runJob(true); expect(out).toContain("failed verification"); expect(out).not.toContain("main version="); });