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.

739 lines · 42.2 KBRaw History
1/** @jsxImportSource hono/jsx */
2/**
3 * The public site around the product: the home page for visitors (Landing), and the product,
4 * comparison and company pages written as Markdown in site/ (built into content.generated.ts by
5 * scripts/build-docs.mjs), served at /features, /compare/github and so on.
6 *
7 * Written for people, search engines and AI assistants alike. Every page is also served as Markdown
8 * (/<page>.md, or the page itself with `Accept: text/markdown`; the home page at /index.md), is listed in
9 * /llms.txt, /llms-full.txt and the sitemap, and carries schema.org structured data: the organization,
10 * the product, breadcrumbs, the date its facts were last checked, and its questions and answers.
11 */
12import { raw } from "hono/html";
13import type { FC } from "hono/jsx";
14import type { AppContext } from "../auth/principal.js";
15import { SITE_NAV, SITE_PAGES } from "../docs/content.generated.js";
16import { imageProxy } from "../lib/camo.js";
17import { ICONS } from "./assets.js";
18import { fenceHighlighter } from "./highlight.js";
19import { escapeHtml, fenceLangs, renderMarkdown } from "./markdown.js";
20import { SITE } from "./seo.js";
21import { render, type WebApp } from "./ui.js";
22
23export interface SitePageInfo {
24 /** URL path, e.g. "/compare/github". */
25 path: string;
26 title: string;
27 description: string;
28 markdown: string;
29 /** The date its facts were last checked, YYYY-MM-DD. */
30 updated: string;
31 /** Blog posts: the date the post first went out, YYYY-MM-DD (`updated` when it's left out). */
32 published?: string;
33 seoTitle: string | null;
34 /** The site/nav.json section it belongs to ("Product", "Compare", "Company"). */
35 section: string;
36}
37
38const origin = (c: AppContext) => c.env.PUBLIC_URL.replace(/\/$/, "");
39
40/** Every site page, in the order of site/nav.json. */
41export function sitePages(): SitePageInfo[] {
42 return SITE_NAV.flatMap((s) => s.pages.map((path) => ({ path, ...SITE_PAGES[path]!, section: s.title })));
43}
44
45export function sitePage(path: string): SitePageInfo | null {
46 return sitePages().find((p) => p.path === path) ?? null;
47}
48
49/** Root-relative links (`](/docs/...)`) made absolute, for Markdown read outside the site. */
50const absolute = (md: string, o: string) => md.replace(/\]\(\/(?!\/)/g, `](${o}/`);
51
52const longDate = (d: string) => new Date(`${d}T00:00:00Z`).toLocaleDateString("en-US", { dateStyle: "long", timeZone: "UTC" });
53
54/** A site page as standalone Markdown: what /<page>.md serves. */
55export function sitePageMarkdown(p: SitePageInfo, o: string): string {
56 return `# ${p.title}\n\n> ${p.description}\n\nSource: ${o}${p.path} (last updated ${p.updated})\n\n${absolute(p.markdown, o).trim()}\n`;
57}
58
59// ---------------------------------------------------------------- questions and answers
60
61/** The "## Frequently asked questions" section of a page: each `###` heading and the text under it. */
62export function faqs(markdown: string): Array<{ q: string; a: string }> {
63 const m = /^## Frequently asked questions\s*$([\s\S]*?)(?=^## |(?![\s\S]))/m.exec(markdown);
64 if (!m) return [];
65 return [...m[1]!.matchAll(/^### +(.+?)\s*$([\s\S]*?)(?=^### |(?![\s\S]))/gm)].map((x) => ({ q: x[1]!.trim(), a: x[2]!.trim() })).filter((x) => x.a);
66}
67
68/** Markdown reduced to plain text, for structured data. */
69export const plainText = (md: string) =>
70 md
71 .replace(/`([^`]*)`/g, "$1")
72 .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
73 .replace(/\*\*([^*]+)\*\*/g, "$1")
74 .replace(/\s+/g, " ")
75 .trim();
76
77// ---------------------------------------------------------------- the home page
78
79/** When the home page's facts were last checked. */
80export const HOME_UPDATED = "2026-10-25";
81
82const HOME = {
83 // No SITE here: seo.tsx imports this module, so SITE isn't defined yet while it loads.
84 title: "Diffinery: open-source GitHub alternative with fast CI, built for AI agents",
85 description:
86 "Diffinery is open-source code hosting and CI for the agentic era, running on your own Cloudflare account: Git repositories, pull requests, GitHub Actions–compatible CI on 4-vCPU runners, and AI agents with their own accounts and guardrails.",
87 h1: "Code hosting and CI for the agentic era",
88 lead: "Diffinery is an open-source GitHub alternative for teams of people and AI agents. Git repositories, pull requests, GitHub Actions–compatible CI on fast runners, and agents with their own accounts and guardrails, running entirely on your own Cloudflare account.",
89 stats: [
90 { value: "4 vCPU", label: "per CI job by default. GitHub's standard runner for private repositories has 2." },
91 { value: "3–5 s", label: "from a job being queued to its first step." },
92 { value: "Apache-2.0", label: "Open source, with nothing to buy. You run it on your own Cloudflare account." },
93 { value: "1 person", label: "approves every agent pull request, enforced by the server." },
94 ],
95 features: [
96 { icon: "repo", title: "Git hosting", text: "Clone, fetch and push over HTTPS with Git LFS. Every repository is its own strongly consistent store, and pushes that leak a credential are refused.", href: "/features#git-hosting" },
97 { icon: "pr", title: "Pull requests with intent", text: "Reviews, required approvals and checks, auto-merge and a merge queue. Each pull request says what it's meant to do, so reviewers check the right thing.", href: "/features#pull-requests-reviews-and-issues" },
98 { icon: "play", title: "GitHub Actions–compatible CI", text: "The same triggers, expressions, matrices, reusable workflows, secrets, OIDC and Docker. Actions from GitHub run as they do there.", href: "/features#actions-ci-that-runs-your-github-workflows" },
99 { icon: "zap", title: "Fast, right-sized runners", text: "4 vCPUs and 12 GiB per job by default, started in seconds. Jobs that only wait move to smaller runners on their own, never at the cost of speed.", href: "/performance" },
100 { icon: "bot", title: "AI agents with guardrails", text: "Agents get their own accounts and tokens, run tasks with Claude Code or Codex, review pull requests and merge on green. They can't push to protected branches or reach your secrets.", href: "/agents" },
101 { icon: "shield", title: "Secure by default", text: "Passkeys instead of passwords, invite-only sign-up, encrypted secrets, scoped job tokens and an append-only audit log.", href: "/security" },
102 ],
103 /** What agents do on Diffinery: the home page's "Built for AI coding agents" cards, its Markdown and the structured data. */
104 agents: [
105 { icon: "shield", title: "Agent accounts with guardrails", text: "Each agent has its own login and tokens, owned by a person or an organization. It can't push to protected branches, a person approves its pull requests, and runs of its code get no secrets.", href: "/docs/agents/overview#guardrails" },
106 { icon: "bot", title: "Agent tasks", text: "Give an agent a goal: Claude Code or Codex works in a sandbox with your own model key and opens a pull request. Plan first, best of N attempts, and fixes for its own failing checks.", href: "/docs/agents/tasks" },
107 { icon: "eye", title: "Agent review", text: "An agent reviews every pull request for bugs, security issues and logic errors, posts inline findings with suggested fixes and reports an Agent review check.", href: "/docs/agents/review" },
108 { icon: "history", title: "Today and Needs you", text: "One page each morning: what needs you, what your agents are working on, and what merged or failed since yesterday, across every repository.", href: "/docs/agents/today" },
109 { icon: "prMerged", title: "Merge on green", text: "Pull requests merge by themselves once their checks pass, with or without a person's approval, as you choose. An agent's change to a workflow file still needs a person to merge it.", href: "/docs/collaboration/merge-on-green" },
110 { icon: "key", title: "Your own subscription", text: "Open a terminal in the browser, sign in to Claude Code or Codex with your own Claude or ChatGPT plan, and give it tasks that open pull requests.", href: "/docs/agents/terminal" },
111 ],
112 /** The tools Diffinery plugs into: the home page's integrations, its Markdown and the structured data. */
113 integrations: [
114 { icon: "bot", title: "Claude Code plugin", text: "The MCP server, a skill for the branch → pull request → CI → merge loop, /diffinery:pr-status, and a live band of the pull request's CI checks above the prompt.", install: "claude plugin marketplace add codician-team/diffinery", href: "/docs/connect/claude-code" },
115 { icon: "code", title: "Codex plugin", text: "The same MCP server, skills and session hook for OpenAI's Codex CLI.", install: "codex plugin marketplace add https://diffinery.com/codician/diffinery.git", href: "/docs/connect/codex" },
116 { icon: "link", title: "MCP server, in the official registry", text: "52 tools for code, pull requests, CI logs, sandboxes, Today and the docs, with a browser sign-in, for Claude, Claude Code, Codex, Cursor and any MCP client. Listed as com.diffinery/diffinery.", install: "https://<your host>/mcp", href: "/docs/agents/mcp" },
117 { icon: "play", title: "The diffinery CLI and gh", text: "Pull requests with intent, auto-merge, runs and logs, runners and migrations. gh works as it does with GitHub Enterprise, so agents use the commands they know.", install: "curl -fsSL https://diffinery.com/install | bash", href: "/docs/agents/github-cli" },
118 { icon: "repo", title: "VS Code, Cursor and Windsurf", text: "Clone from Diffinery with a passkey sign-in, and push and pull with it.", install: "https://diffinery.com/install-editor", href: "/docs/repositories/clone-in-your-editor" },
119 { icon: "package", title: "JetBrains IDEs", text: "Get from Version Control with a passkey sign-in, in IntelliJ IDEA, PyCharm, WebStorm and the rest (2024.3 or later).", install: "https://diffinery.com/-/jetbrains/updatePlugins.xml", href: "/docs/repositories/clone-in-jetbrains-ides" },
120 ],
121 workflow: `name: CI
122on: [push, pull_request]
123jobs:
124 test:
125 runs-on: ubuntu-latest # 4 vCPU, 12 GiB on Diffinery
126 steps:
127 - uses: actions/checkout@v4
128 - uses: actions/setup-node@v4
129 with: { node-version: 22, cache: pnpm }
130 - run: pnpm install --frozen-lockfile
131 - run: pnpm test`,
132 compare: {
133 cols: ["Diffinery", "GitHub", "GitHub with a runner service"],
134 rows: [
135 ["Git hosting, pull requests, issues", "Yes", "Yes", "Yes, on GitHub"],
136 ["Runs GitHub Actions workflows", "Yes, as they are", "Yes", "Yes, with a new runs-on label"],
137 ["Default Linux runner, private repositories", "4 vCPU, 12 GiB", "2 vCPU, 8 GB", "Your choice of size, billed by the vendor"],
138 ["Guardrails for AI agents", "Built in for every agent account", "Rulesets and permissions you configure", "Rulesets and permissions you configure"],
139 ["Built-in MCP server and agent sandboxes", "Yes", "MCP server, Codespaces", "GitHub's"],
140 ["Hosted macOS and Windows runners", "No (self-hosted)", "Yes", "Depends on the vendor"],
141 ["Open source", "Yes, Apache-2.0", "No", "No"],
142 ["Who runs it", "You, on your own Cloudflare account", "GitHub", "GitHub and the runner vendor"],
143 ],
144 },
145 faq: [
146 {
147 q: "What is Diffinery?",
148 a: "Diffinery is a git forge and CI platform for teams of people and AI agents. It hosts Git repositories, pull requests, issues and releases, runs GitHub Actions workflows on its own hosted runners, and gives AI agents their own accounts, tokens and server-enforced guardrails. It runs entirely on Cloudflare, and it's open source under the Apache License 2.0: you deploy it to your own Cloudflare account.",
149 },
150 {
151 q: "Is Diffinery an alternative to GitHub?",
152 a: "Yes, for teams whose work fits it: Diffinery replaces GitHub's code hosting, pull requests and GitHub Actions in one platform. It doesn't have GitHub's hosted macOS and Windows runners, forks, app marketplace, Codespaces, Pages or Discussions. See [Diffinery vs GitHub](/compare/github).",
153 },
154 {
155 q: "Do my GitHub Actions workflows run on Diffinery?",
156 a: "Most run without changes. Diffinery reads `.github/workflows/*.yml` with the same syntax, triggers, expressions and actions. Hosted runners are Linux x64; macOS, Windows and Arm jobs need a self-hosted runner. Every difference is listed in [Compatibility with GitHub Actions](/docs/actions/compatibility).",
157 },
158 {
159 q: "Is Diffinery built on Cloudflare?",
160 a: "Yes, entirely: Git hosting, pull requests with reviews in a web UI and GitHub Actions–compatible CI, marketplace actions included, all run on Cloudflare. One Cloudflare Worker serves the site, the API, Git and the MCP server; each repository is its own Durable Object, with large objects in R2 and accounts in D1; Workflows and Queues drive CI runs, webhooks and email; and every CI job runs in a Cloudflare Container that exists only while the job runs. There are no servers to manage. See [Built on Cloudflare](/built-on-cloudflare).",
161 },
162 {
163 q: "How is Diffinery different from Blacksmith or Depot?",
164 a: "Blacksmith sells faster runners for GitHub Actions, and Depot sells runners, its own CI engine and container builds. With either, your code and pull requests usually stay on GitHub and jobs run on their machines. Diffinery replaces GitHub and GitHub Actions together, with fast runners built in, as one open-source platform you run on your own Cloudflare account. See [Diffinery vs Blacksmith](/compare/blacksmith) and [Diffinery vs Depot](/compare/depot).",
165 },
166 {
167 q: "How does Diffinery work with Claude Code, Codex and other AI agents?",
168 a: "Agents connect over MCP at `/mcp` on your instance with a browser sign-in, or use Git, the GitHub CLI and the REST API with an agent token. Each agent is an account of its own: it can't push to protected branches, its pull requests need a person's approval, and runs of its code get no secrets. See [Diffinery for AI agents](/agents).",
169 },
170 {
171 q: "How do I move from GitHub?",
172 a: "Diffinery's importer copies a repository or a whole organization from GitHub on the server, with history, issues, pull requests, reviews, releases and branch protection, keeping their numbers. Then you enter your secrets' values and push. See [Migrate from GitHub](/docs/migrate-from-github).",
173 },
174 {
175 q: "Is Diffinery open source?",
176 a: "Yes, under the Apache License 2.0, with no paid edition and nothing sold. The source code is at [diffinery.com/codician/diffinery](https://diffinery.com/codician/diffinery) and mirrored to [github.com/codician-team/diffinery](https://github.com/codician-team/diffinery). See [Open source](/open-source).",
177 },
178 {
179 q: "Can I try Diffinery without deploying it?",
180 a: "Yes. [Request early access](https://diffinery.com/early-access) to diffinery.com, the project's own instance: its administrators review requests and email invitations, and you try Diffinery there. To run Diffinery for your team, deploy your own instance. See [Early access](/docs/accounts/early-access).",
181 },
182 {
183 q: "How do I start using Diffinery, and what does it cost?",
184 a: "Deploy your own instance to your Cloudflare account and domain with one command, `scripts/setup.sh`, then sign in as its first administrator and invite your team. See [Deploy your own Diffinery](/docs/self-hosting/deploy). Diffinery is free; you pay Cloudflare for what your instance uses, on the Workers Paid plan (a minimum of $5 USD per month per account, plus usage; [Cloudflare's pricing](https://developers.cloudflare.com/workers/platform/pricing/), checked October 25, 2026). There's no hosted service to sign up for; to try Diffinery first, [request early access](https://diffinery.com/early-access) to diffinery.com, the project's own instance.",
185 },
186 ],
187};
188
189/** The home page as Markdown, for agents: /index.md, and / with `Accept: text/markdown`. */
190export function homeMarkdown(o: string): string {
191 const lines = [
192 `# ${SITE.name}`,
193 "",
194 `> ${HOME.description}`,
195 "",
196 `Source: ${o}/ (last updated ${HOME_UPDATED})`,
197 "",
198 `## ${HOME.h1}`,
199 "",
200 HOME.lead,
201 "",
202 ...HOME.stats.map((s) => `- **${s.value}**: ${s.label}`),
203 "",
204 "## What you get",
205 "",
206 ...HOME.features.map((f) => `- **[${f.title}](${f.href})**: ${f.text}`),
207 "",
208 "## Built for AI coding agents",
209 "",
210 AGENTS_LEAD,
211 "",
212 ...HOME.agents.map((f) => `- **[${f.title}](${f.href})**: ${f.text}`),
213 "",
214 "## Works where you and your agents already work",
215 "",
216 ...HOME.integrations.map((f) => `- **[${f.title}](${f.href})**: ${f.text} \`${f.install}\``),
217 "",
218 "## Your workflows, as they are",
219 "",
220 "```yaml",
221 HOME.workflow,
222 "```",
223 "",
224 "## How Diffinery compares",
225 "",
226 `| | ${HOME.compare.cols.join(" | ")} |`,
227 `| --- | ${HOME.compare.cols.map(() => "---").join(" | ")} |`,
228 ...HOME.compare.rows.map((r) => `| ${r.join(" | ")} |`),
229 "",
230 "Runner services include Blacksmith and Depot. See [all comparisons](/compare).",
231 "",
232 "## Open source, on your own Cloudflare account",
233 "",
234 `Diffinery is open source under the Apache License 2.0: [source code](${SOURCE.repo}), [mirror on GitHub](${SOURCE.github}). Deploy it with [Deploy your own Diffinery](/docs/self-hosting/deploy); you pay Cloudflare only for what your instance uses. To try it first, [request early access](${EARLY_ACCESS}) to diffinery.com. See [Open source](/open-source).`,
235 "",
236 "## Frequently asked questions",
237 "",
238 ...HOME.faq.flatMap((f) => [`### ${f.q}`, "", f.a, ""]),
239 ];
240 return absolute(lines.join("\n"), o);
241}
242
243const AGENTS_LEAD =
244 "Claude Code, Codex and other agents work through Git, the GitHub CLI, the REST API or the built-in MCP server, as accounts of their own, and Diffinery can run them for you. Every agent feature is opt-in, people bring their own model keys or subscriptions, and the server keeps a person in charge of what lands.";
245
246/** Everything the home page says, for IndexNow (web/indexnow.ts): its Markdown and the landing page's own copy. */
247export async function homeContent(o: string): Promise<string> {
248 return `${homeMarkdown(o)}\n${await (<Landing />).toString()}`;
249}
250
251/**
252 * What sets Diffinery apart on Cloudflare, in the words every page uses (llms.txt, structured data). Checked against
253 * the other Git and CI projects on Cloudflare listed on /built-on-cloudflare; re-check them when this changes.
254 */
255export const CLOUDFLARE_CLAIM =
256 "an open-source platform where Git hosting, pull requests with reviews in a web UI and GitHub Actions–compatible CI, marketplace actions included, all run on Cloudflare (checked October 25, 2026).";
257
258/** Where Diffinery's source lives, whatever instance serves this page, and its license. */
259export const SOURCE = {
260 repo: "https://diffinery.com/codician/diffinery",
261 github: "https://github.com/codician-team/diffinery",
262 license: "https://www.apache.org/licenses/LICENSE-2.0",
263 spdx: "Apache-2.0",
264};
265
266/** Where to ask to try Diffinery on the project's own instance, whatever instance serves this page (web/early-access.tsx). */
267export const EARLY_ACCESS = "https://diffinery.com/early-access";
268
269/** schema.org ids for the site's entities, shared by every page's graph. */
270const ids = (o: string) => ({ org: `${o}/#organization`, site: `${o}/#website`, app: `${o}/#software`, source: `${o}/#source` });
271
272function entities(o: string): object[] {
273 const id = ids(o);
274 return [
275 { "@type": "Organization", "@id": id.org, name: SITE.name, url: `${o}/`, logo: { "@type": "ImageObject", url: `${o}/icon-512.png`, width: 512, height: 512 }, description: SITE.description },
276 { "@type": "WebSite", "@id": id.site, name: SITE.name, url: `${o}/`, inLanguage: "en", publisher: { "@id": id.org } },
277 {
278 "@type": "SoftwareApplication",
279 "@id": id.app,
280 name: SITE.name,
281 url: `${o}/`,
282 applicationCategory: "DeveloperApplication",
283 applicationSubCategory: "Git hosting and continuous integration",
284 operatingSystem: "Web",
285 description: HOME.lead,
286 featureList: [
287 ...HOME.features.map((f) => `${f.title}: ${f.text}`),
288 ...HOME.agents.map((f) => `${f.title}: ${f.text}`),
289 ...HOME.integrations.map((f) => `${f.title}: ${f.text}`),
290 `Runs entirely on Cloudflare: ${CLOUDFLARE_CLAIM}`, `Open source under the Apache License 2.0, deployed to your own Cloudflare account`,
291 ],
292 license: SOURCE.license,
293 isAccessibleForFree: true,
294 offers: { "@type": "Offer", price: "0", priceCurrency: "USD", description: "Free, open-source software. Your instance's Cloudflare usage is billed by Cloudflare." },
295 softwareHelp: { "@type": "CreativeWork", url: `${o}/docs/self-hosting/deploy` },
296 installUrl: `${o}/docs/self-hosting/deploy`,
297 downloadUrl: `${SOURCE.repo}/releases`,
298 publisher: { "@id": id.org },
299 sameAs: [`${o}/docs`, SOURCE.repo, SOURCE.github],
300 },
301 {
302 "@type": "SoftwareSourceCode",
303 "@id": id.source,
304 name: SITE.name,
305 description: `The source code of ${SITE.name}, which you deploy to your own Cloudflare account.`,
306 codeRepository: SOURCE.repo,
307 sameAs: [SOURCE.github],
308 license: SOURCE.license,
309 programmingLanguage: "TypeScript",
310 runtimePlatform: "Cloudflare Workers",
311 isAccessibleForFree: true,
312 targetProduct: { "@id": id.app },
313 publisher: { "@id": id.org },
314 },
315 ];
316}
317
318function faqPage(url: string, list: Array<{ q: string; a: string }>): object[] {
319 if (!list.length) return [];
320 return [{ "@type": "FAQPage", "@id": `${url}#faq`, url, mainEntity: list.map((f) => ({ "@type": "Question", name: f.q, acceptedAnswer: { "@type": "Answer", text: plainText(f.a) } })) }];
321}
322
323/**
324 * Structured data for a docs page (docs/web.tsx): a TechArticle in the site's graph, with breadcrumbs (Home, Docs, the
325 * page), the date it was last reviewed when its frontmatter says, and its questions and answers.
326 */
327export function docsJsonLd(o: string, p: { url: string; title: string; description: string; section: string; markdown: string; updated?: string; home?: boolean }): object[] {
328 const id = ids(o);
329 const trail = [
330 { name: "Home", item: `${o}/` },
331 { name: "Docs", item: `${o}/docs` },
332 ...(p.home ? [] : [{ name: p.title, item: p.url }]),
333 ];
334 return [
335 ...entities(o),
336 {
337 "@type": "TechArticle",
338 "@id": `${p.url}#page`,
339 url: p.url,
340 name: p.title,
341 headline: p.title,
342 description: p.description,
343 articleSection: p.section,
344 inLanguage: "en",
345 ...(p.updated ? { dateModified: p.updated } : {}),
346 author: { "@id": id.org },
347 publisher: { "@id": id.org },
348 isPartOf: { "@id": id.site },
349 about: { "@id": id.app },
350 breadcrumb: { "@id": `${p.url}#breadcrumb` },
351 },
352 { "@type": "BreadcrumbList", "@id": `${p.url}#breadcrumb`, itemListElement: trail.map((t, i) => ({ "@type": "ListItem", position: i + 1, name: t.name, item: t.item })) },
353 ...faqPage(p.url, faqs(p.markdown)),
354 ];
355}
356
357const Cta: FC = () => (
358 <>
359 <a class="btn primary" href="/docs/self-hosting">
360 Deploy your own
361 </a>
362 <a class="btn" href={EARLY_ACCESS} title="Try Diffinery on diffinery.com before you deploy your own">
363 Request early access
364 </a>
365 <a class="btn" href={SOURCE.repo}>
366 View the source
367 </a>
368 </>
369);
370
371/** Inline Markdown (links and code) of a home page answer, as HTML. */
372const inline = (md: string) => renderMarkdown(md, { idPrefix: "" }).replace(/^<p>|<\/p>\s*$/g, "");
373
374/** The home page for visitors. */
375export const Landing: FC = () => (
376 <div class="mk">
377 <style>{raw(MARKETING_CSS)}</style>
378 <section class="hero">
379 <a class="ea-eyebrow" href="/open-source">
380 {raw(ICONS.code)} Open source under Apache-2.0
381 </a>
382 <h1>{HOME.h1}</h1>
383 <p>{HOME.lead}</p>
384 <p class="row" style="justify-content:center">
385 <Cta />
386 </p>
387 <p class="muted small">
388 Free, with nothing to buy: you run Diffinery on your own Cloudflare account. Want to try it first? <a href={EARLY_ACCESS}>Request early access</a> to diffinery.com. Source on <a href={SOURCE.repo}>Diffinery</a> and <a href={SOURCE.github}>GitHub</a>.
389 </p>
390 </section>
391 <ul class="mk-stats" aria-label="Diffinery in numbers">
392 {HOME.stats.map((s) => (
393 <li>
394 <b>{s.value}</b>
395 <span>{s.label}</span>
396 </li>
397 ))}
398 </ul>
399 <section class="mk-sec">
400 <h2>Everything your team and its agents need, in one place</h2>
401 <div class="features">
402 {HOME.features.map((f) => (
403 <a class="box mk-card" href={f.href}>
404 <h3>
405 {raw(ICONS[f.icon as keyof typeof ICONS])} {f.title}
406 </h3>
407 <p class="muted m0">{f.text}</p>
408 </a>
409 ))}
410 </div>
411 </section>
412 <section class="mk-sec mk-split">
413 <div>
414 <h2>Your workflows, as they are</h2>
415 <p>Diffinery reads <code>.github/workflows</code> with the same triggers, expressions, matrices, reusable workflows, environments, secrets and OIDC as GitHub Actions. Common actions such as checkout, setup-node and cache are built into the runner, so they finish in seconds.</p>
416 <ul>
417 <li>Each job gets a fresh Ubuntu 24.04 container with 4 vCPUs and 12 GiB of memory.</li>
418 <li>Caches restore in parallel and extract while they download.</li>
419 <li>Self-hosted runners cover macOS, Windows, Arm and GPUs.</li>
420 </ul>
421 <p>
422 <a href="/performance">See the measurements</a> · <a href="/docs/actions/compatibility">Compatibility with GitHub Actions</a>
423 </p>
424 </div>
425 {/* One line: whitespace inside <pre> shows. */}
426 {raw(`<pre class="mk-code" aria-label="A GitHub Actions workflow that runs on Diffinery unchanged"><code>${escapeHtml(HOME.workflow)}</code></pre>`)}
427 </section>
428 <section class="mk-sec" id="agents">
429 <h2>Built for AI coding agents</h2>
430 <p class="mk-wide">{AGENTS_LEAD}</p>
431 <div class="features">
432 {HOME.agents.map((f) => (
433 <a class="box mk-card" href={f.href}>
434 <h3>
435 {raw(ICONS[f.icon as keyof typeof ICONS])} {f.title}
436 </h3>
437 <p class="muted m0">{f.text}</p>
438 </a>
439 ))}
440 </div>
441 <figure class="mk-shot">
442 <a href="/docs/agents/today">
443 <img src="/docs/images/today.webp" width="1280" height="1000" loading="lazy" alt="The Today page on Diffinery: what needs you, the agent pull requests at work, your agent tasks and what changed since yesterday" />
444 </a>
445 <figcaption class="muted small">Today, the first page after you sign in: an agent pull request waiting for a person, the agents at work and the task that opened it.</figcaption>
446 </figure>
447 <p>
448 <a href="/agents">Diffinery for AI agents</a> · <a href="/docs/connect">Connect Claude, Claude Code or Codex</a>
449 </p>
450 </section>
451 <section class="mk-sec" id="integrations">
452 <h2>Works where you and your agents already work</h2>
453 <p class="mk-wide">Plugins and extensions for the tools you use, all with a browser sign-in, for diffinery.com or your own instance.</p>
454 <div class="features">
455 {HOME.integrations.map((f) => (
456 <a class="box mk-card" href={f.href}>
457 <h3>
458 {raw(ICONS[f.icon as keyof typeof ICONS])} {f.title}
459 </h3>
460 <p class="muted">{f.text}</p>
461 <code class="mk-install">{f.install}</code>
462 </a>
463 ))}
464 </div>
465 <p>
466 <a href="/docs/connect">Connect AI apps</a> · <a href="/features#works-with-the-tools-you-already-use">Every integration</a>
467 </p>
468 </section>
469 <section class="mk-sec">
470 <h2>How Diffinery compares</h2>
471 <div class="table-wrap">
472 <table class="mk-table">
473 <thead>
474 <tr>
475 <th></th>
476 {HOME.compare.cols.map((h) => (
477 <th scope="col">{h}</th>
478 ))}
479 </tr>
480 </thead>
481 <tbody>
482 {HOME.compare.rows.map((r) => (
483 <tr>
484 <th scope="row">{r[0]}</th>
485 {r.slice(1).map((v) => (
486 <td>{v}</td>
487 ))}
488 </tr>
489 ))}
490 </tbody>
491 </table>
492 </div>
493 <p class="muted small">
494 Runner services include Blacksmith and Depot. GitHub's runner sizes are from its documentation, checked {longDate(HOME_UPDATED)}. Read the full comparisons with{" "}
495 <a href="/compare/github">GitHub</a>, <a href="/compare/gitlab">GitLab</a>, <a href="/compare/blacksmith">Blacksmith</a> and <a href="/compare/depot">Depot</a>.
496 </p>
497 </section>
498 <section class="mk-sec mk-faq" id="faq">
499 <h2>Frequently asked questions</h2>
500 {HOME.faq.map((f) => (
501 <div class="mk-q">
502 <h3>{f.q}</h3>
503 <p>{raw(inline(f.a))}</p>
504 </div>
505 ))}
506 </section>
507 <section class="mk-band">
508 <h2>Run Diffinery on your own Cloudflare account</h2>
509 <p class="muted">Open source under Apache-2.0. Your workflows, your editor and gh keep working, your agents get guardrails, and you pay Cloudflare only for what you use. Not ready to deploy? Request early access and try it on diffinery.com first.</p>
510 <p class="row" style="justify-content:center">
511 <Cta />
512 </p>
513 </section>
514 </div>
515);
516
517/** The home page for signed-out visitors (registerAccount calls this for "/"). */
518export function landingPage(c: AppContext): Response | Promise<Response> {
519 const o = origin(c);
520 if (wantsMarkdown(c)) return markdownResponse(c, homeMarkdown(o));
521 c.header("link", `<${o}/index.md>; rel="alternate"; type="text/markdown", <${o}/llms.txt>; rel="describedby"; type="text/plain"`);
522 c.header("vary", "Accept");
523 return render(
524 c,
525 {
526 title: "",
527 home: true,
528 seoTitle: HOME.title,
529 description: HOME.description,
530 jsonLd: [...entities(o), { "@type": "WebPage", "@id": `${o}/#webpage`, url: `${o}/`, name: HOME.title, description: HOME.description, isPartOf: { "@id": ids(o).site }, about: { "@id": ids(o).app }, dateModified: HOME_UPDATED, inLanguage: "en" }, ...faqPage(`${o}/`, HOME.faq)],
531 head: <link rel="alternate" type="text/markdown" href="/index.md" title="Diffinery (Markdown)" />,
532 },
533 <Landing />,
534 );
535}
536
537// ---------------------------------------------------------------- the Markdown pages (site/)
538
539const wantsMarkdown = (c: AppContext) => /\btext\/markdown\b/i.test(c.req.header("accept") ?? "");
540const PUBLIC = { "cache-control": "public, max-age=300", "access-control-allow-origin": "*", "x-content-type-options": "nosniff" };
541const markdownResponse = (c: AppContext, body: string) => c.body(body, 200, { ...PUBLIC, "content-type": "text/markdown; charset=utf-8", vary: "Accept" });
542
543const htmlCache = new Map<string, string>();
544async function pageHtml(c: AppContext, p: SitePageInfo): Promise<string> {
545 let html = htmlCache.get(p.path);
546 if (html === undefined) {
547 html = renderMarkdown(p.markdown, { idPrefix: "", highlight: await fenceHighlighter(fenceLangs(p.markdown)), image: imageProxy(c.env) });
548 htmlCache.set(p.path, html);
549 }
550 return html;
551}
552
553/** Blog posts: /blog/<post>, not the /blog index. */
554const isPost = (p: SitePageInfo) => p.path.startsWith("/blog/");
555
556/** Comparisons, measurements and blog posts are articles: dated, with sources. */
557const isArticle = (p: SitePageInfo) => p.path.startsWith("/compare/") || p.path === "/performance" || p.path === "/built-on-cloudflare" || isPost(p);
558
559function crumbs(p: SitePageInfo): Array<{ name: string; path: string }> {
560 const out = [{ name: "Home", path: "/" }];
561 if (p.path.startsWith("/compare/")) out.push({ name: "Compare", path: "/compare" });
562 if (isPost(p)) out.push({ name: "Blog", path: "/blog" });
563 out.push({ name: p.title, path: p.path });
564 return out;
565}
566
567async function sitePageResponse(c: AppContext, p: SitePageInfo): Promise<Response> {
568 const o = origin(c);
569 const url = `${o}${p.path}`;
570 const html = await pageHtml(c, p);
571 const id = ids(o);
572 const trail = crumbs(p);
573 const related = sitePages().filter((x) => x.section === p.section && x.path !== p.path);
574 c.header("link", `<${o}${p.path}.md>; rel="alternate"; type="text/markdown", <${o}/llms.txt>; rel="describedby"; type="text/plain"`);
575 c.header("vary", "Accept");
576 const page = {
577 "@type": isPost(p) ? "BlogPosting" : isArticle(p) ? "Article" : p.path === "/about" ? "AboutPage" : "WebPage",
578 "@id": `${url}#page`,
579 url,
580 name: p.title,
581 ...(isArticle(p) ? { headline: p.title } : {}),
582 description: p.description,
583 inLanguage: "en",
584 ...(isPost(p) ? { datePublished: p.published ?? p.updated } : {}),
585 dateModified: p.updated,
586 ...(isArticle(p) ? { author: { "@id": id.org } } : {}),
587 publisher: { "@id": id.org },
588 isPartOf: { "@id": id.site },
589 about: { "@id": id.app },
590 breadcrumb: { "@id": `${url}#breadcrumb` },
591 };
592 const breadcrumb = { "@type": "BreadcrumbList", "@id": `${url}#breadcrumb`, itemListElement: trail.map((t, i) => ({ "@type": "ListItem", position: i + 1, name: t.name, item: `${o}${t.path}` })) };
593 return render(
594 c,
595 {
596 title: p.title,
597 seoTitle: p.seoTitle ?? undefined,
598 description: p.description,
599 modified: isArticle(p) ? p.updated : undefined,
600 jsonLd: [...entities(o), page, breadcrumb, ...faqPage(url, faqs(p.markdown))],
601 head: <link rel="alternate" type="text/markdown" href={`${p.path}.md`} title={`${p.title} (Markdown)`} />,
602 },
603 <article class="mk mk-page">
604 <style>{raw(MARKETING_CSS)}</style>
605 <header class="mk-head">
606 <nav class="mk-crumbs" aria-label="Breadcrumb">
607 {trail.slice(0, -1).map((t) => (
608 <>
609 <a href={t.path}>{t.name}</a> <span class="faint">/</span>{" "}
610 </>
611 ))}
612 </nav>
613 <h1>{p.title}</h1>
614 <p class="mk-lead">{p.description}</p>
615 <p class="mk-meta muted small">
616 {isPost(p) && p.published && p.published !== p.updated ? (
617 <>
618 Published <time datetime={p.published}>{longDate(p.published)}</time> ·{" "}
619 </>
620 ) : null}
621 Last updated <time datetime={p.updated}>{longDate(p.updated)}</time> ·{" "}
622 <a href={`${p.path}.md`} title="This page as plain Markdown">
623 View as Markdown
624 </a>
625 </p>
626 </header>
627 <div class="markdown mk-body">{raw(html)}</div>
628 <aside class="mk-band">
629 <h2>Run Diffinery yourself</h2>
630 <p class="muted">Open source under Apache-2.0, deployed to your own Cloudflare account. Or request early access and try it on diffinery.com first.</p>
631 <p class="row" style="justify-content:center">
632 <Cta />
633 </p>
634 </aside>
635 {related.length ? (
636 <nav class="mk-related" aria-label={`More in ${p.section}`}>
637 <h2>More in {p.section}</h2>
638 <ul>
639 {related.map((r) => (
640 <li>
641 <a href={r.path}>{r.title}</a>
642 <span class="muted small">{r.description}</span>
643 </li>
644 ))}
645 </ul>
646 </nav>
647 ) : null}
648 </article>,
649 );
650}
651
652/** The sitemap's entries for the site: the home page first, then every page with the date it was last checked. */
653export function siteSitemapEntries(): Array<{ path: string; updated: string }> {
654 return [{ path: "/", updated: HOME_UPDATED }, ...sitePages().map((p) => ({ path: p.path, updated: p.updated }))];
655}
656
657/** The site's part of /llms.txt: what Diffinery is, then every site page as Markdown. */
658export function siteLlmsLines(o: string): string[] {
659 const lines = [
660 "## About Diffinery",
661 "",
662 `- [Home page](${o}/index.md): ${HOME.description}`,
663 `- Open source under the Apache License 2.0 (SPDX: ${SOURCE.spdx}). Source code: ${SOURCE.repo}, mirrored to ${SOURCE.github}. Free, with no paid edition and nothing sold.`,
664 `- How to use it: deploy your own instance to your own Cloudflare account and domain (Workers Paid plan); you pay Cloudflare for what it uses. Guide: ${o}/docs/self-hosting/deploy.md. Overview: ${o}/docs/self-hosting.md. There's no hosted service for teams to sign up for: diffinery.com is the project's home and its own instance, with accounts by invitation. To try Diffinery before deploying your own, request early access at ${EARLY_ACCESS} (how it works: ${o}/docs/accounts/early-access.md).`,
665 `- Runs entirely on Cloudflare (Workers, Durable Objects, R2, D1, Queues, Workflows, Containers): code, pull requests, CI runners and the agent features alike. It is ${CLOUDFLARE_CLAIM} See ${o}/built-on-cloudflare.md, which compares the other Git and CI projects on Cloudflare.`,
666 "- Hosted CI runners: Ubuntu 24.04 x64 containers, 4 vCPU and 12 GiB of memory by default, with smaller sizes chosen per job from its measured runs. No hosted macOS, Windows or Arm runners; self-hosted runners cover them.",
667 "- Runs GitHub Actions workflow files from `.github/workflows/` as they are; differences are listed at " + `${o}/docs/actions/compatibility.md`,
668 `- Built for AI coding agents (${o}/agents.md): agent accounts with server-enforced guardrails (${o}/docs/agents/overview.md); agent tasks that run Claude Code or Codex in a sandbox with the person's own model key and open a pull request (${o}/docs/agents/tasks.md); agent review with inline findings and an Agent review check (${o}/docs/agents/review.md); a Today page of what needs you (${o}/docs/agents/today.md); merge on green (${o}/docs/collaboration/merge-on-green.md); browser terminals where Claude Code or Codex runs on the person's own Claude or ChatGPT plan (${o}/docs/agents/terminal.md). All opt-in; Diffinery doesn't sell or include model access.`,
669 `- Plugins and integrations: Claude Code and Codex plugins (marketplace codician-team/diffinery on GitHub, or ${SOURCE.repo}.git), an MCP server listed in the official MCP Registry as com.diffinery/diffinery (https://{diffinery_host}/mcp), the diffinery CLI, gh through the GitHub-compatible REST and GraphQL APIs, and extensions for VS Code, Cursor, Windsurf and JetBrains IDEs. Guides: ${o}/docs/connect.md`,
670 "",
671 ];
672 for (const s of SITE_NAV) {
673 lines.push(`## ${s.title}`, "");
674 for (const path of s.pages) {
675 const p = sitePage(path)!;
676 lines.push(`- [${p.title}](${o}${path}.md): ${p.description} (updated ${p.updated})`);
677 }
678 lines.push("");
679 }
680 return lines;
681}
682
683/** The site's pages for /llms-full.txt, home page first. */
684export function siteFullMarkdown(o: string): string[] {
685 return [homeMarkdown(o), ...sitePages().map((p) => sitePageMarkdown(p, o))];
686}
687
688export function registerSite(web: WebApp): void {
689 web.get("/index.md", (c) => markdownResponse(c, homeMarkdown(origin(c))));
690 for (const p of sitePages()) {
691 web.get(`${p.path}.md`, (c) => markdownResponse(c, sitePageMarkdown(p, origin(c))));
692 web.get(`${p.path}/`, (c) => c.redirect(p.path, 301));
693 web.get(p.path, (c) => (wantsMarkdown(c) ? markdownResponse(c, sitePageMarkdown(p, origin(c))) : sitePageResponse(c, p)));
694 }
695}
696
697export const MARKETING_CSS = `
698.mk .hero h1{max-width:820px;margin-left:auto;margin-right:auto;font-size:48px;line-height:1.08}
699.mk .hero>p:first-of-type{max-width:720px}
700.mk-stats{list-style:none;margin:8px 0 8px;padding:0;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
701.mk-stats li{border:1px solid var(--border);border-radius:var(--r);background:var(--bg);padding:16px}
702.mk-stats b{display:block;font-size:24px;letter-spacing:-.02em;margin-bottom:4px}
703.mk-stats span{color:var(--fg2);font-size:13.5px;line-height:1.45}
704.mk-sec{margin:56px 0 0}
705.mk-sec>h2,.mk-band h2,.mk-related h2{font-size:26px;letter-spacing:-.02em;margin:0 0 10px}
706.mk-wide{max-width:760px;color:var(--fg2);font-size:16px}
707.mk .features{grid-template-columns:repeat(3,minmax(0,1fr))}
708.mk-card{display:block;color:var(--fg)}a.mk-card:hover{border-color:var(--accent);text-decoration:none}
709.mk-card h3{display:flex;align-items:center;gap:8px}.mk-card h3 svg{flex:none;color:var(--accent)}
710.mk-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:32px;align-items:center}
711.mk-split>*{min-width:0}
712.mk .table-wrap{overflow:auto}
713.mk-split p,.mk-split li{color:var(--fg2);font-size:15px;line-height:1.65}
714.mk-code{margin:0;padding:18px 20px;background:var(--code-bg);border:1px solid var(--border);border-radius:var(--r);overflow:auto;font-size:13px;line-height:1.6}
715.mk-table{width:100%;border-collapse:collapse;font-size:14px}
716.mk-table th,.mk-table td{border-bottom:1px solid var(--border2);padding:10px 12px;text-align:left;vertical-align:top}
717.mk-table thead th{font-weight:600;border-bottom:1px solid var(--border)}.mk-table thead th:nth-child(2){color:var(--accent)}
718.mk-table tbody th{font-weight:500;color:var(--fg2)}
719.mk-faq .mk-q{border-top:1px solid var(--border2);padding:14px 0}.mk-faq h3{font-size:16px;margin:0 0 6px}.mk-faq p{margin:0;color:var(--fg2);line-height:1.65;max-width:860px}
720.mk-install{display:block;font-size:12px;overflow-wrap:anywhere;margin-top:auto}
721.mk-card:has(.mk-install){display:flex;flex-direction:column}
722.mk-shot{margin:20px 0 12px}.mk-shot img{display:block;width:100%;height:auto;border:1px solid var(--border);border-radius:var(--r)}.mk-shot figcaption{margin-top:8px}
723.mk-band{margin:56px 0 0;padding:36px 20px;text-align:center;border:1px solid var(--border);border-radius:var(--r);background:var(--bg2)}
724.mk-band p{margin:0 auto 14px;max-width:620px}
725.mk-page{max-width:880px;margin:0 auto}
726.mk-head{padding:28px 0 18px;border-bottom:1px solid var(--border2);margin-bottom:8px}
727.mk-head h1{font-size:36px;line-height:1.15;letter-spacing:-.025em;margin:8px 0 10px}
728.mk-crumbs{font-size:13px;min-height:18px}
729.mk-lead{font-size:18px;color:var(--fg2);margin:0 0 10px;line-height:1.55}
730.mk-meta{margin:0}
731.mk-body{font-size:15.5px;line-height:1.7}
732.mk-body h2{font-size:23px;margin-top:38px;scroll-margin-top:64px}.mk-body h3{font-size:17px;margin-top:26px;scroll-margin-top:64px}
733.mk-body table{font-size:14px;width:100%}.mk-body td code{white-space:nowrap}
734.mk-related{margin:40px 0 0}
735.mk-related ul{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px}
736.mk-related li{border:1px solid var(--border);border-radius:var(--r);padding:12px 14px;display:flex;flex-direction:column;gap:4px}
737@media (max-width:900px){.mk .features{grid-template-columns:repeat(2,minmax(0,1fr))}.mk-stats{grid-template-columns:repeat(2,minmax(0,1fr))}.mk-split{grid-template-columns:minmax(0,1fr)}}
738@media (max-width:600px){.mk .features{grid-template-columns:minmax(0,1fr)}.mk .hero{padding-top:40px}.mk .hero h1{font-size:32px}.mk-head h1{font-size:27px}.mk-stats{grid-template-columns:minmax(0,1fr)}.mk-sec>h2,.mk-band h2{font-size:22px}}
739`;