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.

Merge 32c4b64f7323c8f4968f9a17f281bec2dfebcf77 into 683b739beb0ea3e50cf58e3a579f825990056406

Diffinery authored 2 parents 683b73932c4b64commit fa826f5a95b38191e53f9782bfb3e4115a206e86Browse files
2 files changed+73−20
modifiedapps/diffinery/test/e2e/email.test.mjs+47−13View file
@@ -12,7 +12,7 @@
1212 // and complaint intake, no email to a suppressed address, lifting it again; the
1313 // morning digest on its schedule (phase 5c) and Today at phone width.
1414 import "./fresh-connections.mjs";
15-import { spawn, execFileSync } from "node:child_process";
15+import { spawn, spawnSync, execFileSync } from "node:child_process";
1616 import { mkdtempSync, writeFileSync, mkdirSync, existsSync, rmSync } from "node:fs";
1717 import { tmpdir } from "node:os";
1818 import { createHmac, randomBytes } from "node:crypto";
@@ -77,7 +77,8 @@
7777 }
7878
7979 const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
80-async function until(what, fn, timeoutMs = 60_000) {
80+/** `explain`, if given, says on a timeout where things stood (the polling's own log lines are left out of the tail). */
81+async function until(what, fn, timeoutMs = 60_000, explain = async () => "") {
8182 const end = Date.now() + timeoutMs;
8283 let last;
8384 while (Date.now() < end) {
@@ -85,9 +86,38 @@
8586 if (last) return last;
8687 await sleep(500);
8788 }
88- assert.fail(`timed out waiting for ${what}\n--- wrangler log tail ---\n${devLog.slice(-4000)}`);
89+ const tail = devLog
90+ .split("\n")
91+ .filter((l) => !/runners\/claim 204/.test(l))
92+ .join("\n")
93+ .slice(-4000);
94+ assert.fail(`timed out waiting for ${what}\n${await explain().catch((e) => `(couldn't say where things stood: ${e.message})`)}\n--- wrangler log tail ---\n${tail}`);
8995 }
9096
97+/**
98+ * Waits for a self-hosted job of the repository to claim; on a timeout, says what the push printed and
99+ * where the repository's runs and their jobs stood, so a job that never became claimable can be told apart.
100+ */
101+async function claimJob(repoPath, pushed = "") {
102+ return until(
103+ "a job to claim",
104+ async () => {
105+ const r = await runnerClaim(repoPath, ["linux"]);
106+ return r.status === 200 ? r.json : null;
107+ },
108+ 60_000,
109+ async () => {
110+ const runs = (await ok("GET", `${repoPath}/actions/runs?per_page=10`)).workflow_runs;
111+ const lines = [`push output: ${pushed.trim() || "(none)"}`, `runs: ${runs.length}`];
112+ for (const r of runs) {
113+ const jobs = (await ok("GET", `${repoPath}/actions/runs/${r.id}/jobs?filter=all`)).jobs;
114+ lines.push(`- run ${r.run_number} ${r.head_sha.slice(0, 7)} ${r.status}/${r.conclusion}: ${jobs.map((j) => `${j.name} ${j.status}/${j.conclusion}`).join(", ") || "no jobs"}`);
115+ }
116+ return lines.join("\n");
117+ },
118+ );
119+}
120+
91121 /** My emails, newest first, with the full message (kept in local development only). */
92122 const emails = async (q = "") => ok("GET", `/diffinery/emails?per_page=200${q}`);
93123 /** Wait for an email matching `pred` and return it. */
@@ -103,6 +133,18 @@
103133 });
104134 }
105135
136+/** Pushes main and returns what the server said (a workflow that failed to start says so here). */
137+function gitPush(cwd) {
138+ const auth = Buffer.from(`x-token:${token}`).toString("base64");
139+ const r = spawnSync("git", ["-c", `http.extraHeader=Authorization: Basic ${auth}`, "-c", "protocol.version=2", "push", "origin", "main"], {
140+ cwd,
141+ encoding: "utf8",
142+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
143+ });
144+ assert.equal(r.status, 0, `git push failed: ${r.stderr}`);
145+ return r.stderr.split("\n").filter((l) => l.startsWith("remote:")).join("\n");
146+}
147+
106148 function spawnAsync(cmd, args, env, timeoutMs) {
107149 return new Promise((done) => {
108150 const child = spawn(cmd, args, { env: { ...process.env, ...env } });
@@ -277,11 +319,7 @@
277319 git(src, ["add", "-A"]);
278320 git(src, ["commit", "-q", "-m", "Add CI that fails"]);
279321 git(src, ["remote", "add", "origin", `${base}/${owner}/mailbox.git`]);
280- git(src, ["push", "-q", "origin", "main"]);
281- const claim = await until("a job to claim", async () => {
282- const r = await runnerClaim(`/repos/${owner}/mailbox`, ["linux"]);
283- return r.status === 200 ? r.json : null;
284- });
322+ const claim = await claimJob(`/repos/${owner}/mailbox`, gitPush(src));
285323 await spawnAsync(process.execPath, [AGENT], { DIFFINERY_API_URL: claim.api_url, DIFFINERY_JOB_ID: claim.job_id, DIFFINERY_JOB_TOKEN: claim.token, DIFFINERY_WORK_ROOT: join(work, `job-${claim.job_id}`) }, 180_000);
286324 const failed = await email("run failed", (e) => e.kind === "run" && e.subject.includes(`${owner}/mailbox`));
287325 assert.equal(failed.status, "sent");
@@ -294,11 +332,7 @@
294332 writeFileSync(join(src, file), `${file}\n`);
295333 git(src, ["add", "-A"]);
296334 git(src, ["commit", "-q", "-m", `Add ${file}`]);
297- git(src, ["push", "-q", "origin", "main"]);
298- const c = await until("a job to claim", async () => {
299- const r = await runnerClaim(`/repos/${owner}/mailbox`, ["linux"]);
300- return r.status === 200 ? r.json : null;
301- });
335+ const c = await claimJob(`/repos/${owner}/mailbox`, gitPush(src));
302336 await spawnAsync(process.execPath, [AGENT], { DIFFINERY_API_URL: c.api_url, DIFFINERY_JOB_ID: c.job_id, DIFFINERY_JOB_TOKEN: c.token, DIFFINERY_WORK_ROOT: join(work, `job-${c.job_id}`) }, 180_000);
303337 };
304338 await ok("PUT", `/repos/${owner}/mailbox/subscription`, { ignored: true });
modifiedapps/diffinery/test/unit/markdown-safety.test.ts+26−7View file
@@ -10,6 +10,25 @@
1010 return { html, ms: performance.now() - t };
1111 }
1212
13+/** Ordinary Markdown of `length` characters: paragraphs with emphasis, a link and code. */
14+function prose(length: number): string {
15+ const p = "Some *plain* prose with a [link](/x) and `code`.\n\n";
16+ return p.repeat(Math.ceil(length / p.length)).slice(0, length);
17+}
18+
19+/**
20+ * Renders `source` about as fast as ordinary Markdown of the same length, timed on the same machine,
21+ * so a slower or busy CI host doesn't change the verdict (the hosts differ about 2x per core and can
22+ * stall for a moment, vitest.shared.ts). Each takes the best of three. These inputs took 0.1 to 5
23+ * times as long as the prose on 2026-10-24; a backtracking pattern took seconds, hundreds of times as long.
24+ */
25+function expectLinear(source: string): void {
26+ const best = (s: string) => Math.min(...[0, 1, 2].map(() => timed(s).ms));
27+ const baseline = best(prose(source.length));
28+ const ms = best(source);
29+ expect(ms, `${ms.toFixed(1)} ms, against ${baseline.toFixed(1)} ms for ordinary Markdown as long`).toBeLessThan(25 * baseline + 20);
30+}
31+
1332 describe("markdown block patterns", () => {
1433 // Each of these took seconds with a single backtracking regex per line.
1534 it.each([
@@ -20,7 +39,7 @@
2039 ["a table delimiter row with spaces before text", "a|b\n|-" + spaces + "x"],
2140 ["a dash run with spaces before text", "a|b\n---" + spaces + "x"],
2241 ])("renders %s in linear time", (_name, source) => {
23- expect(timed(source).ms).toBeLessThan(500);
42+ expectLinear(source);
2443 });
2544
2645 // Unmatched link and emphasis openers each scan ahead; a budget per render bounds the total.
@@ -31,7 +50,7 @@
3150 ["list items of emphasis openers", "- " + "*> ".repeat(19_000)],
3251 ["many such paragraphs", ("_1.".repeat(19_000) + "\n\n").repeat(8)],
3352 ])("bounds the lookahead for %s", (_name, source) => {
34- expect(timed(source).ms).toBeLessThan(500);
53+ expectLinear(source);
3554 });
3655
3756 it("formats ordinary text in full", () => {
@@ -83,7 +102,7 @@
83102 ["a long tilde run", "~".repeat(59_000) + "`"],
84103 ["a long backtick run", "`".repeat(59_000) + "x`"],
85104 ])("renders %s in linear time", (_name, source) => {
86- expect(timed(source).ms).toBeLessThan(500);
105+ expectLinear(source);
87106 });
88107
89108 it("keeps hard line breaks", () => {
@@ -134,10 +153,10 @@
134153 });
135154
136155 it("renders many comment openers in linear time", () => {
137- expect(timed("a <!-- ".repeat(40_000)).ms).toBeLessThan(500);
138- expect(timed("a <!-- b --> ".repeat(30_000)).ms).toBeLessThan(500);
139- expect(timed("<!-- x -->\n".repeat(40_000)).ms).toBeLessThan(500);
140- expect(timed("`` <!-- ` ".repeat(30_000) + "-->").ms).toBeLessThan(500);
156+ expectLinear("a <!-- ".repeat(40_000));
157+ expectLinear("a <!-- b --> ".repeat(30_000));
158+ expectLinear("<!-- x -->\n".repeat(40_000));
159+ expectLinear("`` <!-- ` ".repeat(30_000) + "-->");
141160 });
142161
143162 it("strips comments from plain text, outside code", () => {