import { beforeEach, describe, expect, mock, test } from "bun:test"; import { fakeD1 } from "../metering/fake-d1.test-helper"; import { scopedTestState } from "./scoped.test-helper"; import type { SiteDeployRequest } from "./deploy"; let d1 = fakeD1(); const scoped = scopedTestState(); scoped.install(); const r1 = () => scoped.repo("r1"); const kv = new Map(); const fakeEnv: Record = { get DB() { return d1.binding; }, KV: { get: async (key: string, type?: string) => { const value = kv.get(key) ?? null; return value !== null && type === "json" ? JSON.parse(value) : value; }, put: async (key: string, value: string) => void kv.set(key, value), delete: async (key: string) => void kv.delete(key), }, SITES_DOMAIN: "sites.test", PUBLIC_URL: "https://forge.test", }; mock.module("cloudflare:workers", () => ({ DurableObject: class {}, env: fakeEnv, waitUntil: () => undefined })); const deploys: SiteDeployRequest[] = []; let site: Record = {}; const actualDeploy = await import("./deploy"); mock.module("./deploy", () => ({ ...actualDeploy, loadSiteForRepo: async () => site, createSiteDeployment: async (input: SiteDeployRequest) => { deploys.push(input); const id = `d${deploys.length}`; scoped.insert("r1", "site_deployments", { id, site_id: input.site.id, label: input.label, sha: input.sha, status: "queued", }); return { id }; }, })); const actualRepos = await import("../repos"); mock.module("../repos", () => ({ ...actualRepos, loadRepoById: async () => ({ id: "r1", visibility: "public" }) })); const { deploySitesForRun } = await import("./triggers"); const HEAD = "a".repeat(40); function insertRun(id: string, fields: Partial> = {}): void { const row = { id, repo_id: "r1", source_repo_id: "r1", untrusted: 0, event: "pull_request", ref: "refs/pull/7/head", sha: HEAD, pr_number: 7, actor_id: "u1", conclusion: "success", ...fields, }; scoped.insert("r1", "workflow_runs", { ...row, run_number: nextRun++, status: "completed", created_at: 0, }); scoped.register("workflow_run", id, "r1"); r1() .query( "INSERT INTO job_artifacts (id, run_id, job_id, name, size, r2_key, created_at) VALUES (?, ?, 'j', 'github-pages', 0, 'k', 0)", ) .run(`a-${id}`, id); } let nextRun = 1; beforeEach(() => { d1 = fakeD1(); scoped.reset(); kv.clear(); deploys.length = 0; site = { id: "s1", repo_id: "r1", label: "blog--alice", branch: "main", source: "workflow", previews: 1, access: "public", scratch: 0, disabled_reason: null, artifact_name: "github-pages", commit_previews: 0, }; for (const [id, number, state, head, ref] of [ ["i7", 7, "open", "r1", "feature"], ["i8", 8, "open", "fork", "feature"], ["i9", 9, "closed", "r1", "old"], ] as const) { scoped.insert("r1", "issues", { id, number, state }); scoped.insert("r1", "pull_requests", { issue_id: id, head_repo_id: head, head_ref: ref, head_sha: HEAD }); } scoped.insert("r1", "push_events", { id: "p1", ref: "refs/heads/feature", after_sha: HEAD, pusher_id: "author", created_at: 1, }); }); describe("pull_request runs deploy a site preview", () => { test("a trusted same-repo run deploys as its head branch's preview and comments once", async () => { insertRun("run1"); expect(await deploySitesForRun("run1")).toBe(true); expect(deploys).toHaveLength(1); expect(deploys[0]).toMatchObject({ environment: "preview", label: "feature--blog--alice", branch: "feature", ref: "refs/heads/feature", sha: HEAD, artifactId: "a-run1", source: "artifact", trustedId: "author", }); r1().run("UPDATE site_deployments SET status = 'superseded'"); insertRun("run2"); expect(await deploySitesForRun("run2")).toBe(true); const comments = r1().query("SELECT issue_id, body FROM comments").all() as { issue_id: string; body: string }[]; expect(comments).toHaveLength(1); expect(comments[0].issue_id).toBe("i7"); expect(comments[0].body).toContain("pr-7--blog--alice.sites.test"); }); test("shares the host a branch push of the same head deploys to, and deploys a commit only once", async () => { insertRun("pr"); insertRun("push", { event: "push", ref: "refs/heads/feature", pr_number: null }); expect(await deploySitesForRun("pr")).toBe(true); expect(await deploySitesForRun("push")).toBe(false); expect(deploys.map((deploy) => [deploy.label, deploy.branch])).toEqual([["feature--blog--alice", "feature"]]); }); test("a newer commit on the same head deploys again, so the newest deployment wins", async () => { const next = "b".repeat(40); r1().query("UPDATE pull_requests SET head_sha = ? WHERE issue_id = 'i7'").run(next); insertRun("push", { event: "push", ref: "refs/heads/feature", pr_number: null }); insertRun("pr", { sha: next }); expect(await deploySitesForRun("push")).toBe(true); expect(await deploySitesForRun("pr")).toBe(true); expect(deploys.map((deploy) => [deploy.label, deploy.sha])).toEqual([ ["feature--blog--alice", HEAD], ["feature--blog--alice", next], ]); }); test("never deploys forks, untrusted runs, stale heads, closed pulls, merge-queue runs or disabled previews", async () => { insertRun("fork-source", { source_repo_id: "fork" }); insertRun("untrusted", { untrusted: 1 }); insertRun("fork-head", { ref: "refs/pull/8/head", pr_number: 8 }); insertRun("stale", { sha: "b".repeat(40) }); insertRun("closed", { ref: "refs/pull/9/head", pr_number: 9 }); insertRun("mismatch", { ref: "refs/pull/8/head" }); insertRun("queue", { ref: "refs/heads/gitfor-merge-queue/main/pr-7" }); insertRun("failed", { conclusion: "failure" }); for (const id of ["fork-source", "untrusted", "fork-head", "stale", "closed", "mismatch", "queue", "failed"]) { expect(await deploySitesForRun(id)).toBe(false); } insertRun("ok"); site.previews = 0; expect(await deploySitesForRun("ok")).toBe(false); site.previews = 1; site.source = "branch"; expect(await deploySitesForRun("ok")).toBe(false); expect(deploys).toHaveLength(0); }); test("a pull request from the production branch never deploys production", async () => { r1().run("UPDATE pull_requests SET head_ref = 'main' WHERE issue_id = 'i7'"); insertRun("main-head"); expect(await deploySitesForRun("main-head")).toBe(false); expect(deploys).toHaveLength(0); }); test("needs the site's configured artifact", async () => { insertRun("run"); site.artifact_name = "dist"; expect(await deploySitesForRun("run")).toBe(false); }); });