Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

165 lines · 8.2 KBRaw History
1import { spawnSync } from "node:child_process";
2import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3import { tmpdir } from "node:os";
4import { join } from "node:path";
5import { afterEach, describe, expect, it } from "vitest";
6
7/**
8 * BOOTSTRAP_ADMINS on deploy: scripts/bootstrap-admins.sh (run by bootstrap.sh) and render-config.mjs's
9 * --admins, against a fake Cloudflare API (curl) on PATH. The deploy takes the list from the environment,
10 * else wrangler.jsonc, else keeps the deployed Worker's, and never logs the addresses.
11 */
12
13const repo = new URL("../../../../", import.meta.url).pathname;
14const LIVE = "first@example.com,second@example.com";
15
16type State = {
17 /** The deployed Worker's bindings; null before the first deploy, "denied" for a token without Workers Scripts. */
18 worker: Array<{ type: string; name: string; text?: string }> | null | "denied";
19};
20
21// curl ... URL -H ...: answers from state.json, logs each call.
22const FAKE_CURL = `#!/usr/bin/env node
23const fs = require("node:fs");
24const dir = process.env.FAKE_DIR;
25const state = JSON.parse(fs.readFileSync(dir + "/state.json", "utf8"));
26const a = process.argv.slice(2);
27const method = a.includes("-X") ? a[a.indexOf("-X") + 1] : "GET";
28const url = a.find((x) => x.startsWith("https://"));
29fs.appendFileSync(dir + "/calls.log", JSON.stringify({ method, url }) + "\\n");
30const path = new URL(url).pathname.replace(/^\\/client\\/v4\\/accounts\\/acct/, "");
31const fail = (code, message) => ({ success: false, errors: [{ code, message }], result: null });
32let out;
33if (path === "/workers/scripts/diffinery/settings") {
34 if (state.worker === "denied") out = fail(10000, "Authentication error");
35 else out = state.worker ? { success: true, result: { bindings: state.worker } } : fail(10007, "This Worker does not exist on your account.");
36} else out = fail(7003, "no route for " + path);
37process.stdout.write(JSON.stringify(out));
38`;
39
40let dir = "";
41afterEach(() => {
42 if (dir) rmSync(dir, { recursive: true, force: true });
43 dir = "";
44});
45
46function tree(state: State, fileAdmins?: string): void {
47 dir = mkdtempSync(join(tmpdir(), "admins-"));
48 for (const d of ["scripts", "apps/diffinery", "bin"]) mkdirSync(join(dir, d), { recursive: true });
49 for (const f of ["bootstrap-admins.sh", "jsonc.mjs", "render-config.mjs"]) copyFileSync(join(repo, "scripts", f), join(dir, "scripts", f));
50 let cfg = readFileSync(join(repo, "apps/diffinery/wrangler.jsonc"), "utf8");
51 if (fileAdmins !== undefined) cfg = cfg.replace(/"BOOTSTRAP_ADMINS": "[^"]*",(\s*"ACCESS_TEAM_DOMAIN": "",\s*"ACCESS_AUD": ""\s*\},\s*"d1_databases")/, `"BOOTSTRAP_ADMINS": ${JSON.stringify(fileAdmins)},$1`);
52 writeFileSync(join(dir, "apps/diffinery/wrangler.jsonc"), cfg);
53 writeFileSync(join(dir, "bin/curl"), FAKE_CURL);
54 chmodSync(join(dir, "bin/curl"), 0o755);
55 writeFileSync(join(dir, "state.json"), JSON.stringify(state));
56}
57
58function run(cmd: string[], extraEnv: Record<string, string | undefined> = {}) {
59 const env: Record<string, string> = { PATH: `${join(dir, "bin")}:${process.env.PATH}`, HOME: dir, FAKE_DIR: dir, CLOUDFLARE_API_TOKEN: "token", CLOUDFLARE_ACCOUNT_ID: "acct" };
60 for (const [k, v] of Object.entries(extraEnv)) if (v === undefined) delete env[k];
61 else env[k] = v;
62 const r = spawnSync(cmd[0]!, cmd.slice(1), { cwd: dir, encoding: "utf8", env });
63 const calls = existsSync(join(dir, "calls.log")) ? readFileSync(join(dir, "calls.log"), "utf8").trim().split("\n").filter(Boolean).map((l) => JSON.parse(l) as { method: string; url: string }) : [];
64 return { status: r.status, stdout: r.stdout, stderr: r.stderr, calls };
65}
66
67const admins = (env: Record<string, string | undefined> = {}) => {
68 const r = run(["bash", join(dir, "scripts/bootstrap-admins.sh")], env);
69 // The addresses go to render-config on stdout, never into the deploy log.
70 for (const email of [...LIVE.split(","), "ops@example.com", "file@example.com"]) expect(r.stderr).not.toContain(email);
71 return { ...r, list: r.stdout.trim() };
72};
73const live = [{ type: "plain_text", name: "BOOTSTRAP_ADMINS", text: LIVE }, { type: "plain_text", name: "PUBLIC_URL", text: "https://git.example.com" }, { type: "secret_text", name: "MASTER_KEY" }];
74
75describe("scripts/bootstrap-admins.sh", () => {
76 it("takes BOOTSTRAP_ADMINS from the environment first, without asking Cloudflare", () => {
77 tree({ worker: live }, "file@example.com");
78 const r = admins({ BOOTSTRAP_ADMINS: " ops@example.com, me@example.com " });
79 expect(r.status).toBe(0);
80 expect(r.list).toBe("ops@example.com, me@example.com");
81 expect(r.calls).toEqual([]);
82 expect(r.stderr).toContain("2 address(es), from the BOOTSTRAP_ADMINS environment variable");
83 });
84
85 it("then a list kept in wrangler.jsonc", () => {
86 tree({ worker: live }, "file@example.com");
87 const r = admins({ BOOTSTRAP_ADMINS: "" });
88 expect(r.status).toBe(0);
89 expect(r.list).toBe("file@example.com");
90 expect(r.calls).toEqual([]);
91 });
92
93 it("keeps the deployed Worker's list when neither sets one (the Deploy workflow)", () => {
94 tree({ worker: live });
95 const r = admins();
96 expect(r.status).toBe(0);
97 expect(r.list).toBe(LIVE);
98 expect(r.calls.map((c) => `${c.method} ${new URL(c.url).pathname}`)).toEqual(["GET /client/v4/accounts/acct/workers/scripts/diffinery/settings"]);
99 expect(r.stderr).toContain("2 address(es), from the deployed diffinery Worker");
100 expect(r.stderr).not.toContain("::warning::");
101 });
102
103 it("warns loudly on a brand-new instance with no list, and goes on with none", () => {
104 for (const worker of [null, [{ type: "plain_text", name: "BOOTSTRAP_ADMINS", text: "" }]]) {
105 tree({ worker });
106 const r = admins();
107 expect(r.status).toBe(0);
108 expect(r.list).toBe("");
109 expect(r.stderr).toContain("::warning::BOOTSTRAP_ADMINS is empty");
110 expect(r.stderr).toContain("nobody becomes a site administrator");
111 rmSync(dir, { recursive: true, force: true });
112 }
113 });
114
115 it("warns, without calling Cloudflare, when there are no credentials", () => {
116 tree({ worker: live });
117 const r = admins({ CLOUDFLARE_API_TOKEN: undefined });
118 expect(r.status).toBe(0);
119 expect(r.list).toBe("");
120 expect(r.calls).toEqual([]);
121 expect(r.stderr).toContain("::warning::BOOTSTRAP_ADMINS is empty");
122 });
123
124 it("stops the deploy when the Worker's settings can't be read, rather than drop its list", () => {
125 tree({ worker: "denied" });
126 const r = admins();
127 expect(r.status).toBe(1);
128 expect(r.list).toBe("");
129 expect(r.stderr).toContain("::error::Could not read the diffinery Worker's settings to keep its BOOTSTRAP_ADMINS");
130 expect(r.stderr).toContain("Workers Scripts → Edit");
131 expect(r.stderr).toContain("10000: Authentication error");
132 });
133});
134
135describe("scripts/render-config.mjs --admins", () => {
136 const render = (args: string[]) => {
137 const r = run(["node", join(dir, "scripts/render-config.mjs"), "--d1", "db-id", "--public-url", "https://git.example.com", ...args]);
138 const cfg = r.status === 0 ? (JSON.parse(readFileSync(join(dir, "apps/diffinery/wrangler.deploy.jsonc"), "utf8").split("\n").slice(1).join("\n")) as { vars: Record<string, string> }) : null;
139 return { ...r, cfg };
140 };
141
142 it("sets BOOTSTRAP_ADMINS, trimmed, and leaves wrangler.jsonc's value without the flag", () => {
143 tree({ worker: null });
144 expect(render(["--admins", " a@example.com ,b@example.com,, "]).cfg!.vars.BOOTSTRAP_ADMINS).toBe("a@example.com,b@example.com");
145 expect(render(["--admins", ""]).cfg!.vars.BOOTSTRAP_ADMINS).toBe("");
146 expect(render([]).cfg!.vars.BOOTSTRAP_ADMINS).toBe("");
147 rmSync(dir, { recursive: true, force: true });
148 tree({ worker: null }, "file@example.com");
149 expect(render([]).cfg!.vars.BOOTSTRAP_ADMINS).toBe("file@example.com");
150 });
151
152 it("refuses an entry that isn't an email address", () => {
153 tree({ worker: null });
154 const r = render(["--admins", "a@example.com,diffy"]);
155 expect(r.status).not.toBe(0);
156 expect(r.stderr).toContain('not an email address: "diffy"');
157 });
158
159 it("ships no personal addresses: the repository's wrangler.jsonc leaves the list empty", () => {
160 tree({ worker: null });
161 const r = render([]);
162 expect(r.cfg!.vars.BOOTSTRAP_ADMINS).toBe("");
163 expect(JSON.stringify(r.cfg)).not.toContain("diffinery.com");
164 });
165});