Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date. | 1 | /** |
| 2 | * The whole service, on Node, opening builds for g1t Actions jobs | |
| 3 | * (`start_from_workflow`): a D1 made of node:sqlite with the service's | |
| 4 | * migrations, the other services as fakes that answer by method, and | |
| 5 | * Cloudflare's API as a recorded fetch. See moves.flow.test.ts. | |
| 6 | */ | |
| 7 | ||
| 8 | import assert from "node:assert/strict"; | |
| 9 | import { readdirSync, readFileSync } from "node:fs"; | |
| 10 | import { registerHooks } from "node:module"; | |
| 11 | import { DatabaseSync } from "node:sqlite"; | |
| 12 | import { beforeEach, test } from "node:test"; | |
| 13 | ||
| 14 | registerHooks({ | |
| 15 | resolve(specifier, context, next) { | |
| 16 | try { | |
| 17 | return next(specifier, context); | |
| 18 | } catch (error) { | |
| 19 | if (specifier.startsWith(".")) return next(`${specifier}.ts`, context); | |
| 20 | throw error; | |
| 21 | } | |
| 22 | }, | |
| 23 | }); | |
| 24 | const { default: worker } = await import("./index.ts"); | |
| 25 | ||
| 26 | function d1(db: DatabaseSync) { | |
| 27 | const value = (v: unknown) => (v === undefined ? null : typeof v === "boolean" ? Number(v) : v); | |
| 28 | const statement = (sql: string, params: unknown[] = []) => ({ | |
| 29 | sql, | |
| 30 | params, | |
| 31 | bind: (...args: unknown[]) => statement(sql, args.map(value)), | |
| 32 | async first<T>() { | |
| 33 | return (db.prepare(sql).get(...(params as never[])) as T) ?? null; | |
| 34 | }, | |
| 35 | async all<T>() { | |
| 36 | return { results: db.prepare(sql).all(...(params as never[])) as T[], success: true }; | |
| 37 | }, | |
| 38 | async run() { | |
| 39 | const r = db.prepare(sql).run(...(params as never[])); | |
| 40 | return { success: true, meta: { changes: Number(r.changes) } }; | |
| 41 | }, | |
| 42 | }); | |
| 43 | return { | |
| 44 | prepare: (sql: string) => statement(sql), | |
| 45 | async batch(statements: ReturnType<typeof statement>[]) { | |
| 46 | db.exec("BEGIN"); | |
| 47 | try { | |
| 48 | const out = statements.map((s) => ({ results: db.prepare(s.sql).all(...(s.params as never[])) })); | |
| 49 | db.exec("COMMIT"); | |
| 50 | return out; | |
| 51 | } catch (error) { | |
| 52 | db.exec("ROLLBACK"); | |
| 53 | throw error; | |
| 54 | } | |
| 55 | }, | |
| 56 | }; | |
| 57 | } | |
| 58 | ||
| 59 | const REPO = "rep_web"; | |
| 60 | const PROJECT = "prj_web"; | |
| 61 | const SHA = "4b8d0f2a6c1e3579bd02468ace13579bdf02468a"; | |
| 62 | ||
| 63 | function world() { | |
| 64 | const sqlite = new DatabaseSync(":memory:"); | |
| 65 | const dir = new URL("../migrations/", import.meta.url); | |
| 66 | for (const file of readdirSync(dir).sort()) sqlite.exec(readFileSync(new URL(file, dir), "utf8")); | |
| 67 | const state = { | |
| 68 | sqlite, | |
| 69 | run: { id: "run_1", event: "push", ref: "refs/heads/main", sha: SHA, pull: null as number | null, status: "in_progress", actor: "ada" }, | |
| 70 | jobStatus: "in_progress", | |
| 71 | approval: null as null | { state: string }, | |
| 72 | pull: { status: "open", headCommit: SHA, branch: "feature" as string | null, fork: null as null | { namespace: string; name: string } }, | |
| 73 | collaborator: "write", | |
| 74 | plan: true, | |
| 75 | limit: "ok", | |
| 76 | /** What other services were asked, as `SERVICE method`. */ | |
| 77 | calls: [] as string[], | |
| 78 | /** What `resolve_settings` was asked for. */ | |
| 79 | resolved: [] as { environment: string; trusted: boolean }[], | |
| 80 | }; | |
| 81 | const project = () => ({ | |
| 82 | id: PROJECT, | |
| 83 | workspace: "acme", | |
| 84 | slug: "web", | |
| 85 | name: "web", | |
| 86 | primary: true, | |
| 87 | source: { kind: "hosted", repoId: REPO, repo: { namespace: "acme", name: "web" }, defaultBranch: "main", rootDir: "site" }, | |
| 88 | }); | |
| 89 | const ok = (value: unknown) => ({ ok: true, value }); | |
| 90 | const service = (name: string, answer: (method: string, args: any) => unknown) => ({ | |
| 91 | async fetch(input: string | Request, init?: RequestInit) { | |
| 92 | const url = typeof input === "string" ? input : input.url; | |
| 93 | const method = new URL(url).pathname.split("/").pop()!; | |
| 94 | state.calls.push(`${name} ${method}`); | |
| 95 | const args = init?.body ? JSON.parse(String(init.body)) : {}; | |
| 96 | return Response.json((await answer(method, args)) ?? ok(null)); | |
| 97 | }, | |
| 98 | }); | |
| 99 | const env = { | |
| 100 | DB: d1(sqlite), | |
| 101 | CLOUDFLARE_API_TOKEN: "test", | |
| 102 | CLOUDFLARE_ACCOUNT_ID: "acct", | |
| 103 | DISPATCH_NAMESPACE: "g1t-deployments", | |
| 104 | SITE: "https://g1t.sh", | |
| 105 | REPOS: service("REPOS", (method) => { | |
| 106 | if (method === "get") return ok({ id: REPO, namespace: "acme", name: "web", isPrivate: true, defaultBranch: "main" }); | |
| 107 | return ok([]); | |
| 108 | }), | |
| 109 | PROJECTS: service("PROJECTS", (method) => (method === "by_repo" ? [project()] : ok(project()))), | |
| 110 | IDENTITY: service("IDENTITY", (method, args) => { | |
| 111 | if (method === "get_workspace") return { id: `wsp_${args.slug}`, slug: args.slug }; | |
| 112 | if (method === "collaborator_permission") return ok({ role: state.collaborator }); | |
| 113 | return ok(null); | |
| 114 | }), | |
| 115 | BILLING: service("BILLING", (method) => { | |
| 116 | if (method === "has_feature") return state.plan ? ok(true) : { ok: false, error: { code: "payment_required", message: "Deployments need the Team plan." } }; | |
| 117 | if (method === "check_limit") return ok({ state: state.limit, message: state.limit === "stopped" ? "The workspace reached its spend limit." : null }); | |
| 118 | if (method === "prices") return { prices: [] }; | |
| 119 | return ok(null); | |
| 120 | }), | |
| 121 | WORK: service("WORK", (method) => { | |
| 122 | if (method === "get_pull") return ok({ pull: { ...state.pull, number: 9, author: { username: "eve", kind: "user" } } }); | |
| 123 | return ok(null); | |
| 124 | }), | |
| 125 | ACTIONS: service("ACTIONS", (method, args) => { | |
| 126 | if (method === "run") { | |
| 127 | return ok({ run: state.run, jobs: [{ id: "job_1", status: state.jobStatus }], notes: [], approval: state.approval }); | |
| 128 | } | |
| 129 | if (method === "resolve_settings") { | |
| 130 | state.resolved.push({ environment: args.environment, trusted: args.trusted }); | |
| 131 | return { secrets: { API_KEY: "s3cret-value" }, variables: { PUBLIC_URL: "https://example.com" } }; | |
| 132 | } | |
| 133 | return ok(null); | |
| 134 | }), | |
| 135 | RUNNER: service("RUNNER", () => ok(true)), | |
| 136 | }; | |
| 137 | return { state, env }; | |
| 138 | } | |
| 139 | ||
| 140 | let w: ReturnType<typeof world>; | |
| 141 | const realFetch = globalThis.fetch; | |
| 142 | ||
| 143 | beforeEach(() => { | |
| 144 | w = world(); | |
| 145 | globalThis.fetch = (async (input: string | Request) => { | |
| 146 | const url = new URL(typeof input === "string" ? input : input.url); | |
| 147 | w.state.calls.push(`CLOUDFLARE ${url.pathname}`); | |
| 148 | if (url.pathname.endsWith("/graphql")) return Response.json({ data: { viewer: { accounts: [{}] } } }); | |
| 149 | return Response.json({ success: true, result: { jwt: "done", buckets: [] }, result_info: { total_pages: 1 } }); | |
| 150 | }) as typeof fetch; | |
| 151 | w.state.sqlite | |
| 152 | .prepare( | |
| 153 | `INSERT INTO settings (project_id, repo_id, workspace, slug, enabled, previews, production, idle_days, updated_at) | |
| 154 | VALUES (?, ?, 'acme', 'web', 1, 1, 1, 7, '2026-10-05T00:00:00Z')`, | |
| 155 | ) | |
| 156 | .run(PROJECT, REPO); | |
| 157 | }); | |
| 158 | ||
| 159 | process.on("exit", () => { | |
| 160 | globalThis.fetch = realFetch; | |
| 161 | }); | |
| 162 | ||
| 163 | const jobActor = (scopes: string[] = ["contents:read", "deployments:write"]) => ({ | |
| 164 | id: "wsp_acme", | |
| 165 | username: "acme", | |
| 166 | kind: "workspace", | |
| 167 | verified: true, | |
| 168 | workspaces: [{ slug: "acme", role: "member" }], | |
| 169 | token: { token_id: "tok_job", repo: "acme/web", job: { run_id: "run_1", job_id: "job_1" }, scopes }, | |
| 170 | }); | |
| 171 | ||
| 172 | async function post(path: string, body: unknown): Promise<any> { | |
| 173 | const response = await worker.fetch( | |
| 174 | new Request(`https://deployments${path}`, { method: "POST", body: JSON.stringify(body) }), | |
| 175 | w.env as never, | |
| 176 | {} as never, | |
| 177 | ); | |
| 178 | return response.json(); | |
| 179 | } | |
| 180 | ||
| 181 | const open = (body: Record<string, unknown> = {}) => | |
| 182 | post("/rpc/start_from_workflow", { actor: jobActor(), repo: { namespace: "acme", name: "web" }, ...body }); | |
| 183 | ||
| 184 | const rows = (sql: string, ...params: unknown[]) => | |
| 185 | (w.state.sqlite.prepare(sql).all(...(params as never[])) as any[]).map((row) => ({ ...row })); | |
| 186 | ||
| 187 | test("a push to the production branch opens a production build the job runs, with no sandbox and nothing reserved", async () => { | |
| 188 | const answer = await open(); | |
| 189 | assert.equal(answer.ok, true, JSON.stringify(answer)); | |
| 190 | const build = answer.value; | |
| 191 | assert.equal(build.kind, "production"); | |
| 192 | assert.equal(build.project, "web"); | |
| 193 | assert.equal(build.commit, SHA); | |
| 194 | assert.equal(build.url, "https://web-acme.g1t.page"); | |
| 195 | assert.equal(build.trusted, true); | |
| 196 | assert.equal(build.root_dir, "site"); | |
| 197 | assert.deepEqual(build.build_secrets, { API_KEY: "s3cret-value" }); | |
| 198 | assert.deepEqual(build.build_env, { PUBLIC_URL: "https://example.com" }); | |
| 199 | assert.match(build.token, /^[0-9a-f]{64}$/); | |
| 200 | assert.deepEqual(rows("SELECT kind, status, run_id, job_id, created_by, trusted FROM deployments"), [ | |
| 201 | { kind: "production", status: "queued", run_id: "run_1", job_id: "job_1", created_by: "ada", trusted: 1 }, | |
| 202 | ]); | |
| 203 | // The job runs it: no sandbox, and no compute reserved for it. | |
| 204 | assert.ok(!w.state.calls.includes("RUNNER start_deploy")); | |
| 205 | assert.ok(!w.state.calls.some((call) => call === "BILLING reserve" || call === "BILLING entitlements")); | |
| 206 | // The commit's status says it is building. | |
| 207 | assert.ok(w.state.calls.includes("WORK set_commit_status")); | |
| 208 | }); | |
| 209 | ||
| 210 | test("the job reports with the build's token, and its seconds are not charged again", async () => { | |
| 211 | const { value: build } = await open(); | |
| 212 | assert.equal((await post(`/jobs/${build.deploy_id}/started`, { token: build.token })).ok, true); | |
| 213 | const finished = await post(`/jobs/${build.deploy_id}/finish`, { | |
| 214 | token: build.token, | |
| 215 | worker: { compatibilityDate: "2026-09-26", vars: {}, notFoundHandling: "single-page-application" }, | |
| 216 | completionJwt: "done", | |
| 217 | buildSeconds: 42, | |
| 218 | detected: "static", | |
| 219 | }); | |
| 220 | assert.equal(finished.ok, true, JSON.stringify(finished)); | |
| 221 | assert.equal(rows("SELECT error FROM deployments")[0].error, null); | |
| 222 | assert.deepEqual(rows("SELECT status, build_seconds, detected FROM deployments"), [{ status: "ready", build_seconds: 42, detected: "static" }]); | |
| 223 | assert.deepEqual(rows("SELECT script, kind FROM apps"), [{ script: "web-acme", kind: "production" }]); | |
| 224 | assert.ok(!w.state.calls.includes("BILLING charge_feature")); | |
| 225 | assert.deepEqual(rows("SELECT build_seconds FROM meters WHERE build_seconds > 0"), []); | |
| 226 | // The token was for that build alone, until it finished. | |
| 227 | assert.equal((await post(`/jobs/${build.deploy_id}/started`, { token: build.token })).ok, false); | |
| 228 | }); | |
| 229 | ||
| 230 | test("a sandbox's build is still charged by the second; a workflow job's never is", async () => { | |
| 231 | // A classic build: what a push opens, built by g1t's sandbox. | |
| 232 | const { createHash } = await import("node:crypto"); | |
| 233 | const token = "f".repeat(64); | |
| 234 | w.state.sqlite | |
| 235 | .prepare( | |
| 236 | `INSERT INTO deployments (id, project_id, workspace, slug, repo_id, repo, kind, branch, number, commit_sha, script, status, | |
| 237 | token_hash, trusted, created_by, created_at) | |
| 238 | VALUES ('dpl_sandbox', ?, 'acme', 'web', ?, 'acme/web', 'preview', 'other', NULL, ?, 'web-git-other-acme', 'building', ?, 1, 'g1t', '2026-10-12T00:00:00Z')`, | |
| 239 | ) | |
| 240 | .run(PROJECT, REPO, SHA, createHash("sha256").update(token).digest("hex")); | |
| 241 | const finish = { worker: { compatibilityDate: "2026-09-26", vars: {} }, completionJwt: "done", buildSeconds: 30 }; | |
| 242 | assert.equal((await post("/jobs/dpl_sandbox/finish", { token, ...finish })).ok, true); | |
| 243 | assert.equal(w.state.calls.filter((call) => call === "BILLING charge_feature").length, 1); | |
| 244 | assert.deepEqual(rows("SELECT build_seconds FROM meters"), [{ build_seconds: 30 }]); | |
| 245 | // A workflow job's build of the same project, finished and then failed: not charged. | |
| 246 | const { value: done } = await open(); | |
| 247 | assert.equal((await post(`/jobs/${done.deploy_id}/finish`, { token: done.token, ...finish })).ok, true); | |
| 248 | const { value: failed } = await open(); | |
| 249 | await post(`/jobs/${failed.deploy_id}/fail`, { token: failed.token, message: "npm run build failed", buildSeconds: 99 }); | |
| 250 | assert.equal(w.state.calls.filter((call) => call === "BILLING charge_feature").length, 1); | |
| 251 | assert.deepEqual(rows("SELECT build_seconds FROM meters"), [{ build_seconds: 30 }]); | |
| 252 | // What runs after the build is the same app as any: in apps, which the sweep meters. | |
| 253 | assert.deepEqual(rows("SELECT script, kind FROM apps ORDER BY script"), [ | |
| 254 | { script: "web-acme", kind: "production" }, | |
| 255 | { script: "web-git-other-acme", kind: "preview" }, | |
| 256 | ]); | |
| 257 | }); | |
| 258 | ||
| 259 | test("another branch builds its preview; a pull request's run builds the pull request's", async () => { | |
| 260 | w.state.run = { ...w.state.run, ref: "refs/heads/feature/x" }; | |
| 261 | const branch = (await open()).value; | |
| 262 | assert.deepEqual([branch.kind, branch.branch, branch.number], ["preview", "feature/x", null]); | |
| 263 | w.state.run = { ...w.state.run, event: "pull_request", ref: "refs/pull/9/merge", pull: 9 }; | |
| 264 | const pull = (await open()).value; | |
| 265 | assert.deepEqual([pull.kind, pull.branch, pull.number, pull.trusted], ["preview", "feature", 9, true]); | |
| 266 | assert.deepEqual(w.state.resolved.map((r) => r.environment), ["preview", "preview"]); | |
| 267 | }); | |
| 268 | ||
| 269 | test("a pull request from a fork, or one that had to be approved, gets no build secrets", async () => { | |
| 270 | w.state.run = { ...w.state.run, event: "pull_request", ref: "refs/pull/9/merge", pull: 9 }; | |
| 271 | w.state.pull = { ...w.state.pull, branch: null, fork: { namespace: "eve", name: "web" } }; | |
| 272 | const fork = (await open()).value; | |
| 273 | assert.deepEqual([fork.branch, fork.trusted, fork.build_secrets], ["pr-9", false, {}]); | |
| 274 | w.state.pull = { ...w.state.pull, branch: "feature", fork: null }; | |
| 275 | w.state.approval = { state: "approved" }; | |
| 276 | const approved = (await open()).value; | |
| 277 | assert.deepEqual([approved.trusted, approved.build_secrets], [false, {}]); | |
| 278 | assert.deepEqual(w.state.resolved.map((r) => r.trusted), [false, false]); | |
| 279 | }); | |
| 280 | ||
| 281 | test("it is refused without a job's write token, after the job, without the plan, or with previews off", async () => { | |
| 282 | const refused = async (body: Record<string, unknown>) => (await open(body)).error?.code; | |
| 283 | assert.equal(await refused({ actor: jobActor(["deployments:read"]) }), "forbidden"); | |
| 284 | assert.equal(await refused({ actor: { ...jobActor(), token: { token_id: "tok_person", scopes: null } } }), "forbidden"); | |
| 285 | assert.equal(await refused({ repo: { namespace: "acme", name: "api" } }), "forbidden"); | |
| 286 | assert.equal(await refused({ project: "docs" }), "not_found"); | |
| 287 | w.state.jobStatus = "completed"; | |
| 288 | assert.equal(await refused({}), "conflict"); | |
| 289 | w.state.jobStatus = "in_progress"; | |
| 290 | w.state.run = { ...w.state.run, ref: "refs/tags/v1.0.0" }; | |
| 291 | assert.equal(await refused({}), "invalid"); | |
| 292 | w.state.run = { ...w.state.run, ref: "refs/heads/main" }; | |
| 293 | w.state.plan = false; | |
| 294 | const unpaid = await open(); | |
| 295 | assert.deepEqual([unpaid.ok, unpaid.error.code], [false, "payment_required"]); | |
| 296 | assert.deepEqual(rows("SELECT status FROM deployments"), [{ status: "skipped" }]); | |
| 297 | w.state.plan = true; | |
| 298 | w.state.limit = "stopped"; | |
| 299 | const stopped = await open(); | |
| 300 | assert.deepEqual([stopped.ok, stopped.error.code, stopped.error.message], [false, "payment_required", "The workspace reached its spend limit."]); | |
| 301 | w.state.limit = "ok"; | |
| 302 | w.state.sqlite.prepare("UPDATE settings SET previews = 0").run(); | |
| 303 | w.state.run = { ...w.state.run, ref: "refs/heads/feature" }; | |
| 304 | assert.equal(await refused({}), "conflict"); | |
| 305 | assert.ok(!w.state.calls.includes("RUNNER start_deploy")); | |
| 306 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.