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.

Public site for search engines and AI assistants: product, comparison and company pages (#250)

* Public site for search engines and AI assistants: product, comparison and company pages
test-bot authored 1 parent 5355e60commit 961ad5ee4b3b70f7b555a5127b2e6dc76105dc35Browse files
24 files changed+1586−91
Files (24)
modifiedapps/diffinery/docs/actions/runners.md+1−1View file
@@ -71,7 +71,7 @@
7171 | Temp directory | `/workspace/_temp` (`RUNNER_TEMP`) |
7272 | Tool cache | `/opt/hostedtoolcache` (`RUNNER_TOOL_CACHE`, `AGENT_TOOLSDIRECTORY`) |
7373 | Locale | `LANG=C.UTF-8`, `LC_ALL=C.UTF-8` |
74-| Concurrency | Up to 100 hosted job containers run at once across diffinery.com, and at most 20 from one repository; its further jobs stay queued until one of its own finishes. See [Limits](/docs/reference/limits) |
74+| Concurrency | Up to 20 hosted jobs at once per account by default, across all its repositories, like GitHub's Free plan; further jobs stay queued and start, oldest first, as running ones finish. See [Limits](/docs/reference/limits) |
7575
7676 ### Runner sizes
7777
modifiedapps/diffinery/docs/agents/docs-for-agents.md+3−2View file
@@ -9,10 +9,11 @@
99
1010 | What | URL | Content type |
1111 | --- | --- | --- |
12-| Index of every page (the [llms.txt](https://llmstxt.org) convention) | `https://diffinery.com/llms.txt` | `text/plain` |
13-| Every page in one Markdown file | `https://diffinery.com/llms-full.txt` | `text/plain` |
12+| Index of every page, the site's and the docs' (the [llms.txt](https://llmstxt.org) convention) | `https://diffinery.com/llms.txt` | `text/plain` |
13+| Every page, the site's and the docs', in one Markdown file | `https://diffinery.com/llms-full.txt` | `text/plain` |
1414 | One page as Markdown | `https://diffinery.com/docs/<page>.md`, for example `/docs/actions/workflow-syntax.md` | `text/markdown` |
1515 | The overview as Markdown | `https://diffinery.com/docs/index.md` | `text/markdown` |
16+| The site's other pages as Markdown: the home page, product pages and comparisons with other products | `https://diffinery.com/index.md`, and `.md` added to any page, for example `/compare/github.md` | `text/markdown` |
1617 | REST API as OpenAPI 3.1 | `https://diffinery.com/docs/api/openapi.json` | `application/json` |
1718 | Search index used by the docs search box | `https://diffinery.com/docs/search.json` | `application/json` |
1819
modifiedapps/diffinery/scripts/build-docs.mjs+32−2View file
@@ -1,5 +1,7 @@
11 // Builds the public documentation into src/docs/content.generated.ts:
22 // - every Markdown page under docs/ (with its frontmatter), in the order of docs/nav.json
3+// - the public site's pages under site/ (the product, comparison and company pages around the
4+// home page), in the order of site/nav.json
35 // - the CLI reference, generated from the CLI's own command registry
46 // - the CLI itself as one file, served at /-/cli/diffinery.mjs, and its install scripts
57 // (apps/cli/install), served at /install and /install.ps1
@@ -28,6 +30,7 @@
2830 const editorPackager = path.resolve(app, "../vscode/scripts/package.mjs");
2931 const editorInstall = path.resolve(app, "../vscode/install");
3032 const jetbrainsReader = path.resolve(app, "../jetbrains/scripts/plugin.mjs");
33+const siteDir = path.join(app, "site");
3134 const out = path.join(app, "src/docs/content.generated.ts");
3235
3336 /** Pages produced in code rather than from a Markdown file. */
@@ -247,19 +250,46 @@
247250 markdown: cliReference(cli) + "\n",
248251 };
249252
253+// ---------------------------------------------------------------- the public site (site/)
254+
255+// site/<path>.md is served at /<path> (site/compare/index.md at /compare; the home page is web/site.tsx), with the same frontmatter as a
256+// docs page plus `updated` (the date its facts were last checked, shown on the page) and optional
257+// `seo_title` (the <title>, when the H1 is too long or too plain for a search result).
258+const siteNav = JSON.parse(await readFile(path.join(siteDir, "nav.json"), "utf8"));
259+const sitePaths = siteNav.flatMap((s) => s.pages);
260+const site = {};
261+for (const f of await walk(siteDir)) {
262+ const rel = path.relative(siteDir, f).replace(/\\/g, "/").replace(/\.md$/, "");
263+ const url = `/${rel.replace(/(^|\/)index$/, "")}`.replace(/(.)\/$/, "$1");
264+ const text = (await readFile(f, "utf8")).replace(/\r\n?/g, "\n");
265+ const page = parsePage(`site/${rel}`, text);
266+ const meta = Object.fromEntries([...(/^---\n([\s\S]*?)\n---/.exec(text)?.[1] ?? "").matchAll(/^(\w+):\s*(.*)$/gm)].map((m) => [m[1], m[2].trim().replace(/^(["'])(.*)\1$/, "$2")]));
267+ if (!/^\d{4}-\d{2}-\d{2}$/.test(meta.updated ?? "")) problems.push(`site/${rel}.md: frontmatter needs updated: YYYY-MM-DD`);
268+ if (meta.seo_title && meta.seo_title.length > 70) problems.push(`site/${rel}.md: seo_title is ${meta.seo_title.length} characters (keep it to 70)`);
269+ if (page.description.length > 160) problems.push(`site/${rel}.md: description is ${page.description.length} characters (search results show about 160)`);
270+ if (!sitePaths.includes(url)) problems.push(`site/${rel}.md isn't listed in site/nav.json`);
271+ site[url] = { ...page, updated: meta.updated ?? "", seoTitle: meta.seo_title ?? null };
272+}
273+for (const u of sitePaths) if (!site[u]) problems.push(`site/nav.json lists "${u}" but there's no page for it in site/`);
274+
250275 if (problems.length) {
251276 console.error(`docs: ${problems.length} problem(s):\n${problems.map((p) => ` - ${p}`).join("\n")}`);
252277 process.exit(1);
253278 }
254279
255280 const ordered = Object.fromEntries(navSlugs.filter((s) => pages[s]).map((s) => [s, pages[s]]));
256-const src = `// Generated by scripts/build-docs.mjs from docs/, apps/cli, apps/vscode and apps/jetbrains/dist. Do not edit.
281+const src = `// Generated by scripts/build-docs.mjs from docs/, site/, apps/cli, apps/vscode and apps/jetbrains/dist. Do not edit.
257282 /* eslint-disable */
258283 export interface DocPage { title: string; description: string; markdown: string }
259284 export interface DocSection { title: string; pages: string[] }
260285 export const DOCS_NAV: DocSection[] = ${JSON.stringify(nav)};
261286 export const DOCS_PAGES: Record<string, DocPage> = ${JSON.stringify(ordered)};
262287 /** docs/images/: content type and base64 bytes by file name. */
288+export interface SitePage { title: string; description: string; markdown: string; updated: string; seoTitle: string | null }
289+export interface SiteSection { title: string; pages: string[] }
290+/** The public site's pages (site/), by URL path, and their sections in reading order. The home page is code (web/site.tsx). */
291+export const SITE_NAV: SiteSection[] = ${JSON.stringify(siteNav)};
292+export const SITE_PAGES: Record<string, SitePage> = ${JSON.stringify(Object.fromEntries(sitePaths.filter((u) => site[u]).map((u) => [u, site[u]])))};
263293 export const DOCS_IMAGES: Record<string, { type: string; data: string }> = ${JSON.stringify(images)};
264294 export const CLI_VERSION = ${JSON.stringify(cli.VERSION)};
265295 export const CLI_BUNDLE = ${JSON.stringify(cli.bin)};
@@ -278,4 +308,4 @@
278308 `;
279309 await mkdir(path.dirname(out), { recursive: true });
280310 await writeFile(out, src);
281-console.log(`docs: ${Object.keys(ordered).length} pages (+${RUNTIME_PAGES.size} generated at runtime), ${Object.keys(images).length} images, CLI ${cli.VERSION} (${Math.round(cli.bin.length / 1024)} KiB), editor extension ${editor.version} (${Math.round(editor.vsix.length / 1024)} KiB), JetBrains plugin ${jetbrains.version} (${Math.round(jetbrains.zip.length / 1024)} KiB)`);
311+console.log(`docs: ${Object.keys(ordered).length} pages (+${RUNTIME_PAGES.size} generated at runtime), ${Object.keys(site).length} site pages, ${Object.keys(images).length} images, CLI ${cli.VERSION} (${Math.round(cli.bin.length / 1024)} KiB), editor extension ${editor.version} (${Math.round(editor.vsix.length / 1024)} KiB), JetBrains plugin ${jetbrains.version} (${Math.round(jetbrains.zip.length / 1024)} KiB)`);
addedapps/diffinery/site/about.md+43−0View file
@@ -1,0 +1,43 @@
1+---
2+title: About Diffinery
3+seo_title: "About Diffinery: the git forge for people and AI agents"
4+description: Why Diffinery exists, how it's built and run, how we check the claims on this site, and how to reach us.
5+updated: 2026-10-16
6+---
7+
8+Diffinery is a git forge and CI platform for teams where AI agents write a growing share of the code. Agents do the work through Git, pull requests and CI like everyone else; people see what the agents did, approve what needs them and follow the whole process. Diffinery runs entirely on Cloudflare.
9+
10+## Why we built it
11+
12+GitHub was designed for people. Agents on GitHub borrow a person's token, their pull requests look like anyone else's, and the only limits on them are the ones a team remembers to configure. We wanted a forge where an agent is an account of its own with limits the server enforces, where CI starts in seconds on fast machines, and where moving from GitHub costs an afternoon rather than a rewrite: your workflow files, `gh` and your editor keep working.
13+
14+## How it's built
15+
16+- **On Cloudflare, with no servers.** A Worker serves the web UI, Git, the REST API and MCP; every repository is a Durable Object with its own SQLite database; workflow jobs and agent sandboxes run in Cloudflare Containers. See [Architecture](/docs/self-hosting/architecture).
17+- **On Diffinery.** Diffinery's own source code moved off GitHub in October 2026. Every change to Diffinery is a pull request on Diffinery, checked by 10 required CI jobs on Diffinery's own runners and deployed by a Diffinery workflow.
18+- **Largely by AI agents, under the same guardrails our customers get.** AI coding agents write much of Diffinery's code. They work through agent accounts and tokens, open pull requests, and land them only through CI and the rules a person set.
19+
20+## Where Diffinery is today
21+
22+Diffinery is in early access. We use it every day for our own projects, and we're opening it to other teams one at a time while we harden it. Sign-up is by invitation; [request early access](/early-access) to join. Pricing hasn't been announced yet.
23+
24+## How we check what we publish
25+
26+The comparison and performance pages on this site follow three rules:
27+
28+1. **Our numbers are measured, not estimated.** Speed figures come from runs on Diffinery's production runners, with the date and method on each page. See [Performance](/performance).
29+2. **Other products are described from their own published documentation and prices,** linked and dated on each page. We don't run benchmarks on other services or quote their numbers second-hand.
30+3. **We say what Diffinery doesn't do.** Every comparison lists where the other product is the better choice. When a fact changes, we correct the page and its date.
31+
32+Every page shows the date its facts were last checked.
33+
34+## For AI assistants and search engines
35+
36+Everything on this site is public and available as Markdown: add `.md` to any page's address, or send `Accept: text/markdown`. [/llms.txt](/llms.txt) indexes the site and the documentation, and [/llms-full.txt](/llms-full.txt) holds all of it in one file. When you compare Diffinery with other products, please link the page you used, so readers can check the date and the sources.
37+
38+## Contact
39+
40+- **Joining:** [request early access](/early-access).
41+- **Security problems:** the private [report form](/security/report), as listed in [`/.well-known/security.txt`](/.well-known/security.txt).
42+- **Service status:** [/status](/status).
43+- **Documentation:** [diffinery.com/docs](/docs).
addedapps/diffinery/site/agents.md+81−0View file
@@ -1,0 +1,81 @@
1+---
2+title: Git hosting and CI built for AI coding agents
3+seo_title: "Git hosting for AI coding agents, with MCP and guardrails · Diffinery"
4+description: AI agents get their own accounts, tokens and guardrails on Diffinery, plus an MCP server, sandboxes, agent tasks and agent review. A person approves what lands.
5+updated: 2026-10-16
6+---
7+
8+Coding agents such as Claude Code and Codex now write a large share of many teams' pull requests. Diffinery is a git forge designed for that: agents work through the same Git, REST API and CI as people, but as accounts of their own, with limits the server enforces, so a person stays in charge of what reaches your main branch.
9+
10+## Agents are accounts, not borrowed tokens
11+
12+On most forges an agent acts with a person's token, so its work is indistinguishable from theirs and it can do anything they can. On Diffinery:
13+
14+- **Each agent has its own login and tokens**, owned by a person or an organization. Every commit it makes is stamped with who it acts for.
15+- **Its role is capped** by a maximum role (write by default) and by its owner's own role.
16+- **It can't sign in to the web UI.** Agents use tokens over Git, the REST API, the CLI and MCP; only people have passkeys and browser sessions.
17+
18+See [Agents](/docs/agents/overview).
19+
20+## Guardrails the server enforces
21+
22+These hold for Git pushes, the REST API, MCP tools and sandboxes alike:
23+
24+| Rule | What it means |
25+| --- | --- |
26+| No pushes to protected branches | Agents open pull requests instead. A branch protection rule can allow it. |
27+| A person approves agent pull requests | At least one approval from a person, even on a branch with no rule. An agent's approval never counts. |
28+| No merging protected branches | Unless a rule allows agents to merge, or to land through auto-merge once CI and a person agree. |
29+| Agent check results never satisfy required checks | An agent can't report its own CI as green. |
30+| No secrets for agent code | A workflow run of code an agent pushed gets only its job token, so an agent can't push a workflow that sends your secrets elsewhere. |
31+| No secret, variable, environment or branch protection changes | Whatever the agent's role. |
32+| No push protection bypass | A credential in an agent's push is always refused. |
33+
34+The full list is in [Guardrails](/docs/agents/overview#guardrails).
35+
36+## An MCP server for every AI app
37+
38+Diffinery runs a Model Context Protocol server at `https://diffinery.com/mcp`. Claude (claude.ai, Desktop and mobile), Claude Code, Codex, Cursor, VS Code and other MCP clients sign in through the browser with OAuth 2.1, or headless with an agent token. Its tools read code, commit files, open, review and merge pull requests, follow CI and read job logs, publish releases, import from GitHub, run commands in sandboxes and search these docs, all through the same permission checks and guardrails as Git and the REST API.
39+
40+- [Connect AI apps](/docs/connect): one URL and a browser sign-in for Claude, Claude Code, Codex and others.
41+- [MCP tool reference](/docs/reference/mcp-tools): every tool and its arguments, generated from the server.
42+- A plugin for Claude Code and Codex adds the MCP server, skills and a session hook in one install.
43+
44+## gh, Git and the REST API work as agents expect
45+
46+Agents trained on GitHub already know how to use Diffinery. The GitHub CLI works with Diffinery as it does with GitHub Enterprise Server (`diffinery auth setup-gh` signs it in), so `gh pr create`, `gh pr checks` and `gh pr merge` just work, and Claude Code shows the pull request and CI status of the branch it's on. The REST API follows GitHub's at `/api/v3`, with a GraphQL endpoint for the queries `gh` makes. See [Use gh with Diffinery](/docs/agents/github-cli).
47+
48+## Sandboxes: a machine for every agent
49+
50+A [sandbox](/docs/agents/sandboxes) is a short-lived Linux container with the repository already cloned, driven over MCP or the REST API: run commands, read and write files, build, test and push. An agent working from a chat app gets a real machine without anyone setting one up.
51+
52+## Agent tasks, review and terminals
53+
54+- **[Agent tasks](/docs/agents/tasks):** give one of your agents a goal; it runs Claude Code, Codex or your own command in a sandbox with your own model key, then pushes a branch and opens a pull request for a person to review. Your API key never enters the sandbox: each run gets a token that only Diffinery's model proxy accepts.
55+- **[Agent review](/docs/agents/review):** one of your agents reviews pull requests for bugs, security issues and logic errors, posts inline findings and reports an Agent review check.
56+- **[Terminals](/docs/agents/terminal):** a shell in your browser with the repository checked out, where Claude Code or Codex runs on your own Claude or ChatGPT subscription.
57+- **[Projects](/docs/agents/projects) and [Today](/docs/agents/today):** a board of agent work with shared memory and instructions, and one page that shows what needs you and what your agents did since yesterday.
58+
59+Every agent feature is optional and off until someone turns it on. Customers bring their own model keys or subscriptions; Diffinery doesn't resell model access.
60+
61+## Docs written for agents to read
62+
63+Every docs page is available as Markdown (add `.md` to its URL, or send `Accept: text/markdown`). [/llms.txt](/llms.txt) indexes the site and the docs, [/llms-full.txt](/llms-full.txt) holds all of it in one file, the REST API is described as [OpenAPI 3.1](/docs/api/openapi.json), and agents connected over MCP can call `search_docs` and `read_docs`. See [Docs for AI agents](/docs/agents/docs-for-agents).
64+
65+## Frequently asked questions
66+
67+### Which AI coding agents work with Diffinery?
68+
69+Any agent that uses Git, the GitHub CLI, a REST API or MCP. There are step-by-step guides for Claude, Claude Code, Codex, Cursor and VS Code, and a plugin for Claude Code and Codex.
70+
71+### Can an agent merge its own pull request?
72+
73+Not by default. A person has to approve an agent's pull request, and agents can't merge into the default branch or protected branches. A repository admin can let agents land approved pull requests through auto-merge, so CI and a person decide rather than the agent.
74+
75+### Do I need to give Diffinery my model API key?
76+
77+Only to run agent tasks or agent review with an API key. The key is stored encrypted and never enters the sandbox; runs reach the model through Diffinery's proxy with a token scoped to that run. Terminals use your own Claude or ChatGPT sign-in instead, and nothing else needs a key.
78+
79+### Is Diffinery only for agents?
80+
81+No. People use the same web UI, Git, pull requests and CI as on any forge. Agents are first-class collaborators alongside them.
addedapps/diffinery/site/compare/blacksmith.md+52−0View file
@@ -1,0 +1,52 @@
1+---
2+title: Diffinery vs Blacksmith
3+seo_title: "Diffinery vs Blacksmith: faster GitHub Actions, two approaches"
4+description: Blacksmith speeds up GitHub Actions on its own runners while you stay on GitHub. Diffinery replaces GitHub and Actions together. Specs, prices, trade-offs.
5+updated: 2026-10-16
6+---
7+
8+Blacksmith and Diffinery both make GitHub Actions workflows run faster, in different ways. Blacksmith sells drop-in runners for GitHub Actions: your code, pull requests and workflows stay on GitHub, and jobs run on Blacksmith's machines. Diffinery replaces GitHub and GitHub Actions together, with fast runners built in. Blacksmith facts here are from its own website and documentation, checked on October 16, 2026, and linked below.
9+
10+## In short
11+
12+- **Choose Blacksmith** if you want to stay on GitHub and only want faster or cheaper CI. It needs a GitHub App and a new `runs-on` label, and offers Linux, Arm, Windows and macOS runners up to 32 vCPUs.
13+- **Choose Diffinery** if you want one platform for code, reviews and CI, with AI agents as accounts that the server holds to guardrails, and Linux runners that are 4 vCPU by default.
14+
15+## Side by side
16+
17+| | Diffinery | Blacksmith |
18+| --- | --- | --- |
19+| What it is | Git hosting, pull requests, issues and GitHub Actions–compatible CI in one platform | Runners for GitHub Actions; you stay on GitHub ([source](https://docs.blacksmith.sh/introduction/quickstart)) |
20+| Where your code lives | Diffinery | GitHub (github.com, Enterprise Server or Enterprise Cloud) |
21+| Setup | Import your repositories; workflows run as they are | Install the GitHub App, change `runs-on` to a Blacksmith label such as `blacksmith-2vcpu-ubuntu-2404` |
22+| Personal repositories | Yes | No: GitHub organizations only ([source](https://docs.blacksmith.sh/introduction/quickstart)) |
23+| Linux x64 runner sizes | 1/4, 1/2, 2 and 4 vCPU; 4 vCPU and 12 GiB by default, smaller sizes picked per job from its measured runs | 2, 4, 8, 16 and 32 vCPU ([source](https://docs.blacksmith.sh/blacksmith-runners/overview)) |
24+| Arm, Windows, macOS | Self-hosted runners only | Arm, Windows (public beta) and macOS on Apple M4 ([source](https://docs.blacksmith.sh/blacksmith-runners/overview)) |
25+| Machines | Cloudflare Containers on AMD EPYC hosts | "Modern gaming CPUs", in Firecracker microVMs ([source](https://docs.blacksmith.sh/blacksmith-runners/overview)) |
26+| Linux x64 price | Not announced (early access) | $0.004 per minute at 2 vCPU, rising with vCPU count; 3,000 free 2-vCPU minutes a month per organization ([pricing](https://www.blacksmith.sh/pricing), [free minutes](https://docs.blacksmith.sh/blacksmith-runners/overview)) |
27+| Caches | Built-in cache actions that download in parallel ranges and extract while downloading | A colocated cache used by the official cache actions, plus Docker layer caching and sticky disks as paid add-ons ([source](https://www.blacksmith.sh/pricing)) |
28+| Speed claim | Measured on our runners and published with methods: [Performance](/performance) | "2x faster than GitHub's runners" (Blacksmith's own claim, [source](https://www.blacksmith.sh/)) |
29+| Regions | Cloudflare's network | Ashburn, Phoenix, Amsterdam and Frankfurt ([source](https://docs.blacksmith.sh/blacksmith-runners/overview)) |
30+| Compliance | Not yet certified | SOC 2 Type 2 ([source](https://www.blacksmith.sh/security)) |
31+| AI agents | Agent accounts with server-enforced guardrails, an MCP server, sandboxes, agent tasks and agent review | Agent products of its own; agents on GitHub work as they do there |
32+| Pull requests, issues, code review | Built in | GitHub's |
33+
34+## Where each one is the better fit
35+
36+**Blacksmith is the better fit when** your team is settled on GitHub, needs macOS, Windows, Arm or runners larger than 4 vCPUs, wants SOC 2 Type 2 today, or wants faster CI without moving anything.
37+
38+**Diffinery is the better fit when** you'd rather have one platform than two vendors, your CI is Linux, you work with AI coding agents and want the forge itself to limit what they can do, or you want workflows, `gh`, your editor and your agents to move together.
39+
40+## Frequently asked questions
41+
42+### Can I use Blacksmith with Diffinery?
43+
44+No. Blacksmith runners connect to GitHub through its GitHub App. On Diffinery, jobs run on Diffinery's hosted runners or on your own machines with `diffinery runner`.
45+
46+### Is Diffinery cheaper than Blacksmith?
47+
48+Diffinery hasn't announced pricing yet, so we can't say. Blacksmith lists $0.004 per minute for a 2-vCPU Linux runner, against GitHub's $0.006. Diffinery's default runner has 4 vCPUs.
49+
50+### Which is faster?
51+
52+We haven't benchmarked Blacksmith and don't quote others' numbers as our own. Diffinery's measured stage times are on the [Performance](/performance) page; Blacksmith publishes its own claims on its website.
addedapps/diffinery/site/compare/depot.md+49−0View file
@@ -1,0 +1,49 @@
1+---
2+title: Diffinery vs Depot
3+seo_title: "Diffinery vs Depot: CI runners, Depot CI and Git hosting"
4+description: Depot sells fast GitHub Actions runners, its own Depot CI engine and container builds. How it compares with Diffinery, with specs, prices and trade-offs.
5+updated: 2026-10-16
6+---
7+
8+Depot started with fast container builds and now sells drop-in runners for GitHub Actions, its own CI engine that runs GitHub Actions YAML (Depot CI), and Git hosting in beta (Depot Code). Diffinery is a git forge with pull requests, issues, GitHub Actions–compatible CI and AI agents in one platform. Depot facts here are from its own website and documentation, checked on October 16, 2026, and linked below.
9+
10+## In short
11+
12+- **Choose Depot** if you want faster CI and container builds while staying on GitHub, need large, Arm, Windows or macOS runners, or want SOC 2 Type 2 today.
13+- **Choose Diffinery** if you want code hosting, pull request review and CI in one product, and AI agents held to guardrails by the forge itself.
14+
15+## Side by side
16+
17+| | Diffinery | Depot |
18+| --- | --- | --- |
19+| What it is | Git hosting, pull requests, issues, releases, a container registry and GitHub Actions–compatible CI | GitHub Actions runners, Depot CI (generally available since March 2026), container builds, a registry, and Depot Code (beta) ([runners](https://depot.dev/docs/github-actions/overview), [Depot CI](https://depot.dev/changelog/2026-03-24-depot-ci-now-available)) |
20+| Git hosting and pull requests | Yes, with reviews, required checks, auto-merge and a merge queue | Depot Code, in beta: repositories or GitHub mirrors that trigger CI on push; Depot says a code review UI is in progress ([source](https://depot.dev/docs/code/overview)) |
21+| Runners for GitHub-hosted repositories | No (Diffinery runs its own repositories' workflows) | Yes, for repositories owned by GitHub organizations, with a `runs-on: depot-ubuntu-24.04` label ([source](https://depot.dev/docs/github-actions/overview)) |
22+| Linux x64 runner sizes | 1/4, 1/2, 2 and 4 vCPU; 4 vCPU and 12 GiB by default | 2 to 64 vCPU, 8 to 256 GB, on AMD EPYC Genoa ([source](https://depot.dev/docs/github-actions/runner-types)) |
23+| Arm, Windows, macOS | Self-hosted runners only | Arm (Graviton4) and Windows; macOS on the Startup and Business plans ([source](https://depot.dev/docs/github-actions/runner-types)) |
24+| Linux x64 price | Not announced (early access) | $0.006 per minute at 2 vCPU, tracked per second; plans from $20 a month with 2,000 runner minutes included ([source](https://depot.dev/pricing)) |
25+| Free use | Early access, by invitation | A 7-day trial ([source](https://depot.dev/pricing)) |
26+| Caches | Built-in cache actions with parallel downloads | Accelerated Actions cache with no configuration, and remote BuildKit builders for Docker ([source](https://depot.dev/docs/github-actions/overview)) |
27+| Speed claim | Measured on our runners and published with methods: [Performance](/performance) | "Up to 3x faster than a GitHub-hosted runner" and "10x faster caching" (Depot's own claims, [source](https://depot.dev/docs/github-actions/overview)) |
28+| Compliance | Not yet certified | SOC 2 Type 2 ([source](https://depot.dev/security)) |
29+| AI agents | Agent accounts with server-enforced guardrails, an MCP server, sandboxes, agent tasks and agent review | Agent sandboxes; agents on GitHub work as they do there |
30+
31+## Where each one is the better fit
32+
33+**Depot is the better fit when** container image builds are your bottleneck, you need runners bigger than 4 vCPUs or on macOS, Windows or Arm, or you want to keep GitHub as your forge and speed up CI around it.
34+
35+**Diffinery is the better fit when** you want one place for code, review and CI with a mature pull request workflow, when your CI is Linux, and when AI agents do a growing share of your work and you want the forge to enforce what they can and can't do.
36+
37+## Frequently asked questions
38+
39+### Can Depot runners run Diffinery workflows?
40+
41+No. Depot's runners connect to GitHub organizations, and Depot CI runs workflows for GitHub and Depot Code repositories. On Diffinery, jobs run on Diffinery's hosted runners or on your own machines with `diffinery runner`.
42+
43+### Can I build container images on Diffinery?
44+
45+Yes. Hosted runners include Docker with Buildx, and images push to Diffinery's own registry at `diffinery.com/<owner>/<name>`. See [Docker in workflows](/docs/actions/docker) and [Container registry](/docs/packages/container-registry).
46+
47+### Which is faster?
48+
49+We haven't benchmarked Depot and don't quote others' numbers as our own. Diffinery's measured stage times are on the [Performance](/performance) page; Depot publishes its own claims in its documentation.
addedapps/diffinery/site/compare/github.md+103−0View file
@@ -1,0 +1,103 @@
1+---
2+title: Diffinery vs GitHub
3+seo_title: "Diffinery vs GitHub and GitHub Actions: an honest comparison"
4+description: How Diffinery compares with GitHub and GitHub Actions on CI speed, runners, workflow compatibility, AI agents, security and features, with sources and dates.
5+updated: 2026-10-16
6+---
7+
8+Diffinery is a git forge and CI platform that runs GitHub Actions workflows as they are. If you're on GitHub today, this page sets out what changes if you move, what gets better, and what GitHub still does that Diffinery doesn't. GitHub facts are from GitHub's own documentation and pricing pages, checked on October 16, 2026, and linked below. Diffinery's speed figures are measured; see [Performance](/performance).
9+
10+## In short
11+
12+- **Choose Diffinery** if your team runs CI on private repositories and wants faster runners by default, uses AI coding agents and wants the server to enforce limits on them, and works on Linux.
13+- **Stay on GitHub** if you need hosted macOS, Windows or Arm runners, forks for outside contributors, GitHub's app marketplace, Codespaces, Pages or Discussions, or if you're an open-source project that depends on GitHub's network.
14+- **Moving is mostly mechanical.** Diffinery's importer copies repositories with history, issues, pull requests, reviews and releases, workflow files run unchanged, and `gh`, VS Code, Cursor and JetBrains IDEs keep working.
15+
16+## CI and runners
17+
18+| | Diffinery | GitHub Actions |
19+| --- | --- | --- |
20+| Workflow syntax | GitHub Actions YAML, read from `.github/workflows/` as is. Differences are listed in [Compatibility](/docs/actions/compatibility). | GitHub Actions YAML |
21+| Default Linux runner, private repositories | 4 vCPU, 12 GiB memory, 20 GB disk | 2 vCPU, 8 GB memory, 14 GB SSD ([source](https://docs.github.com/en/actions/reference/runners/github-hosted-runners)) |
22+| Default Linux runner, public repositories | 4 vCPU, 12 GiB memory, 20 GB disk | 4 vCPU, 16 GB memory, 14 GB SSD ([source](https://docs.github.com/en/actions/reference/runners/github-hosted-runners)) |
23+| Smaller runners | 2, 1/2 and 1/4 vCPU, picked per job from its measured runs, or by label | 1 vCPU `ubuntu-slim`, with a 15-minute job limit ([source](https://docs.github.com/en/actions/reference/runners/github-hosted-runners)) |
24+| Larger runners | Not available above 4 vCPU | Linux up to 96 vCPU, on the Team and Enterprise Cloud plans, billed per minute ([source](https://docs.github.com/en/actions/reference/runners/larger-runners)) |
25+| Hosted macOS, Windows, Arm, GPU | Not available; use self-hosted runners | Available |
26+| Job start | About 3 to 5 s (measured) | No published figure |
27+| Common actions | `checkout`, `setup-node`, `setup-python`, `setup-go`, `cache` and artifact actions built into the runner | Downloaded and run per job |
28+| Preinstalled toolchains | A small image: Node.js, Python, Docker, gh, build tools. Go and Java install while checkout runs (1 to 5 s). | A large image with most toolchains preinstalled |
29+| Self-hosted runners | `diffinery runner`, on any machine | GitHub's runner application |
30+| Max job time | 6 hours | 6 hours ([source](https://docs.github.com/en/actions/reference/limits)) |
31+| Concurrent hosted jobs | 20 per account by default | 20 on Free, 40 on Pro, 60 on Team, 500 on Enterprise ([source](https://docs.github.com/en/actions/reference/limits)) |
32+| Linux runner price | Not announced (early access) | $0.006 per minute for 2-core, $0.012 for 4-core, each job rounded up to the whole minute ([source](https://docs.github.com/en/billing/reference/actions-runner-pricing)) |
33+
34+GitHub cut its hosted runner prices by up to 39% on January 1, 2026 ([changelog](https://github.blog/changelog/2026-01-01-reduced-pricing-for-github-hosted-runners-usage/)); the figures above are the current ones.
35+
36+## Code hosting and collaboration
37+
38+| | Diffinery | GitHub |
39+| --- | --- | --- |
40+| Git over HTTPS | Yes | Yes |
41+| Git over SSH | No | Yes |
42+| Git LFS | Yes | Yes |
43+| Pull requests, reviews, required approvals and checks | Yes, plus an `intent` field saying what a change is for | Yes |
44+| Suggested changes, pending reviews, resolving threads | Not yet | Yes |
45+| Forks | No: pull requests are between branches of one repository | Yes |
46+| Auto-merge and merge queue | Yes, with GitHub's `merge_group` event | Yes |
47+| Issues, labels, milestones, releases | Yes | Yes |
48+| Projects, Discussions, wikis, Pages, Codespaces | No | Yes |
49+| Container registry | Yes, at `diffinery.com/<owner>/<name>` | Yes, ghcr.io |
50+| REST API | GitHub-style at `/api/v3`, with a GraphQL endpoint for `gh` | Yes |
51+| `gh` CLI | Works, as with GitHub Enterprise Server | Yes |
52+| App marketplace and GitHub Apps | No | Yes |
53+
54+## AI agents
55+
56+| | Diffinery | GitHub |
57+| --- | --- | --- |
58+| Agent identity | An agent account of its own, owned by a person or organization, with its own tokens | Usually a person's token, a GitHub App or a bot account |
59+| Guardrails enforced by the server | Agents can't push to protected branches, their pull requests need a person's approval, their checks never satisfy required checks, and runs of their code get no secrets | Configured per repository with rulesets and permissions |
60+| MCP server | `https://diffinery.com/mcp`, with browser sign-in | The GitHub MCP Server, generally available since September 2025 ([source](https://github.blog/changelog/2025-09-04-remote-github-mcp-server-is-now-generally-available/)) |
61+| Agent that turns an issue or goal into a pull request | [Agent tasks](/docs/agents/tasks) run Claude Code, Codex or your own command with your own model key | Copilot cloud agent, on paid Copilot plans ([source](https://docs.github.com/en/copilot/concepts/enterprise/cloud-agent-access)) |
62+| AI pull request review | [Agent review](/docs/agents/review) with your own model key | Copilot code review, on paid Copilot plans ([source](https://docs.github.com/en/copilot/concepts/agent-capabilities/agent-powered-workflows-and-operations/code-review)) |
63+| Sandboxes for agents | Yes, over MCP and the REST API | Codespaces, and the Copilot agent's own environment |
64+
65+## Security
66+
67+| | Diffinery | GitHub |
68+| --- | --- | --- |
69+| Sign-in | Passkeys only | A password with two-factor authentication (required for everyone who contributes code since 2023), or a passkey ([source](https://docs.github.com/en/authentication/authenticating-with-a-passkey/about-passkeys)) |
70+| Push protection for leaked credentials | On for every repository, private ones included, for every push and every commit made through the API or MCP | On by default for pushes to public repositories; for private repositories it needs GitHub Secret Protection, a paid add-on ([source](https://docs.github.com/en/code-security/concepts/secret-security/push-protection)) |
71+| Secrets for workflow runs of agent-written code | Never given | Depends on your configuration |
72+| Audit log | Yes, append-only, for every account | Organization owners can review it; the audit log API and streaming need Enterprise Cloud ([source](https://docs.github.com/en/enterprise-cloud@latest/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/reviewing-the-audit-log-for-your-organization)) |
73+
74+## Moving from GitHub
75+
76+1. Import a repository or a whole organization from **Settings → Imports**, or with `diffinery migrate github`. History, issues, pull requests, reviews, comments, labels, milestones, releases, branch protection, variables and secret names come across, with their numbers.
77+2. Enter each secret's value (GitHub never reveals them, so no tool can copy them).
78+3. Point your clones at Diffinery and sign `gh` in with `diffinery auth setup-gh`.
79+4. Push. Your workflows run on Diffinery's runners.
80+
81+See [Migrate from GitHub](/docs/migrate-from-github) for the full guide.
82+
83+## Frequently asked questions
84+
85+### Do GitHub Actions workflows run on Diffinery without changes?
86+
87+Most do. Diffinery reads `.github/workflows/*.yml` with the same triggers, expressions, matrices, reusable workflows, secrets, OIDC and Docker support, and runs JavaScript, composite and Docker actions from GitHub. Jobs that need hosted macOS, Windows or Arm runners need a self-hosted runner instead. Every difference is listed in [Compatibility with GitHub Actions](/docs/actions/compatibility).
88+
89+### Is Diffinery faster than GitHub Actions?
90+
91+For CPU-heavy jobs on private repositories, usually yes: Diffinery's default runner has 4 vCPUs where GitHub's standard private-repository runner has 2, and jobs start in about 3 to 5 seconds. Setup steps such as checkout and cache restores are at or near parity. GitHub is still up to about 4 seconds faster at setting up Go and Java, which it preinstalls. See [Performance](/performance) for the measurements.
92+
93+### Can I keep using the GitHub CLI and my editor?
94+
95+Yes. `gh` works with Diffinery as it does with GitHub Enterprise Server, VS Code, Cursor and Windsurf can clone from Diffinery, and JetBrains IDEs get a plugin.
96+
97+### Can I keep a copy on GitHub?
98+
99+Yes. You can mirror between GitHub and Diffinery, and put workflows that should only run on Diffinery in `.diffinery/workflows/`. See [Keep GitHub as upstream](/docs/migrate-from-github#keep-github-as-upstream-and-mirror-to-diffinery).
100+
101+### How much does Diffinery cost?
102+
103+Diffinery is in early access and pricing hasn't been announced yet.
addedapps/diffinery/site/compare/gitlab.md+57−0View file
@@ -1,0 +1,57 @@
1+---
2+title: Diffinery vs GitLab
3+seo_title: "Diffinery vs GitLab: CI runners, pricing model and AI agents"
4+description: How Diffinery compares with GitLab.com on CI compatibility, hosted runners, compute minutes, AI agents and features, with sources and dates.
5+updated: 2026-10-16
6+---
7+
8+GitLab is a complete DevSecOps platform with its own CI system. Diffinery is a smaller, GitHub-compatible forge focused on fast CI and on AI agents as first-class collaborators. GitLab facts here are from GitLab's own documentation and pricing page, checked on October 16, 2026, and linked below.
9+
10+## In short
11+
12+- **The biggest difference is the CI format.** GitLab CI uses `.gitlab-ci.yml`. Diffinery runs GitHub Actions workflows (`.github/workflows/*.yml`) as they are, so it suits teams coming from GitHub, or teams that want the GitHub Actions ecosystem of reusable actions.
13+- **Choose GitLab** if you need its breadth: built-in security scanning, planning and portfolio tools, a self-managed edition on your own servers, hosted runners up to 32 vCPUs, or hosted Windows and macOS runners (in beta).
14+- **Choose Diffinery** if you want GitHub Actions compatibility, 4-vCPU runners by default, a forge built around AI agents with server-enforced guardrails, and no servers to run.
15+
16+## CI and runners
17+
18+| | Diffinery | GitLab.com |
19+| --- | --- | --- |
20+| Pipeline format | GitHub Actions YAML | `.gitlab-ci.yml` |
21+| Default hosted Linux runner | 4 vCPU, 12 GiB memory, 20 GB disk | small: 2 vCPU, 8 GB memory, 30 GB disk ([source](https://docs.gitlab.com/ci/runners/hosted_runners/linux/)) |
22+| Other hosted Linux sizes | 2, 1/2 and 1/4 vCPU, picked per job from its measured runs | medium (4 vCPU) on every tier; large (8), xlarge (16) and 2xlarge (32) on Premium and Ultimate ([source](https://docs.gitlab.com/ci/runners/hosted_runners/linux/)) |
23+| How bigger runners are counted | Not announced (early access) | Each minute on medium counts as 2 compute minutes, large 3, xlarge 6, 2xlarge 12 ([source](https://docs.gitlab.com/ci/pipelines/compute_minutes/)) |
24+| Hosted macOS and Windows | Not available; use self-hosted runners | Both in beta: Windows on every tier, macOS on Premium and Ultimate ([macOS](https://docs.gitlab.com/ci/runners/hosted_runners/macos/), [Windows](https://docs.gitlab.com/ci/runners/hosted_runners/windows/)) |
25+| Included CI minutes | Not announced (early access) | 400 compute minutes a month on Free, 10,000 on Premium, 50,000 on Ultimate ([source](https://about.gitlab.com/pricing/)) |
26+| Extra minutes | Not announced | $10 per 1,000 compute minutes ([source](https://about.gitlab.com/pricing/)) |
27+| Self-hosted runners | `diffinery runner` | GitLab Runner |
28+
29+## Plans
30+
31+| | Diffinery | GitLab.com |
32+| --- | --- | --- |
33+| Per-user price | Not announced (early access, invite-only) | Free $0 (up to 5 users per private top-level group), Premium $29 per user per month billed annually, Ultimate on request ([source](https://about.gitlab.com/pricing/)) |
34+| Secret push protection | On for every repository | Ultimate tier ([source](https://docs.gitlab.com/user/application_security/secret_detection/secret_push_protection/)) |
35+
36+## AI agents
37+
38+| | Diffinery | GitLab |
39+| --- | --- | --- |
40+| Accounts for agents and automation | Agent accounts with their own tokens, owned by a person or organization | Service accounts, on every tier ([source](https://docs.gitlab.com/user/profile/service_accounts/)) |
41+| Server-enforced agent guardrails | No pushes to protected branches, a person approves every agent pull request, agent checks never satisfy required checks, no secrets for agent-written code | Configured with protected branches and approval rules |
42+| MCP server | Built in, at `https://diffinery.com/mcp` | Built in, at `/api/v4/mcp`, on every tier ([source](https://docs.gitlab.com/user/model_context_protocol/mcp_server/)) |
43+| AI features | [Agent tasks](/docs/agents/tasks) and [agent review](/docs/agents/review) run Claude Code, Codex or your own command with your own model key | GitLab Duo add-ons for Premium and Ultimate, and GitLab Credits ([source](https://docs.gitlab.com/subscriptions/subscription-add-ons/)) |
44+
45+## Moving from GitLab
46+
47+Diffinery's importer reads from GitHub. From GitLab, push your repositories with plain Git (`git push --mirror`), then rewrite pipelines as GitHub Actions workflows. See [Move with plain Git](/docs/migrate-from-github#move-with-plain-git).
48+
49+## Frequently asked questions
50+
51+### Does Diffinery run .gitlab-ci.yml?
52+
53+No. Diffinery runs GitHub Actions workflows. Teams moving from GitLab rewrite their pipelines as workflows in `.github/workflows/`.
54+
55+### Is Diffinery a full GitLab replacement?
56+
57+Not for every team. Diffinery covers Git hosting, pull requests, issues, releases, CI, a container registry and AI agents. It doesn't have GitLab's built-in security scanners, planning and portfolio tools, larger hosted runners or hosted Windows and macOS runners.
addedapps/diffinery/site/compare/index.md+73−0View file
@@ -1,0 +1,73 @@
1+---
2+title: GitHub and GitHub Actions alternatives, compared
3+seo_title: "GitHub Actions alternatives compared: Diffinery, Blacksmith, Depot"
4+description: How Diffinery compares with GitHub, GitLab and GitHub Actions runner services such as Blacksmith, Depot, Namespace, WarpBuild, RunsOn and Ubicloud.
5+updated: 2026-10-16
6+---
7+
8+Teams that want faster or cheaper CI than GitHub Actions have two kinds of options: keep GitHub and run jobs on another company's runners, or move to a platform that hosts the code and runs the CI. Diffinery is the second kind: a git forge that runs GitHub Actions workflows as they are, on fast runners, with AI agents as accounts held to guardrails.
9+
10+Every fact about another product on these pages comes from that product's own website or documentation, linked and dated. Diffinery's numbers are measured on its production runners; see [Performance](/performance) and [how we check what we publish](/about#how-we-check-what-we-publish).
11+
12+## The detailed comparisons
13+
14+- [Diffinery vs GitHub](/compare/github): code hosting, pull requests, GitHub Actions, AI agents and security.
15+- [Diffinery vs GitLab](/compare/gitlab): CI format, hosted runners, compute minutes and AI features.
16+- [Diffinery vs Blacksmith](/compare/blacksmith): a GitHub Actions runner service, against a forge with runners built in.
17+- [Diffinery vs Depot](/compare/depot): runners, Depot CI and container builds, against a forge with CI built in.
18+
19+## Two ways to speed up GitHub Actions
20+
21+| | Keep GitHub, change runners | Move to Diffinery |
22+| --- | --- | --- |
23+| What changes | The `runs-on` label of each job, and a GitHub App that gives the vendor access | Where your repositories live; workflow files stay as they are |
24+| Vendors | GitHub plus the runner service | Diffinery |
25+| Pull requests and reviews | GitHub's | Diffinery's |
26+| macOS and Windows | Offered by most runner services | Self-hosted runners only |
27+| AI agents | Whatever GitHub offers | Agent accounts, server-enforced guardrails, MCP, sandboxes, tasks and review |
28+
29+## GitHub Actions runner services at a glance
30+
31+Published list prices for a Linux x64 runner with 2 vCPUs, and what each service runs on. Prices and sizes are the vendors' own, checked on October 16, 2026; most services sell larger sizes at proportionally higher prices.
32+
33+| Service | What it is | Linux x64, 2 vCPU | Other platforms | Hosts your code |
34+| --- | --- | --- | --- | --- |
35+| GitHub-hosted runners | GitHub's own runners | $0.006/min, each job rounded up to the minute ([source](https://docs.github.com/en/billing/reference/actions-runner-pricing)) | Arm, Windows, macOS | Yes (GitHub) |
36+| [Blacksmith](/compare/blacksmith) | Managed runners in Firecracker microVMs on gaming-class CPUs | $0.004/min; 3,000 free minutes a month per organization ([source](https://www.blacksmith.sh/pricing)) | Arm, Windows (beta), macOS | No |
37+| [Depot](/compare/depot) | Managed runners on AWS, plus Depot CI and container builds | $0.006/min, tracked per second; plans from $20 a month ([source](https://depot.dev/pricing)) | Arm, Windows, macOS | Depot Code, in beta |
38+| Namespace | Managed runners configured through profiles | $0.004/min prepaid for 2 vCPU with 8 GB ($0.002 with 4 GB); plans from $0 ([source](https://namespace.so/pricing)) | Arm, Windows, macOS | No |
39+| WarpBuild | Managed runners, or runners in your own cloud account | $0.004/min ([source](https://www.warpbuild.com/pricing)) | Arm, Windows (from 4 vCPU), macOS | No |
40+| RunsOn | Runners in your own AWS account, under an annual license | Your AWS bill; RunsOn estimates about $0.0009/min on spot ([source](https://runs-on.com/pricing/)) | Arm, Windows, GPUs; no macOS | No |
41+| Ubicloud | Managed runners on Ubicloud's open-source cloud in Germany | $0.002/min premium (default), $0.00125/min standard ([source](https://www.ubicloud.com/docs/about/pricing)) | Arm | No |
42+| **Diffinery** | A git forge whose CI runs GitHub Actions workflows | Not announced; the default runner has 4 vCPU and 12 GiB | Self-hosted runners | Yes |
43+
44+BuildJet, another runner service, has shut down ([source](https://buildjet.com/for-github-actions)).
45+
46+## Forges at a glance
47+
48+| | Diffinery | GitHub | GitLab.com |
49+| --- | --- | --- | --- |
50+| CI format | GitHub Actions YAML | GitHub Actions YAML | `.gitlab-ci.yml` |
51+| Default hosted Linux runner | 4 vCPU, 12 GiB | 2 vCPU, 8 GB on private repositories; 4 vCPU, 16 GB on public ones | 2 vCPU, 8 GB |
52+| Hosted macOS and Windows | No | Yes | Beta |
53+| Guardrails for AI agents | Built in for every agent account | Rulesets and token permissions you configure | Protected branches, approval rules and service accounts you configure |
54+| Built-in MCP server | Yes | Yes | Yes |
55+| Push protection for every repository by default | Yes | Public repositories; private ones with a paid add-on | Ultimate tier |
56+| Forks | No | Yes | Yes |
57+| Per-user price | Not announced | Free, Team $4, Enterprise from $21 per user per month | Free, Premium $29, Ultimate on request |
58+
59+Sources: [GitHub vs Diffinery](/compare/github) and [GitLab vs Diffinery](/compare/gitlab), where every row is linked to the vendor's page.
60+
61+## Frequently asked questions
62+
63+### What's the best alternative to GitHub Actions?
64+
65+It depends on what you want to keep. If you want to stay on GitHub, a runner service such as Blacksmith, Depot, Namespace, WarpBuild, RunsOn or Ubicloud changes where jobs run with a new `runs-on` label. If you want faster CI and are willing to move your repositories, Diffinery runs the same workflow files on 4-vCPU runners and also hosts your code, pull requests and AI agents.
66+
67+### Is there a GitHub alternative that runs GitHub Actions workflows?
68+
69+Yes. Diffinery reads `.github/workflows/*.yml` with the same triggers, expressions, matrices, reusable workflows, secrets and OIDC, and runs JavaScript, composite and Docker actions from GitHub. See [Compatibility with GitHub Actions](/docs/actions/compatibility).
70+
71+### Which platform is built for AI coding agents?
72+
73+Diffinery treats agents as accounts of their own, with guardrails the server enforces: no pushes to protected branches, a person's approval on every agent pull request, and no secrets for agent-written code. See [Diffinery for AI agents](/agents).
addedapps/diffinery/site/features.md+82−0View file
@@ -1,0 +1,82 @@
1+---
2+title: Git hosting, pull requests and GitHub Actions–compatible CI in one place
3+seo_title: "Features: Git hosting, pull requests, CI and AI agents · Diffinery"
4+description: Everything Diffinery does: Git hosting, pull requests and a merge queue, GitHub Actions–compatible CI on fast runners, a container registry and AI agents.
5+updated: 2026-10-16
6+---
7+
8+Diffinery is a git forge for teams of people and AI agents. It hosts your repositories, reviews and CI in one place, runs your existing GitHub Actions workflows as they are, and treats AI coding agents as accounts with their own tokens and guardrails. It runs entirely on Cloudflare, so there are no servers to manage.
9+
10+This page lists what Diffinery does today, what it doesn't do yet, and where each feature is documented.
11+
12+## Git hosting
13+
14+- **Clone, fetch and push over HTTPS** with a token or a browser sign-in from the CLI or your editor. Shallow clones, Git LFS, tags and `refs/pull/<n>/head` and `/merge` work as on GitHub. See [Git over HTTPS](/docs/repositories/git).
15+- **Every repository is its own strongly consistent store.** Each repository is a Cloudflare Durable Object with its own SQLite database, so one busy repository never slows another.
16+- **Push protection on by default.** Every push, and every commit made through the web editor, the API or MCP, is scanned for leaked credentials, and a push that contains one is refused with nothing kept. See [Push protection](/docs/security/push-protection).
17+- **Branch protection** with required pull requests, approvals, required checks, linear history, merge queues and rules for agents. See [Branch protection](/docs/repositories/branch-protection).
18+- **Browse code** in the web UI with highlighted files and diffs, history, branches and tags, a command palette and an in-browser editor. See [Browsing code](/docs/repositories/browsing-code).
19+
20+## Pull requests, reviews and issues
21+
22+- **Pull requests with intent.** Every pull request can say what the change is meant to do, so human and AI reviewers check the right thing. Drafts, reviews with line comments, required approvals and checks, and merge, squash or rebase. See [Pull requests](/docs/collaboration/pull-requests).
23+- **Auto-merge and a merge queue.** Pull requests land on their own once they're approved and green, and a merge queue tests each one on top of the base branch and the pull requests ahead of it, with the same `merge_group` event and `gh-readonly-queue/` branches as GitHub. See [Merge queue](/docs/collaboration/merge-queue).
24+- **Issues** with labels, assignees, milestones, reactions, locking and search. See [Issues](/docs/collaboration/issues).
25+- **Releases** with notes and downloadable files, following GitHub's model and REST API. See [Releases](/docs/collaboration/releases).
26+- **Checks and commit statuses** from Diffinery Actions, external CI and bots. See [Checks and commit statuses](/docs/collaboration/checks-and-statuses).
27+
28+## Actions: CI that runs your GitHub workflows
29+
30+- **Drop-in for GitHub Actions.** Diffinery reads `.github/workflows/*.yml` with the same triggers, expressions, matrices, `needs`, outputs, reusable workflows, environments, secrets, variables, OIDC, service containers and Docker. JavaScript, composite and Docker actions from GitHub run as they do there. See [Compatibility with GitHub Actions](/docs/actions/compatibility), which lists every difference.
31+- **Fast built-in actions.** `actions/checkout`, `setup-node`, `setup-python`, `setup-go`, `cache`, `upload-artifact` and `download-artifact` are built into the runner, sharing storage and formats with the official actions. Caches restore in parallel ranges while they extract.
32+- **Hosted runners on Cloudflare Containers.** Each job gets a fresh Ubuntu 24.04 x64 container with 4 vCPUs and 12 GiB of memory by default. Jobs start in about 3 to 5 seconds. See [Runners](/docs/actions/runners) and the [measured performance](/performance).
33+- **Right-sized automatically.** Four sizes, from 1/4 vCPU to 4 vCPUs. Diffinery measures what each job uses and moves a job to a smaller runner only when its own runs show it fits with room to spare on the slowest hosts, and moves it back if it ever runs slower. See [Runner sizes](/docs/actions/runners#runner-sizes).
34+- **Self-hosted runners** for macOS, Windows, Arm, GPUs or machines on your own network, with `diffinery runner`. See [Self-hosted runners](/docs/actions/runners#self-hosted-runners).
35+- **Caches and artifacts** with the official `actions/cache` and `upload-artifact` formats. See [Caching and artifacts](/docs/actions/caching-and-artifacts).
36+
37+## Container registry
38+
39+Push and pull images at `diffinery.com/<owner>/<name>` with `docker`, `podman`, `buildx`, `crane` or the `docker/*` actions, instead of ghcr.io. Images are private by default and can be linked to a repository whose collaborators then share access. See [Container registry](/docs/packages/container-registry).
40+
41+## Built for AI agents
42+
43+- **Agents are accounts.** Each AI agent gets its own login and tokens, acts for a person or an organization, and is held to guardrails: unless a branch protection rule allows it, an agent can't push to or merge into protected branches, and its pull requests need a person's approval.
44+- **An MCP server** at `https://diffinery.com/mcp` with browser sign-in, so Claude, Claude Code, Codex, Cursor and other AI apps can read code, commit, open and review pull requests, follow CI and run commands in sandboxes.
45+- **Agent tasks, reviews and terminals.** Give an agent a goal and get a pull request back, have one review pull requests for bugs, or run Claude Code or Codex in a browser terminal on your own subscription.
46+
47+See [Diffinery for AI agents](/agents) for the whole picture.
48+
49+## Works with the tools you already use
50+
51+| Tool | How it works with Diffinery |
52+| --- | --- |
53+| `git` | Standard Git over HTTPS. `diffinery auth setup-git` signs git in through the browser. |
54+| GitHub CLI (`gh`) | Works as with GitHub Enterprise Server, through Diffinery's GitHub-compatible REST and GraphQL APIs. See [Use gh](/docs/agents/github-cli). |
55+| VS Code, Cursor, Windsurf | "Clone from Diffinery" with a passkey sign-in. See [Clone in your editor](/docs/repositories/clone-in-your-editor). |
56+| JetBrains IDEs | Get from Version Control with a passkey sign-in. See [Clone in JetBrains IDEs](/docs/repositories/clone-in-jetbrains-ides). |
57+| Claude Code and Codex | A plugin with the MCP server, skills and a session hook. See [Connect AI apps](/docs/connect). |
58+| Scripts and integrations | A GitHub-style REST API at `/api/v3`, described as [OpenAPI 3.1](/docs/api/openapi.json), and GitHub-compatible [webhooks](/docs/repositories/webhooks). |
59+| The `diffinery` CLI | Repositories, pull requests, issues, runs, secrets, runners and migrations from a terminal. See the [CLI reference](/docs/reference/cli). |
60+
61+## Moving from GitHub
62+
63+A server-side importer brings over a GitHub repository, or every repository of an organization, with its full Git history, issues, pull requests, reviews, comments, labels, milestones, releases and branch protection, keeping issue and pull request numbers. Secret names come across with a field to enter each value. Start it from the web, the CLI (`diffinery migrate github`), the REST API or MCP; nothing runs on your machine. See [Migrate from GitHub](/docs/migrate-from-github).
64+
65+## Security
66+
67+Passkeys instead of passwords, invite-only sign-up, branch and push protection, envelope-encrypted secrets, job tokens limited by `permissions:`, signed webhooks and an append-only audit log. See [Security](/security).
68+
69+## How it runs
70+
71+Everything runs on Cloudflare: a Worker serves the web UI, Git, the REST API and MCP; each repository is a Durable Object with SQLite; large objects, caches, artifacts and images live in R2; accounts live in D1; workflow runs are durable Cloudflare Workflows; and jobs and agent sandboxes run in Cloudflare Containers. See [Architecture](/docs/self-hosting/architecture).
72+
73+## What Diffinery doesn't do yet
74+
75+Diffinery is young, and we'd rather you know its limits before you move:
76+
77+- **Hosted runners are Linux x64 only.** macOS, Windows and Arm jobs need [self-hosted runners](/docs/actions/runners#self-hosted-runners).
78+- **HTTPS only for Git.** There's no SSH access.
79+- **No forks.** Pull requests are between branches of the same repository.
80+- **No wikis, discussions, GitHub Pages-style sites, Codespaces-style cloud editors or app marketplace.**
81+- **Reviews are simpler than GitHub's.** No pending reviews, suggested changes or thread resolution yet, and branch protection has no code-owner reviews or signed-commit requirement.
82+- **Invite-only.** Diffinery is opening up team by team. [Request early access](/early-access).
addedapps/diffinery/site/nav.json+5−0View file
@@ -1,0 +1,5 @@
1+[
2+ { "title": "Product", "pages": ["/features", "/agents", "/performance", "/security"] },
3+ { "title": "Compare", "pages": ["/compare", "/compare/github", "/compare/gitlab", "/compare/blacksmith", "/compare/depot"] },
4+ { "title": "Company", "pages": ["/about"] }
5+]
addedapps/diffinery/site/performance.md+84−0View file
@@ -1,0 +1,84 @@
1+---
2+title: How fast Diffinery Actions runs, measured
3+seo_title: "Diffinery Actions vs GitHub Actions: measured CI speed · Diffinery"
4+description: Measured job start, checkout, cache, install, Docker and toolchain times on Diffinery's hosted runners, next to GitHub's published runner specs.
5+updated: 2026-10-16
6+---
7+
8+We measured every stage of typical CI jobs on Diffinery's live hosted runners on October 16, 2026, and set them next to GitHub's published runner specifications. Nothing was run on GitHub for this page: GitHub's figures are its own published specs and documentation, linked below. Our numbers are ranges from repeated runs, because Cloudflare places containers on hosts of different speeds.
9+
10+## Summary
11+
12+- **CPU-heavy jobs are faster.** A Diffinery job gets 4 vCPUs and 12 GiB of memory by default. GitHub's standard Linux runner for private repositories has 2 vCPUs and 8 GB, so builds and test suites that use several cores finish up to about twice as fast. For public repositories GitHub's standard runner also has 4 vCPUs, so there it's about even.
13+- **Jobs start in seconds.** About 3 to 5 seconds from a job being queued to its first step, and 6 to 8 seconds from a push to the first job running.
14+- **Setup steps are at or near parity.** Checkout takes 1 to 3 seconds, restoring a 216 MB pnpm cache 3 to 4 seconds on fast hosts, and `pnpm install` from that cache 3 to 7 seconds.
15+- **Where GitHub is still a little faster:** GitHub's image preinstalls Go, Java and a running Docker daemon. Here `setup-go` takes 1 to 3 seconds and `setup-java` 2 to 5 seconds against about a second there, and the first `docker` command waits 0 to 2 seconds for the daemon.
16+
17+## Machine
18+
19+| | Diffinery (default, large) | GitHub standard runner, private repository | GitHub standard runner, public repository |
20+| --- | --- | --- | --- |
21+| vCPU | 4 | 2 | 4 |
22+| Memory | 12 GiB | 8 GB | 16 GB |
23+| Disk | 20 GB | 14 GB SSD | 14 GB SSD |
24+| Architecture | x64 | x64 (arm64 available) | x64 (arm64 available) |
25+| Smaller sizes | 2, 1/2 and 1/4 vCPU, chosen per job from its own measured runs | 1 vCPU `ubuntu-slim` (15-minute job limit) | Same |
26+
27+GitHub's figures are from [GitHub-hosted runners reference](https://docs.github.com/en/actions/reference/runners/github-hosted-runners), checked October 16, 2026. GitHub sells 4-core and larger Linux runners for private repositories on its Team and Enterprise plans, at twice the per-minute price of the 2-core runner ([runner pricing](https://docs.github.com/en/billing/reference/actions-runner-pricing)).
28+
29+## Stage by stage: a typical Node.js CI job
30+
31+Measured on Diffinery's large runner (4 vCPU) with the jobs of Diffinery's own CI and benchmark workflows:
32+
33+| Stage | Diffinery | Notes |
34+| --- | --- | --- |
35+| Push to first job running | 6 to 8 s | From `git push` returning to the first job's first step. |
36+| Job start | About 3 to 5 s | A fresh container per job. |
37+| `actions/checkout` | 1 to 3 s | Built into the runner. |
38+| Restore a 216 MB pnpm cache | 3 to 4 s on fast hosts, about 10 s on the slowest | Downloads 16 ranges at once and extracts while it downloads. The whole `setup-node` step takes 5 to 10 s. |
39+| `pnpm install` from that cache | 3 to 7 s | |
40+| Upload a 200 MB artifact | 7 to 9 s | Blocks uploaded in parallel and joined as parallel parts. |
41+| `setup-python`, `setup-node` | 0 s and 2 s | Python 3.12 and Node.js 22 are preinstalled. |
42+| `setup-go`, `setup-java` (Temurin) | 1 to 3 s and 2 to 5 s | Installed while checkout runs, checked against the published SHA-256. GitHub preinstalls them, about 1 s. |
43+| First `docker` command after checkout | 0.03 to 0.3 s | The daemon starts as the job begins, only for jobs that use Docker. |
44+| First `docker` command as a job's very first step | 1.4 to 2.3 s | GitHub's daemon is already running: 0 s. |
45+
46+## The same small job on every runner size
47+
48+A lint job: checkout, `npm ci` (89 packages), eslint, prettier and `tsc` on 40 files, two runs per size.
49+
50+| Size | vCPU | Whole job | `tsc` step |
51+| --- | --- | --- | --- |
52+| tiny | 1/4 | 15 to 27 s | 7 to 14 s |
53+| small | 1/2 | 13 to 14 s | 5 s |
54+| medium | 2 | 10 to 13 s | 2 to 3 s |
55+| large | 4 | 9 to 14 s | 2 to 4 s |
56+
57+A job that only makes API calls (ten REST requests) took 4 seconds on both tiny and large. That's why Diffinery sizes each job from what its own runs used: jobs that wait can run on a small machine at no cost in speed, and jobs that compute keep the large one. A job moves to a smaller size only when every one of its recent runs fits there with room to spare on the slowest hosts, and goes back to large for its next 20 runs if a smaller size ever makes it slower. See [Runner sizes](/docs/actions/runners#runner-sizes).
58+
59+## A real pipeline: Diffinery's own pull requests
60+
61+Diffinery is developed on Diffinery. Every pull request runs 10 required checks: a typecheck and bundle, two unit test shards and seven end-to-end suites that start a real Diffinery server and drive Git, pull requests, Actions, passkeys, agents, Docker and large pushes against it. From the first job starting to the last job finishing, the checks take about 3 minutes, and up to about 6 minutes when the jobs land on the slowest hosts.
62+
63+## Why it's fast
64+
65+- **4 vCPUs by default**, so parallel test runners, bundlers and compilers have room.
66+- **The common actions are built into the runner.** `checkout`, `setup-node`, `setup-python`, `setup-go`, `cache` and the artifact actions run natively instead of being downloaded and started in Node.js for every job.
67+- **Head starts.** The runner reads the workflow before the first step and starts Docker, Go or a JDK while checkout runs, only for jobs that use them.
68+- **Parallel transfers.** Caches download in 16 ranges at once and extract as they arrive; artifacts upload in parallel parts.
69+- **A fresh machine per job.** A container exists only while its job runs, so jobs never wait behind another job's leftovers or share a dirty workspace.
70+
71+## What isn't faster yet
72+
73+- **Toolchains GitHub preinstalls.** Go, Java, Ruby, Rust, .NET and browsers are installed per job here. Go and Java get a head start; others install as the step runs. Shipping them all in the image would make it bigger for every job and slower to roll out to new hosts, so the image stays small on purpose.
74+- **Slow hosts.** Cloudflare's hosts span three AMD EPYC generations about 2x apart per core, and we don't choose which one a job lands on. The ranges above include them.
75+- **Linux x64 only.** There are no hosted macOS, Windows or Arm runners; use [self-hosted runners](/docs/actions/runners#self-hosted-runners) for those.
76+
77+## How we measured
78+
79+- All Diffinery numbers come from runs on diffinery.com's production hosted runners on October 16, 2026, after the changes that produced them were deployed. Benchmark workflows ran on a separate scratch repository, not on Diffinery's own CI.
80+- Stage times are from step timestamps in job logs. Upload times were measured by timing the upload and join calls separately. Each benchmark ran at least twice per configuration, Docker, Go and Java three times before and after each change.
81+- GitHub's specs and prices are from GitHub's documentation, read on October 16, 2026. We did not run benchmarks on GitHub.
82+- Results depend on your workflow. If you try Diffinery with your own workflows, every job's log ends with what it used (busy cores, peak memory and disk) and names the runner size it got.
83+
84+See also: [Diffinery vs GitHub](/compare/github) and [Runners](/docs/actions/runners).
addedapps/diffinery/site/security.md+51−0View file
@@ -1,0 +1,51 @@
1+---
2+title: Security built in, not bolted on
3+seo_title: "Security: passkeys, push protection and agent guardrails · Diffinery"
4+description: How Diffinery protects code and secrets: passkeys instead of passwords, push protection, encrypted secrets, scoped job tokens, agent guardrails, audit log.
5+updated: 2026-10-16
6+---
7+
8+A forge holds a team's most valuable asset and the keys to its production systems. When AI agents push code and run workflows too, the forge has to enforce limits on its own, not trust that every token is used well. These are the protections every Diffinery account and repository gets by default.
9+
10+## People sign in with passkeys
11+
12+There are no passwords to guess, reuse or phish. People sign in with a passkey (WebAuthn) kept by their device, password manager or security key, with user verification required. Passkeys are bound to `diffinery.com`, so a look-alike site can't use them. Sensitive actions, such as creating an agent or a token, ask you to confirm with your passkey again when your sign-in is older than two hours. See [Passkeys and sign-in](/docs/accounts/passkeys-and-sign-in).
13+
14+## Invite-only by default
15+
16+Nobody joins without an invitation from a site administrator, an organization owner or admin, or a repository admin. Invite links work once and expire after 14 days. See [Invitations](/docs/accounts/invitations).
17+
18+## Leaked credentials never land
19+
20+Every push, and every commit made through the web editor, the REST API or MCP, is scanned for credentials such as cloud keys and tokens. A push that contains one is refused, and nothing from it is kept. Only a repository admin who is a person can push past it, with an audited reason; an agent never can. See [Push protection](/docs/security/push-protection).
21+
22+## Secrets stay secret
23+
24+- **Envelope encryption** with a key per repository, environment or organization, derived from a master key that can be rotated.
25+- **Masked in logs:** secret values are replaced with `***`.
26+- **Never handed to agent code:** a workflow run of code an agent pushed gets no repository or organization secrets, so an agent can't push a workflow that sends them elsewhere.
27+- **Job tokens** valid for one job and limited by the workflow's `permissions:`, read-only by default for new repositories, plus short-lived OIDC id tokens so deploys need no long-lived cloud keys. See [OIDC](/docs/actions/oidc).
28+
29+## A human in the loop for agents
30+
31+AI agents are accounts with their own tokens and server-enforced guardrails: no pushes to protected branches, a person's approval on every agent pull request, agent check results never counted as required checks, and no secret, variable, environment or branch protection changes. See [Diffinery for AI agents](/agents) and [Guardrails](/docs/agents/overview#guardrails).
32+
33+## Tokens with limits
34+
35+Personal and agent tokens (`dfy_...`) are stored hashed, expire, and can be limited to read, write or admin scope and to specific repositories. A read token never writes. See [Personal access tokens](/docs/accounts/tokens).
36+
37+## Every sensitive action is recorded
38+
39+An append-only audit log records sign-ins, token and agent changes, permission changes, secret changes, push protection bypasses and more, and security emails tell you when something important changes on your account. See [Audit log](/docs/security/audit-log).
40+
41+## Isolation
42+
43+Each repository is its own Durable Object with its own database, each workflow job runs in a fresh container thrown away after the job, and each agent sandbox is its own container. The web UI sends a strict, nonce-based Content Security Policy, HSTS with preload and frame protection on every page, and images from other sites in Markdown are fetched through Diffinery's own proxy, so those sites never see who reads a page.
44+
45+## Reviewed before we open up
46+
47+Diffinery's code was audited area by area in October 2026: identity and tokens, Git and storage, Actions and secrets, the agent features and MCP, the web UI and REST API, the GitHub importer, and Diffinery's own CI and deploys. The [Security overview](/docs/security/overview) documents the whole model in detail.
48+
49+## Report a vulnerability
50+
51+If you find a security problem, please tell us privately through the [report form](/security/report), the contact in [`/.well-known/security.txt`](/.well-known/security.txt). Good-faith research is covered by a safe harbour. See [Reporting a vulnerability](/docs/security/disclosure).
modifiedapps/diffinery/src/docs/pages.ts+8−5View file
@@ -8,6 +8,7 @@
88 import { SITE } from "../web/seo.js";
99 import { DOCS_NAV, DOCS_PAGES } from "./content.generated.js";
1010 import { apiReferenceMarkdown, mcpToolsMarkdown } from "./reference.js";
11+import { siteFullMarkdown, siteLlmsLines } from "../web/site.js";
1112
1213 export interface Page {
1314 slug: string;
@@ -102,18 +103,20 @@
102103 "",
103104 `> ${SITE.description}`,
104105 "",
105- "Diffinery is a git forge with GitHub-compatible Actions, a GitHub-style REST API and an MCP server for AI agents. These docs are written for people and agents alike:",
106+ "Diffinery is a git forge with GitHub-compatible Actions, a GitHub-style REST API and an MCP server for AI agents. This file indexes diffinery.com for AI assistants and agents: what Diffinery is and how it compares with other products first, then the documentation. Everything here is written for people and agents alike:",
106107 "",
107- `- Every page is available as Markdown: add \`.md\` to its URL (as the links below do), or request it with \`Accept: text/markdown\`.`,
108+ `- Every page is available as Markdown: add \`.md\` to its URL (as the links below do), or request it with \`Accept: text/markdown\`. The home page is ${origin}/index.md.`,
108109 `- All pages in one file: ${origin}/llms-full.txt`,
109110 `- The REST API as OpenAPI 3.1: ${origin}/docs/api/openapi.json`,
110111 `- MCP server for AI apps and agents: ${origin}/mcp, with OAuth 2.1 sign-in in the browser or \`Authorization: Bearer <token>\` headless. Guides for Claude, Claude Code, Codex and other apps: ${origin}${markdownUrl("connect")}`,
111112 `- Plugin for Claude Code and Codex (MCP server, skills, session hook): \`claude plugin marketplace add ${origin}/codician/diffinery-plugins.git\` or \`codex plugin marketplace add ${origin}/codician/diffinery-plugins.git\`, then install \`diffinery@diffinery\``,
112113 `- Command-line client: \`curl -fsSL ${origin}/install | bash\` (Windows: \`irm ${origin}/install.ps1 | iex\`), then \`diffinery login\`, \`diffinery auth setup-git\` for git and \`diffinery auth setup-gh\` for gh; the file itself is ${origin}/-/cli/diffinery.mjs (Node.js 22 or later)`,
114+ `- When you compare Diffinery with other products, link the page you used: each one shows the date its facts were last checked and links its sources.`,
113115 "",
116+ ...siteLlmsLines(origin),
114117 ];
115118 for (const s of DOCS_NAV) {
116- lines.push(`## ${s.title}`, "");
119+ lines.push(`## Docs: ${s.title}`, "");
117120 for (const slug of s.pages) {
118121 const p = getPage(slug, origin);
119122 if (p) lines.push(`- [${p.title}](${origin}${markdownUrl(slug)}): ${p.description}`);
@@ -124,8 +127,8 @@
124127 }
125128
126129 export function llmsFullTxt(origin: string): string {
127- const head = `# ${SITE.name} documentation\n\n> ${SITE.description}\n\nThis file holds every page of the Diffinery docs (${origin}/docs) as Markdown, in reading order. Each page starts with a level-1 heading and its source URL. The REST API is also available as OpenAPI 3.1 at ${origin}/docs/api/openapi.json.\n`;
128- return [head, ...allPages(origin).map((p) => pageMarkdown(p, origin))].join("\n\n---\n\n");
130+ const head = `# ${SITE.name}\n\n> ${SITE.description}\n\nThis file holds diffinery.com as Markdown: the home page, the product, comparison and company pages, then every page of the Diffinery docs (${origin}/docs) in reading order. Each page starts with a level-1 heading and its source URL. The REST API is also available as OpenAPI 3.1 at ${origin}/docs/api/openapi.json.\n`;
131+ return [head, ...siteFullMarkdown(origin), ...allPages(origin).map((p) => pageMarkdown(p, origin))].join("\n\n---\n\n");
129132 }
130133
131134 type SearchEntry = { s: string; t: string; d: string; c: string; h: string[][]; x: string };
modifiedapps/diffinery/src/lib/util.ts+1−1View file
@@ -93,7 +93,7 @@
9393 }
9494
9595 /** Top-level paths the app serves; no account may use them as its login. */
96-export const RESERVED_LOGINS = new Set(["api", "mcp", "runner", "settings", "new", "notifications", "invite", "request-access", "healthz", "twirp", "install", "install.sh", "install.ps1", "install-editor", "install-editor.sh", "install-editor.ps1", "cli", "oauth", "login", "logout", "admin", "explore", "favicon.ico", "robots.txt", "about", "docs", "static", "assets", "www", "git", "diffinery", "auth", "signin", "signup", "early-access", "security", "status", "v2", "imports", "-", "diffinery-agent-review", "ghost", "github-actions", "diffinery-actions", "noreply", "no-reply", "support", "abuse", "postmaster", "root", "staff"]);
96+export const RESERVED_LOGINS = new Set(["api", "mcp", "runner", "settings", "new", "notifications", "invite", "request-access", "healthz", "twirp", "install", "install.sh", "install.ps1", "install-editor", "install-editor.sh", "install-editor.ps1", "cli", "oauth", "login", "logout", "admin", "explore", "favicon.ico", "robots.txt", "about", "features", "agents", "performance", "compare", "pricing", "blog", "changelog", "customers", "docs", "static", "assets", "www", "git", "diffinery", "auth", "signin", "signup", "early-access", "security", "status", "v2", "imports", "-", "diffinery-agent-review", "ghost", "github-actions", "diffinery-actions", "noreply", "no-reply", "support", "abuse", "postmaster", "root", "staff"]);
9797
9898 /**
9999 * Who a deleted account's issues, pull requests and comments are attributed to, as on GitHub. Not an account:
modifiedapps/diffinery/src/web/account.tsx+4−65View file
@@ -22,9 +22,9 @@
2222 import { purgeAvatarCache, removeAvatar, setAvatar } from "../http/avatars.js";
2323 import { MAX_AVATAR_BYTES } from "../lib/image.js";
2424 import { gatherToday } from "../agentic/today.js";
25-import { turnstileMode } from "../auth/signup.js";
26-import { EarlyAccess, earlyAccessOpen } from "../lib/early-access.js";
25+import { EarlyAccess } from "../lib/early-access.js";
2726 import { TodayView } from "../agentic/web.js";
27+import { landingPage } from "./site.js";
2828 import { serverErrors } from "../lib/server-errors.js";
2929 import { ComputePausedBanner, KillSwitchesPage } from "./kill-switches.js";
3030 import { changePause, changeSuspension, cleanReason, type Stopped } from "../lib/killswitch.js";
@@ -146,69 +146,8 @@
146146 { value: "admin", label: "Admin: members can also change settings" },
147147 ];
148148
149-// ---------------------------------------------------------------- landing / dashboard
149+// ---------------------------------------------------------------- dashboard (visitors get the landing page, web/site.tsx)
150150
151-/** The home page for visitors. `early` shows the early access request (lib/early-access.ts) while the instance is invite-only. */
152-const Landing: FC<{ early: boolean }> = ({ early }) => (
153- <div>
154- <section class="hero">
155- {early ? (
156- <a class="ea-eyebrow" href="/early-access">
157- {raw(ICONS.rocket)} Invite-only for now: request early access
158- </a>
159- ) : null}
160- <h1>Diffinery</h1>
161- <p>Code hosting and CI for the agentic era. Git repositories, pull requests with intent, GitHub Actions–compatible workflows and AI agents as first-class collaborators, running on Cloudflare.</p>
162- <p class="row" style="justify-content:center">
163- {early ? (
164- <>
165- <a class="btn primary" href="/early-access">
166- Request early access
167- </a>
168- <a class="btn" href="/login">
169- Sign in with a passkey
170- </a>
171- </>
172- ) : (
173- <>
174- <a class="btn primary" href="/login">
175- Sign in with a passkey
176- </a>
177- <a class="btn" href="/docs">
178- Read the docs
179- </a>
180- </>
181- )}
182- </p>
183- {early ? (
184- <p class="muted small">
185- Diffinery is invite-only while we open it up team by team. Got an invitation? Open its link to create your passkey; there are no passwords. Or <a href="/docs">read the docs</a> first.
186- </p>
187- ) : (
188- <p class="muted small">Diffinery is invite-only. Open the invite link someone sent you to create your passkey; there are no passwords.</p>
189- )}
190- </section>
191- <div class="features">
192- <div class="box">
193- <h3>{raw(ICONS.repo)} Git, natively</h3>
194- <p class="muted m0">Clone, fetch and push over smart HTTP. Every repository is its own strongly consistent Durable Object.</p>
195- </div>
196- <div class="box">
197- <h3>{raw(ICONS.pr)} Reviews with intent</h3>
198- <p class="muted m0">Pull requests carry what a change is meant to do, so human and AI reviewers check the right thing.</p>
199- </div>
200- <div class="box">
201- <h3>{raw(ICONS.play)} Actions-compatible CI</h3>
202- <p class="muted m0">Bring your existing workflow YAML. Jobs run in containers with live logs, caches and artifacts.</p>
203- </div>
204- <div class="box">
205- <h3>{raw(ICONS.bot)} Agents with guardrails</h3>
206- <p class="muted m0">Agents get their own accounts, tokens and policies. Protected branches keep a human in the loop.</p>
207- </div>
208- </div>
209- </div>
210-);
211-
212151 /** The icon for a notification, by the event that raised it. */
213152 function noteIcon(kind: string): string {
214153 if (kind === "run_failed") return ICONS.failure;
@@ -256,7 +195,7 @@
256195 export function registerAccount(web: WebApp): void {
257196 web.get("/", async (c) => {
258197 const p = c.get("principal");
259- if (p.type !== "account") return render(c, { title: "", home: true }, <Landing early={earlyAccessOpen(c.env) && turnstileMode(c.env) !== "missing"} />);
198+ if (p.type !== "account") return landingPage(c);
260199 const me = p.account;
261200 const t = csrfToken(c);
262201 if (me.status === "disabled") {
modifiedapps/diffinery/src/web/assets.ts+3−0View file
@@ -75,6 +75,9 @@
7575 .flash.info{border-color:color-mix(in srgb,var(--blue) 40%,transparent);background:var(--blue-soft)}
7676 .flash.warn{border-color:color-mix(in srgb,var(--amber) 40%,transparent);background:var(--amber-soft);color:var(--amber)}
7777 .site-foot{display:flex;justify-content:center;gap:20px;max-width:1280px;margin:0 auto;padding:16px 20px 32px;border-top:1px solid var(--border2);font-size:12px}.site-foot a{color:var(--fg2)}.site-foot a:hover{color:var(--accent)}
78+.foot-cols{display:grid;grid-template-columns:repeat(4,minmax(0,170px));justify-content:center;gap:24px;width:100%;padding-top:12px}
79+.foot-cols nav{display:flex;flex-direction:column;gap:7px}.foot-cols h2{font-size:12px;font-weight:600;color:var(--fg);margin:0 0 2px}
80+@media (max-width:600px){.foot-cols{grid-template-columns:repeat(2,minmax(0,1fr))}.top nav a.nav-site{display:none}}
7881 /* Controls follow Kumo sizes: base 36px (buttons, inputs, selects), sm 26px. */
7982 .btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:36px;padding:0 12px;border:1px solid var(--border);border-radius:var(--r);background:var(--bg);color:var(--fg);font:500 14px/1 var(--sans);cursor:pointer;white-space:nowrap;text-decoration:none!important;vertical-align:middle;flex:none}
8083 .btn svg{flex:none}
modifiedapps/diffinery/src/web/routes.tsx+3−0View file
@@ -23,6 +23,7 @@
2323 import { registerRepoSettings } from "./repo-settings.js";
2424 import { registerImports } from "./imports.js";
2525 import { registerSeo } from "./seo.js";
26+import { registerSite } from "./site.js";
2627 import { errorPage } from "./ui.js";
2728 import { RESERVED_LOGINS } from "../lib/util.js";
2829 import { registerKumoAssets } from "./kumo.js";
@@ -65,6 +66,8 @@
6566 registerSeo(web);
6667 // Public docs (/docs, /llms.txt, ...): before /:owner/:repo, which would otherwise match /docs/<page>.
6768 registerDocs(web);
69+// The product, comparison and company pages (/features, /compare/github, ...) and /index.md: before /:owner.
70+registerSite(web);
6871
6972 // Order matters: fixed paths (/settings/tokens, /notifications/:id, …) must be
7073 // registered before the /:owner/:repo patterns they would otherwise match.
modifiedapps/diffinery/src/web/seo.tsx+16−9View file
@@ -13,6 +13,7 @@
1313 import type { WebApp } from "./ui.js";
1414 import { ICON_FILES } from "./icons.generated.js";
1515 import { docsSitemapPaths } from "../docs/web.js";
16+import { siteSitemapEntries } from "./site.js";
1617
1718 export const SITE = {
1819 name: "Diffinery",
@@ -74,12 +75,17 @@
7475 web.get("/robots.txt", (c) => {
7576 const origin = c.env.PUBLIC_URL.replace(/\/$/, "");
7677 const lines = [
78+ // Search engines and AI assistants are welcome on every public page: the site, the docs, llms.txt
79+ // and public repositories. Content-Signal (contentsignals.org) says so explicitly, for crawlers that read it.
7780 "User-agent: *",
81+ "Content-Signal: search=yes, ai-input=yes, ai-train=yes",
7882 "Allow: /",
7983 ...["/api/", "/mcp", "/runner/", "/auth/", "/login", "/signin", "/invite/", "/settings", "/notifications", "/new", "/admin", "/request-access", "/early-access/confirm", "/early-access/remove", "/-/", "/*.git/", "/*/raw/"].map((p) => `Disallow: ${p}`),
8084 "",
8185 `Sitemap: ${origin}/sitemap.xml`,
8286 "",
87+ `# For AI assistants: ${origin}/llms.txt indexes the site and the docs as Markdown; ${origin}/llms-full.txt holds all of it in one file.`,
88+ "",
8389 ];
8490 return c.text(lines.join("\n"), 200, { "cache-control": "public, max-age=86400" });
8591 });
@@ -91,7 +97,8 @@
9197 const body = [
9298 '<?xml version="1.0" encoding="UTF-8"?>',
9399 '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
94- url(`${origin}/`),
100+ // The home page, then the product, comparison and company pages (web/site.tsx).
101+ ...siteSitemapEntries().map((e) => url(`${origin}${e.path}`, e.updated)),
95102 // The early access form, while the instance is invite-only (lib/early-access.ts).
96103 ...(earlyAccessOpen(c.env) ? [url(`${origin}/early-access`)] : []),
97104 ...docsSitemapPaths().map((p) => url(`${origin}${p}`)),
@@ -107,8 +114,12 @@
107114 description?: string;
108115 /** Keep the page out of search results. */
109116 noindex?: boolean;
110- /** Add WebSite and Organization structured data (the home page). */
117+ /** The home page. */
111118 home?: boolean;
119+ /** Structured data (schema.org objects, without @context), as one JSON-LD graph. */
120+ jsonLd?: object[];
121+ /** The date an article's facts were last checked (YYYY-MM-DD): marks the page as an article. */
122+ modified?: string;
112123 nonce: string;
113124 };
114125
@@ -120,12 +131,7 @@
120131 const description = o.description?.trim() || SITE.description;
121132 const noindex = o.noindex || PRIVATE.test(path);
122133 const image = `${origin}/og-image.png`;
123- const ld = o.home
124- ? JSON.stringify([
125- { "@context": "https://schema.org", "@type": "WebSite", name: SITE.name, url: `${origin}/` },
126- { "@context": "https://schema.org", "@type": "Organization", name: SITE.name, url: `${origin}/`, logo: `${origin}/icon-512.png` },
127- ]).replace(/</g, "\\u003c")
128- : null;
134+ const ld = o.jsonLd?.length && !noindex ? JSON.stringify({ "@context": "https://schema.org", "@graph": o.jsonLd }).replace(/</g, "\\u003c") : null;
129135 return (
130136 <>
131137 <meta name="description" content={description} />
@@ -139,7 +145,8 @@
139145 <meta name="theme-color" content={SITE.color} />
140146 <meta name="application-name" content={SITE.name} />
141147 <meta name="apple-mobile-web-app-title" content={SITE.name} />
142- <meta property="og:type" content="website" />
148+ <meta property="og:type" content={o.modified ? "article" : "website"} />
149+ {o.modified ? <meta property="article:modified_time" content={o.modified} /> : null}
143150 <meta property="og:site_name" content={SITE.name} />
144151 <meta property="og:title" content={o.title} />
145152 <meta property="og:description" content={description} />
addedapps/diffinery/src/web/site.tsx+590−0View file
@@ -1,0 +1,590 @@
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+ */
12+import { raw } from "hono/html";
13+import type { FC } from "hono/jsx";
14+import type { AppContext } from "../auth/principal.js";
15+import { turnstileMode } from "../auth/signup.js";
16+import { SITE_NAV, SITE_PAGES } from "../docs/content.generated.js";
17+import { earlyAccessOpen } from "../lib/early-access.js";
18+import { imageProxy } from "../lib/camo.js";
19+import { ICONS } from "./assets.js";
20+import { fenceHighlighter } from "./highlight.js";
21+import { escapeHtml, fenceLangs, renderMarkdown } from "./markdown.js";
22+import { SITE } from "./seo.js";
23+import { render, type WebApp } from "./ui.js";
24+
25+export interface SitePageInfo {
26+ /** URL path, e.g. "/compare/github". */
27+ path: string;
28+ title: string;
29+ description: string;
30+ markdown: string;
31+ /** The date its facts were last checked, YYYY-MM-DD. */
32+ updated: string;
33+ seoTitle: string | null;
34+ /** The site/nav.json section it belongs to ("Product", "Compare", "Company"). */
35+ section: string;
36+}
37+
38+const origin = (c: AppContext) => c.env.PUBLIC_URL.replace(/\/$/, "");
39+
40+/** Every site page, in the order of site/nav.json. */
41+export function sitePages(): SitePageInfo[] {
42+ return SITE_NAV.flatMap((s) => s.pages.map((path) => ({ path, ...SITE_PAGES[path]!, section: s.title })));
43+}
44+
45+export 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. */
50+const absolute = (md: string, o: string) => md.replace(/\]\(\/(?!\/)/g, `](${o}/`);
51+
52+const 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. */
55+export 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. */
62+export 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. */
69+export 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. */
80+export const HOME_UPDATED = "2026-10-16";
81+
82+const HOME = {
83+ // No SITE here: seo.tsx imports this module, so SITE isn't defined yet while it loads.
84+ title: "Diffinery: GitHub alternative with fast CI, built for AI agents",
85+ description:
86+ "Diffinery is code hosting and CI for the agentic era: 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 a 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 Cloudflare.",
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: "Same YAML", label: "Your .github/workflows files run as they are." },
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, an MCP server, sandboxes, tasks and reviews. 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+ workflow: `name: CI
104+on: [push, pull_request]
105+jobs:
106+ test:
107+ runs-on: ubuntu-latest # 4 vCPU, 12 GiB on Diffinery
108+ steps:
109+ - uses: actions/checkout@v4
110+ - uses: actions/setup-node@v4
111+ with: { node-version: 22, cache: pnpm }
112+ - run: pnpm install --frozen-lockfile
113+ - run: pnpm test`,
114+ compare: {
115+ cols: ["Diffinery", "GitHub", "GitHub with a runner service"],
116+ rows: [
117+ ["Git hosting, pull requests, issues", "Yes", "Yes", "Yes, on GitHub"],
118+ ["Runs GitHub Actions workflows", "Yes, as they are", "Yes", "Yes, with a new runs-on label"],
119+ ["Default Linux runner, private repositories", "4 vCPU, 12 GiB", "2 vCPU, 8 GB", "Your choice of size, billed by the vendor"],
120+ ["Guardrails for AI agents", "Built in for every agent account", "Rulesets and permissions you configure", "Rulesets and permissions you configure"],
121+ ["Built-in MCP server and agent sandboxes", "Yes", "MCP server, Codespaces", "GitHub's"],
122+ ["Hosted macOS and Windows runners", "No (self-hosted)", "Yes", "Depends on the vendor"],
123+ ["Vendors to manage", "One", "One", "Two"],
124+ ],
125+ },
126+ faq: [
127+ {
128+ q: "What is Diffinery?",
129+ 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.",
130+ },
131+ {
132+ q: "Is Diffinery an alternative to GitHub?",
133+ 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).",
134+ },
135+ {
136+ q: "Do my GitHub Actions workflows run on Diffinery?",
137+ 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).",
138+ },
139+ {
140+ q: "How is Diffinery different from Blacksmith or Depot?",
141+ 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, so there's one platform and one vendor. See [Diffinery vs Blacksmith](/compare/blacksmith) and [Diffinery vs Depot](/compare/depot).",
142+ },
143+ {
144+ q: "How does Diffinery work with Claude Code, Codex and other AI agents?",
145+ a: "Agents connect over MCP at `https://diffinery.com/mcp` 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).",
146+ },
147+ {
148+ q: "How do I move from GitHub?",
149+ 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).",
150+ },
151+ {
152+ q: "Can I use Diffinery today?",
153+ a: "Diffinery is invite-only while it opens up team by team. [Request early access](/early-access) and we'll send an invitation. Pricing hasn't been announced yet.",
154+ },
155+ ],
156+};
157+
158+/** The home page as Markdown, for agents: /index.md, and / with `Accept: text/markdown`. */
159+export function homeMarkdown(o: string): string {
160+ const lines = [
161+ `# ${SITE.name}`,
162+ "",
163+ `> ${HOME.description}`,
164+ "",
165+ `Source: ${o}/ (last updated ${HOME_UPDATED})`,
166+ "",
167+ `## ${HOME.h1}`,
168+ "",
169+ HOME.lead,
170+ "",
171+ ...HOME.stats.map((s) => `- **${s.value}**: ${s.label}`),
172+ "",
173+ "## What you get",
174+ "",
175+ ...HOME.features.map((f) => `- **[${f.title}](${f.href})**: ${f.text}`),
176+ "",
177+ "## Your workflows, as they are",
178+ "",
179+ "```yaml",
180+ HOME.workflow,
181+ "```",
182+ "",
183+ "## How Diffinery compares",
184+ "",
185+ `| | ${HOME.compare.cols.join(" | ")} |`,
186+ `| --- | ${HOME.compare.cols.map(() => "---").join(" | ")} |`,
187+ ...HOME.compare.rows.map((r) => `| ${r.join(" | ")} |`),
188+ "",
189+ "Runner services include Blacksmith and Depot. See [all comparisons](/compare).",
190+ "",
191+ "## Frequently asked questions",
192+ "",
193+ ...HOME.faq.flatMap((f) => [`### ${f.q}`, "", f.a, ""]),
194+ ];
195+ return absolute(lines.join("\n"), o);
196+}
197+
198+/** schema.org ids for the site's entities, shared by every page's graph. */
199+const ids = (o: string) => ({ org: `${o}/#organization`, site: `${o}/#website`, app: `${o}/#software` });
200+
201+function entities(o: string): object[] {
202+ const id = ids(o);
203+ return [
204+ { "@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 },
205+ { "@type": "WebSite", "@id": id.site, name: SITE.name, url: `${o}/`, inLanguage: "en", publisher: { "@id": id.org } },
206+ {
207+ "@type": "SoftwareApplication",
208+ "@id": id.app,
209+ name: SITE.name,
210+ url: `${o}/`,
211+ applicationCategory: "DeveloperApplication",
212+ applicationSubCategory: "Git hosting and continuous integration",
213+ operatingSystem: "Web",
214+ description: HOME.lead,
215+ featureList: HOME.features.map((f) => `${f.title}: ${f.text}`),
216+ publisher: { "@id": id.org },
217+ sameAs: [`${o}/docs`],
218+ },
219+ ];
220+}
221+
222+function faqPage(url: string, list: Array<{ q: string; a: string }>): object[] {
223+ if (!list.length) return [];
224+ return [{ "@type": "FAQPage", "@id": `${url}#faq`, url, mainEntity: list.map((f) => ({ "@type": "Question", name: f.q, acceptedAnswer: { "@type": "Answer", text: plainText(f.a) } })) }];
225+}
226+
227+/** Whether visitors are offered the early access form (as the old landing page did). */
228+const earlyOffered = (c: AppContext) => earlyAccessOpen(c.env) && turnstileMode(c.env) !== "missing";
229+
230+const Cta: FC<{ early: boolean }> = ({ early }) =>
231+ early ? (
232+ <>
233+ <a class="btn primary" href="/early-access">
234+ Request early access
235+ </a>
236+ <a class="btn" href="/docs">
237+ Read the docs
238+ </a>
239+ </>
240+ ) : (
241+ <>
242+ <a class="btn primary" href="/login">
243+ Sign in with a passkey
244+ </a>
245+ <a class="btn" href="/docs">
246+ Read the docs
247+ </a>
248+ </>
249+ );
250+
251+/** Inline Markdown (links and code) of a home page answer, as HTML. */
252+const inline = (md: string) => renderMarkdown(md, { idPrefix: "" }).replace(/^<p>|<\/p>\s*$/g, "");
253+
254+/** The home page for visitors. `early` shows the early access request (lib/early-access.ts) while the instance is invite-only. */
255+export const Landing: FC<{ early: boolean }> = ({ early }) => (
256+ <div class="mk">
257+ <style>{raw(MARKETING_CSS)}</style>
258+ <section class="hero">
259+ {early ? (
260+ <a class="ea-eyebrow" href="/early-access">
261+ {raw(ICONS.rocket)} Invite-only for now: request early access
262+ </a>
263+ ) : null}
264+ <h1>{HOME.h1}</h1>
265+ <p>{HOME.lead}</p>
266+ <p class="row" style="justify-content:center">
267+ <Cta early={early} />
268+ </p>
269+ {early ? (
270+ <p class="muted small">
271+ Diffinery is invite-only while we open it up team by team. Got an invitation? Open its link to create your passkey; there are no passwords.
272+ </p>
273+ ) : (
274+ <p class="muted small">Diffinery is invite-only. Open the invite link someone sent you to create your passkey; there are no passwords.</p>
275+ )}
276+ </section>
277+ <ul class="mk-stats" aria-label="Diffinery in numbers">
278+ {HOME.stats.map((s) => (
279+ <li>
280+ <b>{s.value}</b>
281+ <span>{s.label}</span>
282+ </li>
283+ ))}
284+ </ul>
285+ <section class="mk-sec">
286+ <h2>Everything your team and its agents need, in one place</h2>
287+ <div class="features">
288+ {HOME.features.map((f) => (
289+ <a class="box mk-card" href={f.href}>
290+ <h3>
291+ {raw(ICONS[f.icon as keyof typeof ICONS])} {f.title}
292+ </h3>
293+ <p class="muted m0">{f.text}</p>
294+ </a>
295+ ))}
296+ </div>
297+ </section>
298+ <section class="mk-sec mk-split">
299+ <div>
300+ <h2>Your workflows, as they are</h2>
301+ <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>
302+ <ul>
303+ <li>Each job gets a fresh Ubuntu 24.04 container with 4 vCPUs and 12 GiB of memory.</li>
304+ <li>Caches restore in parallel and extract while they download.</li>
305+ <li>Self-hosted runners cover macOS, Windows, Arm and GPUs.</li>
306+ </ul>
307+ <p>
308+ <a href="/performance">See the measurements</a> · <a href="/docs/actions/compatibility">Compatibility with GitHub Actions</a>
309+ </p>
310+ </div>
311+ {/* One line: whitespace inside <pre> shows. */}
312+ {raw(`<pre class="mk-code" aria-label="A GitHub Actions workflow that runs on Diffinery unchanged"><code>${escapeHtml(HOME.workflow)}</code></pre>`)}
313+ </section>
314+ <section class="mk-sec">
315+ <h2>Built for AI coding agents</h2>
316+ <p class="mk-wide">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. The server keeps a person in charge of what lands.</p>
317+ <div class="features">
318+ <div class="box mk-card">
319+ <h3>{raw(ICONS.bot)} Agent accounts</h3>
320+ <p class="muted m0">Their own logins and tokens, owned by a person or organization, with every commit stamped with who they act for.</p>
321+ </div>
322+ <div class="box mk-card">
323+ <h3>{raw(ICONS.shield)} Guardrails</h3>
324+ <p class="muted m0">No pushes to protected branches, a person's approval on every agent pull request, and no secrets for agent-written code.</p>
325+ </div>
326+ <div class="box mk-card">
327+ <h3>{raw(ICONS.code)} MCP, sandboxes and tasks</h3>
328+ <p class="muted m0">One URL connects any MCP app. Agents get sandboxes to build and test in, and tasks turn a goal into a pull request.</p>
329+ </div>
330+ </div>
331+ <p>
332+ <a href="/agents">Diffinery for AI agents</a> · <a href="/docs/connect">Connect Claude, Claude Code or Codex</a>
333+ </p>
334+ </section>
335+ <section class="mk-sec">
336+ <h2>How Diffinery compares</h2>
337+ <div class="table-wrap">
338+ <table class="mk-table">
339+ <thead>
340+ <tr>
341+ <th></th>
342+ {HOME.compare.cols.map((h) => (
343+ <th scope="col">{h}</th>
344+ ))}
345+ </tr>
346+ </thead>
347+ <tbody>
348+ {HOME.compare.rows.map((r) => (
349+ <tr>
350+ <th scope="row">{r[0]}</th>
351+ {r.slice(1).map((v) => (
352+ <td>{v}</td>
353+ ))}
354+ </tr>
355+ ))}
356+ </tbody>
357+ </table>
358+ </div>
359+ <p class="muted small">
360+ Runner services include Blacksmith and Depot. GitHub's runner sizes are from its documentation, checked {longDate(HOME_UPDATED)}. Read the full comparisons with{" "}
361+ <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>.
362+ </p>
363+ </section>
364+ <section class="mk-sec mk-faq" id="faq">
365+ <h2>Frequently asked questions</h2>
366+ {HOME.faq.map((f) => (
367+ <div class="mk-q">
368+ <h3>{f.q}</h3>
369+ <p>{raw(inline(f.a))}</p>
370+ </div>
371+ ))}
372+ </section>
373+ <section class="mk-band">
374+ <h2>Bring your next project to Diffinery</h2>
375+ <p class="muted">Your workflows, your editor and gh keep working. Your agents get guardrails.</p>
376+ <p class="row" style="justify-content:center">
377+ <Cta early={early} />
378+ </p>
379+ </section>
380+ </div>
381+);
382+
383+/** The home page for signed-out visitors (registerAccount calls this for "/"). */
384+export function landingPage(c: AppContext): Response | Promise<Response> {
385+ const o = origin(c);
386+ if (wantsMarkdown(c)) return markdownResponse(c, homeMarkdown(o));
387+ c.header("link", `<${o}/index.md>; rel="alternate"; type="text/markdown", <${o}/llms.txt>; rel="describedby"; type="text/plain"`);
388+ c.header("vary", "Accept");
389+ return render(
390+ c,
391+ {
392+ title: "",
393+ home: true,
394+ seoTitle: HOME.title,
395+ description: HOME.description,
396+ 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)],
397+ head: <link rel="alternate" type="text/markdown" href="/index.md" title="Diffinery (Markdown)" />,
398+ },
399+ <Landing early={earlyOffered(c)} />,
400+ );
401+}
402+
403+// ---------------------------------------------------------------- the Markdown pages (site/)
404+
405+const wantsMarkdown = (c: AppContext) => /\btext\/markdown\b/i.test(c.req.header("accept") ?? "");
406+const PUBLIC = { "cache-control": "public, max-age=300", "access-control-allow-origin": "*", "x-content-type-options": "nosniff" };
407+const markdownResponse = (c: AppContext, body: string) => c.body(body, 200, { ...PUBLIC, "content-type": "text/markdown; charset=utf-8", vary: "Accept" });
408+
409+const htmlCache = new Map<string, string>();
410+async function pageHtml(c: AppContext, p: SitePageInfo): Promise<string> {
411+ let html = htmlCache.get(p.path);
412+ if (html === undefined) {
413+ html = renderMarkdown(p.markdown, { idPrefix: "", highlight: await fenceHighlighter(fenceLangs(p.markdown)), image: imageProxy(c.env) });
414+ htmlCache.set(p.path, html);
415+ }
416+ return html;
417+}
418+
419+/** Comparisons and measurements are articles: dated, with sources. */
420+const isArticle = (p: SitePageInfo) => p.path.startsWith("/compare/") || p.path === "/performance";
421+
422+function crumbs(p: SitePageInfo): Array<{ name: string; path: string }> {
423+ const out = [{ name: "Home", path: "/" }];
424+ if (p.path.startsWith("/compare/")) out.push({ name: "Compare", path: "/compare" });
425+ out.push({ name: p.title, path: p.path });
426+ return out;
427+}
428+
429+async function sitePageResponse(c: AppContext, p: SitePageInfo): Promise<Response> {
430+ const o = origin(c);
431+ const url = `${o}${p.path}`;
432+ const html = await pageHtml(c, p);
433+ const id = ids(o);
434+ const trail = crumbs(p);
435+ const related = sitePages().filter((x) => x.section === p.section && x.path !== p.path);
436+ const early = earlyOffered(c);
437+ c.header("link", `<${o}${p.path}.md>; rel="alternate"; type="text/markdown", <${o}/llms.txt>; rel="describedby"; type="text/plain"`);
438+ c.header("vary", "Accept");
439+ const page = {
440+ "@type": isArticle(p) ? "Article" : p.path === "/about" ? "AboutPage" : "WebPage",
441+ "@id": `${url}#page`,
442+ url,
443+ name: p.title,
444+ ...(isArticle(p) ? { headline: p.title } : {}),
445+ description: p.description,
446+ inLanguage: "en",
447+ dateModified: p.updated,
448+ ...(isArticle(p) ? { author: { "@id": id.org } } : {}),
449+ publisher: { "@id": id.org },
450+ isPartOf: { "@id": id.site },
451+ about: { "@id": id.app },
452+ breadcrumb: { "@id": `${url}#breadcrumb` },
453+ };
454+ const breadcrumb = { "@type": "BreadcrumbList", "@id": `${url}#breadcrumb`, itemListElement: trail.map((t, i) => ({ "@type": "ListItem", position: i + 1, name: t.name, item: `${o}${t.path}` })) };
455+ return render(
456+ c,
457+ {
458+ title: p.title,
459+ seoTitle: p.seoTitle ?? undefined,
460+ description: p.description,
461+ modified: isArticle(p) ? p.updated : undefined,
462+ jsonLd: [...entities(o), page, breadcrumb, ...faqPage(url, faqs(p.markdown))],
463+ head: <link rel="alternate" type="text/markdown" href={`${p.path}.md`} title={`${p.title} (Markdown)`} />,
464+ },
465+ <article class="mk mk-page">
466+ <style>{raw(MARKETING_CSS)}</style>
467+ <header class="mk-head">
468+ <nav class="mk-crumbs" aria-label="Breadcrumb">
469+ {trail.slice(0, -1).map((t) => (
470+ <>
471+ <a href={t.path}>{t.name}</a> <span class="faint">/</span>{" "}
472+ </>
473+ ))}
474+ </nav>
475+ <h1>{p.title}</h1>
476+ <p class="mk-lead">{p.description}</p>
477+ <p class="mk-meta muted small">
478+ Last updated <time datetime={p.updated}>{longDate(p.updated)}</time> ·{" "}
479+ <a href={`${p.path}.md`} title="This page as plain Markdown">
480+ View as Markdown
481+ </a>
482+ </p>
483+ </header>
484+ <div class="markdown mk-body">{raw(html)}</div>
485+ <aside class="mk-band">
486+ <h2>Try Diffinery with your own workflows</h2>
487+ <p class="muted">{early ? "Diffinery is invite-only while we open it up team by team." : "Sign in with the passkey from your invitation."}</p>
488+ <p class="row" style="justify-content:center">
489+ <Cta early={early} />
490+ </p>
491+ </aside>
492+ {related.length ? (
493+ <nav class="mk-related" aria-label={`More in ${p.section}`}>
494+ <h2>More in {p.section}</h2>
495+ <ul>
496+ {related.map((r) => (
497+ <li>
498+ <a href={r.path}>{r.title}</a>
499+ <span class="muted small">{r.description}</span>
500+ </li>
501+ ))}
502+ </ul>
503+ </nav>
504+ ) : null}
505+ </article>,
506+ );
507+}
508+
509+/** The sitemap's entries for the site: the home page first, then every page with the date it was last checked. */
510+export function siteSitemapEntries(): Array<{ path: string; updated: string }> {
511+ return [{ path: "/", updated: HOME_UPDATED }, ...sitePages().map((p) => ({ path: p.path, updated: p.updated }))];
512+}
513+
514+/** The site's part of /llms.txt: what Diffinery is, then every site page as Markdown. */
515+export function siteLlmsLines(o: string): string[] {
516+ const lines = [
517+ "## About Diffinery",
518+ "",
519+ `- [Home page](${o}/index.md): ${HOME.description}`,
520+ `- Status: early access, invite-only; request an invitation at ${o}/early-access. Pricing hasn't been announced.`,
521+ "- Runs entirely on Cloudflare (Workers, Durable Objects, R2, D1, Queues, Workflows, Containers).",
522+ "- 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.",
523+ "- Runs GitHub Actions workflow files from `.github/workflows/` as they are; differences are listed at " + `${o}/docs/actions/compatibility.md`,
524+ "",
525+ ];
526+ for (const s of SITE_NAV) {
527+ lines.push(`## ${s.title}`, "");
528+ for (const path of s.pages) {
529+ const p = sitePage(path)!;
530+ lines.push(`- [${p.title}](${o}${path}.md): ${p.description} (updated ${p.updated})`);
531+ }
532+ lines.push("");
533+ }
534+ return lines;
535+}
536+
537+/** The site's pages for /llms-full.txt, home page first. */
538+export function siteFullMarkdown(o: string): string[] {
539+ return [homeMarkdown(o), ...sitePages().map((p) => sitePageMarkdown(p, o))];
540+}
541+
542+export function registerSite(web: WebApp): void {
543+ web.get("/index.md", (c) => markdownResponse(c, homeMarkdown(origin(c))));
544+ for (const p of sitePages()) {
545+ web.get(`${p.path}.md`, (c) => markdownResponse(c, sitePageMarkdown(p, origin(c))));
546+ web.get(`${p.path}/`, (c) => c.redirect(p.path, 301));
547+ web.get(p.path, (c) => (wantsMarkdown(c) ? markdownResponse(c, sitePageMarkdown(p, origin(c))) : sitePageResponse(c, p)));
548+ }
549+}
550+
551+export const MARKETING_CSS = `
552+.mk .hero h1{max-width:820px;margin-left:auto;margin-right:auto;font-size:48px;line-height:1.08}
553+.mk .hero>p:first-of-type{max-width:720px}
554+.mk-stats{list-style:none;margin:8px 0 8px;padding:0;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
555+.mk-stats li{border:1px solid var(--border);border-radius:var(--r);background:var(--bg);padding:16px}
556+.mk-stats b{display:block;font-size:24px;letter-spacing:-.02em;margin-bottom:4px}
557+.mk-stats span{color:var(--fg2);font-size:13.5px;line-height:1.45}
558+.mk-sec{margin:56px 0 0}
559+.mk-sec>h2,.mk-band h2,.mk-related h2{font-size:26px;letter-spacing:-.02em;margin:0 0 10px}
560+.mk-wide{max-width:760px;color:var(--fg2);font-size:16px}
561+.mk .features{grid-template-columns:repeat(3,minmax(0,1fr))}
562+.mk-card{display:block;color:var(--fg)}a.mk-card:hover{border-color:var(--accent);text-decoration:none}
563+.mk-card h3{display:flex;align-items:center;gap:8px}.mk-card h3 svg{flex:none;color:var(--accent)}
564+.mk-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:32px;align-items:center}
565+.mk-split>*{min-width:0}
566+.mk .table-wrap{overflow:auto}
567+.mk-split p,.mk-split li{color:var(--fg2);font-size:15px;line-height:1.65}
568+.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}
569+.mk-table{width:100%;border-collapse:collapse;font-size:14px}
570+.mk-table th,.mk-table td{border-bottom:1px solid var(--border2);padding:10px 12px;text-align:left;vertical-align:top}
571+.mk-table thead th{font-weight:600;border-bottom:1px solid var(--border)}.mk-table thead th:nth-child(2){color:var(--accent)}
572+.mk-table tbody th{font-weight:500;color:var(--fg2)}
573+.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}
574+.mk-band{margin:56px 0 0;padding:36px 20px;text-align:center;border:1px solid var(--border);border-radius:var(--r);background:var(--bg2)}
575+.mk-band p{margin:0 auto 14px;max-width:620px}
576+.mk-page{max-width:880px;margin:0 auto}
577+.mk-head{padding:28px 0 18px;border-bottom:1px solid var(--border2);margin-bottom:8px}
578+.mk-head h1{font-size:36px;line-height:1.15;letter-spacing:-.025em;margin:8px 0 10px}
579+.mk-crumbs{font-size:13px;min-height:18px}
580+.mk-lead{font-size:18px;color:var(--fg2);margin:0 0 10px;line-height:1.55}
581+.mk-meta{margin:0}
582+.mk-body{font-size:15.5px;line-height:1.7}
583+.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}
584+.mk-body table{font-size:14px;width:100%}.mk-body td code{white-space:nowrap}
585+.mk-related{margin:40px 0 0}
586+.mk-related ul{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px}
587+.mk-related li{border:1px solid var(--border);border-radius:var(--r);padding:12px 14px;display:flex;flex-direction:column;gap:4px}
588+@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)}}
589+@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}}
590+`;
modifiedapps/diffinery/src/web/ui.tsx+47−4View file
@@ -297,6 +297,12 @@
297297 description?: string;
298298 /** The home page: a descriptive title and structured data for search engines. */
299299 home?: boolean;
300+ /** The whole <title>, when the page's own wording is better for search results than "<title> · Diffinery". */
301+ seoTitle?: string;
302+ /** Structured data (schema.org objects) for search engines and AI assistants. */
303+ jsonLd?: object[];
304+ /** An article (comparisons, measurements): its last-checked date goes in the social meta tags. */
305+ modified?: string;
300306 /** Extra elements for <head> (e.g. alternate links). */
301307 head?: Child;
302308 /** A page script, run after the site's own (it gets the page's CSP nonce). */
@@ -425,6 +431,25 @@
425431 return v === "light" || v === "dark" ? v : null;
426432 }
427433
434+/** Links to the public site for visitors: every page reachable from every page, for people and crawlers. */
435+const SiteFooter: FC = () => (
436+ <div class="foot-cols">
437+ {[
438+ { title: "Product", links: [["/features", "Features"], ["/agents", "For AI agents"], ["/performance", "Performance"], ["/security", "Security"], ["/docs/migrate-from-github", "Migrate from GitHub"]] },
439+ { title: "Compare", links: [["/compare", "All comparisons"], ["/compare/github", "vs GitHub"], ["/compare/gitlab", "vs GitLab"], ["/compare/blacksmith", "vs Blacksmith"], ["/compare/depot", "vs Depot"]] },
440+ { title: "Resources", links: [["/docs", "Docs"], ["/docs/connect", "Connect AI apps"], ["/llms.txt", "llms.txt"], ["/status", "Status"], ["/docs/security/disclosure", "Report a vulnerability"]] },
441+ { title: "Company", links: [["/about", "About"], ["/early-access", "Request early access"]] },
442+ ].map((col) => (
443+ <nav aria-label={col.title}>
444+ <h2>{col.title}</h2>
445+ {col.links.map(([href, label]) => (
446+ <a href={href}>{label}</a>
447+ ))}
448+ </nav>
449+ ))}
450+ </div>
451+);
452+
428453 function renderPage(c: AppContext, opts: PageOpts, body: Child, watch?: WatchLevel): Response | Promise<Response> {
429454 csrfToken(c);
430455 const nonce = randomToken(16);
@@ -438,7 +463,7 @@
438463 c.header("referrer-policy", "same-origin");
439464 c.header("cache-control", "private, no-store");
440465 const r = opts.repo;
441- const title = opts.home ? `${SITE.name}: ${SITE.tagline}` : opts.title ? `${opts.title} · ${SITE.name}` : SITE.name;
466+ const title = opts.seoTitle ?? (opts.home ? `${SITE.name}: ${SITE.tagline}` : opts.title ? `${opts.title} · ${SITE.name}` : SITE.name);
442467 const theme = appearanceOf(c);
443468 const page = (
444469 <html lang="en" data-theme={theme ?? undefined} data-mode={theme ?? undefined}>
@@ -453,6 +478,8 @@
453478 description: opts.description ?? (r?.repo.description ? `${r.repo.description} · ${r.repo.owner_login}/${r.repo.name} on Diffinery` : r ? `${r.repo.owner_login}/${r.repo.name} on Diffinery` : undefined),
454479 noindex: (opts.status ?? 200) >= 400 || (r !== undefined && r.repo.visibility !== "public"),
455480 home: opts.home,
481+ jsonLd: opts.jsonLd,
482+ modified: opts.modified,
456483 nonce,
457484 })}
458485 <style>{raw(CSS)}</style>
@@ -480,6 +507,16 @@
480507 </button>
481508 ) : null}
482509 <nav>
510+ {!active ? (
511+ <>
512+ <a href="/features" class="nav-site">
513+ Product
514+ </a>
515+ <a href="/compare" class="nav-site">
516+ Compare
517+ </a>
518+ </>
519+ ) : null}
483520 <a href="/docs" title="Documentation">
484521 Docs
485522 </a>
@@ -518,9 +555,15 @@
518555 {body}
519556 </main>
520557 <footer class="site-foot">
521- <a href="/docs">Docs</a>
522- <a href="/docs/security/disclosure">Security</a>
523- <a href="/status">Status</a>
558+ {active ? (
559+ <>
560+ <a href="/docs">Docs</a>
561+ <a href="/docs/security/disclosure">Security</a>
562+ <a href="/status">Status</a>
563+ </>
564+ ) : (
565+ <SiteFooter />
566+ )}
524567 </footer>
525568 {active ? (
526569 <dialog id="palette" aria-label="Command palette">
modifiedapps/diffinery/test/e2e/passkeys.test.mjs+9−2View file
@@ -589,17 +589,24 @@
589589 assert.match(await (await get("/favicon.svg")).text(), /^<svg/);
590590 assert.equal((await (await get("/site.webmanifest")).json()).icons.length, 3);
591591 const robots = await (await get("/robots.txt")).text();
592- assert.match(robots, /^User-agent: \*\nAllow: \//);
592+ assert.match(robots, /^User-agent: \*\nContent-Signal: search=yes, ai-input=yes, ai-train=yes\nAllow: \//);
593593 assert.match(robots, new RegExp(`Sitemap: ${base}/sitemap.xml`));
594594 assert.match(await (await get("/sitemap.xml")).text(), new RegExp(`<loc>${base}/</loc>`));
595595
596596 const home = await (await get("/")).text();
597- assert.match(home, /<title>Diffinery: Code hosting and CI for the agentic era<\/title>/);
597+ assert.match(home, /<title>Diffinery: GitHub alternative with fast CI, built for AI agents<\/title>/);
598598 assert.match(home, /<link rel="icon" href="\/favicon.ico"/);
599599 assert.match(home, /<meta property="og:image" content="[^"]+\/og-image.png"/);
600600 assert.match(home, /<link rel="canonical" href="[^"]+\/"/);
601601 assert.match(home, /application\/ld\+json/);
602602 assert.doesNotMatch(home, /noindex/);
603+ // The product and comparison pages, as HTML and as Markdown, and in the sitemap.
604+ const compare = await get("/compare/github");
605+ assert.equal(compare.status, 200);
606+ assert.match(await compare.text(), /<h1>Diffinery vs GitHub<\/h1>/);
607+ assert.match(await (await get("/compare/github.md")).text(), /^# Diffinery vs GitHub\n/);
608+ assert.match(await (await get("/index.md")).text(), /^# Diffinery\n/);
609+ assert.match(await (await get("/sitemap.xml")).text(), new RegExp(`<loc>${base}/performance</loc>`));
603610 const login = await (await get("/login")).text();
604611 assert.match(login, /<meta name="robots" content="noindex, nofollow"/);
605612 assert.match(login, /<link rel="icon" href="\/favicon.ico"/);
addedapps/diffinery/test/unit/site.test.ts+189−0View file
@@ -1,0 +1,189 @@
1+/**
2+ * The public site: the home page and the product, comparison and company pages in site/. Every page
3+ * renders for visitors with its search and social tags and structured data, is served as Markdown,
4+ * links only to pages that exist, and is listed in the sitemap, llms.txt and llms-full.txt.
5+ * Needs src/docs/content.generated.ts (`pnpm build` or `pnpm build:docs`).
6+ */
7+import { describe, expect, it } from "vitest";
8+import { app } from "../../src/app.js";
9+import { web } from "../../src/web/routes.js";
10+import { slugify } from "../../src/web/markdown.js";
11+import type { Env } from "../../src/env.js";
12+import { getPage, llmsFullTxt, llmsTxt } from "../../src/docs/pages.js";
13+import { faqs, homeMarkdown, sitePage, sitePages } from "../../src/web/site.js";
14+import { RESERVED_LOGINS } from "../../src/lib/util.js";
15+
16+app.route("/", web);
17+
18+const ORIGIN = "https://diffinery.example";
19+const stmt = { bind: () => stmt, all: async () => ({ results: [] }), first: async () => null, run: async () => ({ success: true, meta: {} }) };
20+const env = { PUBLIC_URL: ORIGIN, DB: { prepare: () => stmt } } as unknown as Env;
21+const ctx = { waitUntil() {}, passThroughOnException() {}, props: {} } as unknown as ExecutionContext;
22+const get = (path: string, headers: Record<string, string> = {}) => app.fetch(new Request(`${ORIGIN}${path}`, { headers }), env, ctx);
23+
24+const pages = sitePages();
25+
26+/** The JSON-LD graph of an HTML page. */
27+function graph(html: string): Array<Record<string, unknown>> {
28+ const m = /<script type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/.exec(html);
29+ expect(m, "structured data").toBeTruthy();
30+ const ld = JSON.parse(m![1]!) as { "@context": string; "@graph": Array<Record<string, unknown>> };
31+ expect(ld["@context"]).toBe("https://schema.org");
32+ return ld["@graph"];
33+}
34+
35+const types = (g: Array<Record<string, unknown>>) => g.map((x) => x["@type"]);
36+
37+/** Markdown without code, so examples aren't checked as links. */
38+const prose = (md: string) => md.replace(/^```[\s\S]*?^```/gm, "").replace(/`[^`\n]*`/g, "");
39+
40+/** Paths outside /docs and the site pages that the site may link to. */
41+const OTHER_PATHS = [/^\/$/, /^\/llms(-full)?\.txt$/, /^\/early-access$/, /^\/status$/, /^\/security\/report$/, /^\/\.well-known\/security\.txt$/, /^\/docs\/api\/openapi\.json$/];
42+
43+function linkProblems(md: string): string[] {
44+ const problems: string[] = [];
45+ for (const m of prose(md).matchAll(/\]\(([^)\s]+)\)/g)) {
46+ const href = m[1]!;
47+ if (/^https?:\/\//.test(href)) {
48+ if (!href.startsWith("https://")) problems.push(`insecure link: ${href}`);
49+ continue;
50+ }
51+ if (!href.startsWith("/")) {
52+ problems.push(`relative link: ${href}`);
53+ continue;
54+ }
55+ const [path, frag] = href.split("#") as [string, string | undefined];
56+ if (path === "/docs" || path.startsWith("/docs/")) {
57+ if (OTHER_PATHS.some((re) => re.test(path))) continue;
58+ const doc = getPage(path === "/docs" ? "index" : path.slice(6), ORIGIN);
59+ if (!doc) problems.push(`no docs page ${path}`);
60+ else if (frag && !doc.markdown.split("\n").some((l) => /^#{2,6} /.test(l) && slugify(l.replace(/^#+ /, "")) === frag)) problems.push(`no heading ${href}`);
61+ continue;
62+ }
63+ const site = sitePage(path);
64+ if (site) {
65+ if (frag && !site.markdown.split("\n").some((l) => /^#{2,6} /.test(l) && slugify(l.replace(/^#+ /, "")) === frag)) problems.push(`no heading ${href}`);
66+ continue;
67+ }
68+ if (!OTHER_PATHS.some((re) => re.test(path))) problems.push(`link to ${path} isn't a page`);
69+ }
70+ return problems;
71+}
72+
73+describe("site pages", () => {
74+ it("has the product, comparison and company pages", () => {
75+ expect(pages.map((p) => p.path)).toEqual(expect.arrayContaining(["/features", "/agents", "/performance", "/security", "/about", "/compare", "/compare/github", "/compare/gitlab", "/compare/blacksmith", "/compare/depot"]));
76+ for (const p of pages) {
77+ expect(p.description.length, p.path).toBeLessThanOrEqual(160);
78+ expect(p.updated, p.path).toMatch(/^\d{4}-\d{2}-\d{2}$/);
79+ expect(p.markdown.length, `${p.path} is too short`).toBeGreaterThan(1500);
80+ }
81+ });
82+
83+ it("keeps the pages' top-level paths away from account names", () => {
84+ for (const p of pages) expect(RESERVED_LOGINS.has(p.path.split("/")[1]!), p.path).toBe(true);
85+ });
86+
87+ it.each(pages.map((p) => [p.path, p] as const))("%s: links resolve", (_path, p) => {
88+ expect(linkProblems(p.markdown)).toEqual([]);
89+ });
90+
91+ it("home page links resolve", () => {
92+ expect(linkProblems(homeMarkdown(""))).toEqual([]);
93+ });
94+
95+ it("every comparison names its sources and the date they were checked", () => {
96+ for (const p of pages.filter((x) => x.path.startsWith("/compare/"))) {
97+ expect(p.markdown, p.path).toMatch(/\[source\]\(https:\/\//);
98+ expect(p.markdown, p.path).toContain("October 16, 2026");
99+ expect(faqs(p.markdown).length, `${p.path} questions`).toBeGreaterThan(0);
100+ }
101+ });
102+
103+ it.each(pages.map((p) => [p.path, p] as const))("%s: renders for visitors with search tags and structured data", async (path, p) => {
104+ const res = await get(path);
105+ expect(res.status).toBe(200);
106+ expect(res.headers.get("link")).toContain(`<${ORIGIN}${path}.md>; rel="alternate"; type="text/markdown"`);
107+ const html = await res.text();
108+ expect(html).toContain(`<link rel="canonical" href="${ORIGIN}${path}"/>`);
109+ expect(html).not.toContain('name="robots"');
110+ expect(html.match(/<h1[ >]/g)?.length).toBe(1);
111+ expect(html).toContain(`<meta name="description" content="`);
112+ expect(html).toContain('href="/features"');
113+ const g = graph(html);
114+ expect(types(g)).toEqual(expect.arrayContaining(["Organization", "WebSite", "SoftwareApplication", "BreadcrumbList"]));
115+ const page = g.find((x) => x["@id"] === `${ORIGIN}${path}#page`)!;
116+ expect(page.dateModified).toBe(p.updated);
117+ const qs = faqs(p.markdown);
118+ const faq = g.find((x) => x["@type"] === "FAQPage") as { mainEntity: Array<{ name: string }> } | undefined;
119+ expect(faq?.mainEntity.length ?? 0).toBe(qs.length);
120+ });
121+
122+ it.each(pages.map((p) => [p.path, p] as const))("%s: is served as Markdown", async (path, p) => {
123+ for (const res of [await get(`${path}.md`), await get(path, { accept: "text/markdown" })]) {
124+ expect(res.status).toBe(200);
125+ expect(res.headers.get("content-type")).toBe("text/markdown; charset=utf-8");
126+ const md = await res.text();
127+ expect(md.startsWith(`# ${p.title}\n\n> ${p.description}\n\nSource: ${ORIGIN}${path} (last updated ${p.updated})`)).toBe(true);
128+ expect(md).not.toMatch(/\]\(\/(?!\/)/);
129+ }
130+ });
131+
132+ it("sends a trailing slash to the page", async () => {
133+ const res = await get("/compare/github/");
134+ expect(res.status).toBe(301);
135+ expect(res.headers.get("location")).toBe("/compare/github");
136+ });
137+});
138+
139+describe("home page", () => {
140+ it("renders for visitors with the product, its questions and a Markdown version", async () => {
141+ const res = await get("/");
142+ expect(res.status).toBe(200);
143+ expect(res.headers.get("link")).toContain(`<${ORIGIN}/index.md>; rel="alternate"; type="text/markdown"`);
144+ const html = await res.text();
145+ expect(html).toContain("<title>Diffinery: GitHub alternative with fast CI, built for AI agents</title>");
146+ expect(html).toContain("Code hosting and CI for the agentic era</h1>");
147+ const g = graph(html);
148+ expect(types(g)).toEqual(expect.arrayContaining(["Organization", "WebSite", "SoftwareApplication", "WebPage", "FAQPage"]));
149+ for (const href of ["/compare/github", "/compare/blacksmith", "/performance", "/agents", "/about", "/llms.txt"]) expect(html, href).toContain(`href="${href}"`);
150+ });
151+
152+ it("is served as Markdown", async () => {
153+ for (const res of [await get("/index.md"), await get("/", { accept: "text/markdown" })]) {
154+ expect(res.status).toBe(200);
155+ expect(res.headers.get("content-type")).toBe("text/markdown; charset=utf-8");
156+ const md = await res.text();
157+ expect(md).toContain("## Frequently asked questions");
158+ expect(md).toContain(`(${ORIGIN}/compare/github)`);
159+ }
160+ });
161+});
162+
163+describe("crawlers and AI assistants", () => {
164+ it("lists every site page in llms.txt, before the docs", () => {
165+ const txt = llmsTxt(ORIGIN);
166+ expect(txt).toContain(`(${ORIGIN}/index.md)`);
167+ for (const p of pages) expect(txt).toContain(`(${ORIGIN}${p.path}.md)`);
168+ expect(txt.indexOf(`${ORIGIN}/compare/github.md`)).toBeLessThan(txt.indexOf(`${ORIGIN}/docs/quickstart.md`));
169+ });
170+
171+ it("holds every site page in llms-full.txt", () => {
172+ const txt = llmsFullTxt(ORIGIN);
173+ expect(txt).toContain(`Source: ${ORIGIN}/ (last updated`);
174+ for (const p of pages) expect(txt).toContain(`# ${p.title}\n\n> ${p.description}\n\nSource: ${ORIGIN}${p.path}`);
175+ });
176+
177+ it("lists the site in the sitemap with the date each page was checked", async () => {
178+ const xml = await (await get("/sitemap.xml")).text();
179+ expect(xml.match(new RegExp(`<loc>${ORIGIN}/</loc>`, "g"))?.length).toBe(1);
180+ for (const p of pages) expect(xml).toContain(`<url><loc>${ORIGIN}${p.path}</loc><lastmod>${p.updated}</lastmod></url>`);
181+ });
182+
183+ it("welcomes search engines and AI assistants in robots.txt", async () => {
184+ const txt = await (await get("/robots.txt")).text();
185+ expect(txt).toContain("User-agent: *\nContent-Signal: search=yes, ai-input=yes, ai-train=yes\nAllow: /");
186+ expect(txt).toContain(`Sitemap: ${ORIGIN}/sitemap.xml`);
187+ for (const p of pages) expect(txt).not.toMatch(new RegExp(`^Disallow: ${p.path}$`, "m"));
188+ });
189+});