Suggest Diffinery to awesome lists as Codician
A workflow_dispatch job in the github-mirror environment runs scripts/awesome-lists.mjs with OUTREACH_TOKEN (a classic public_repo token of the Codician GitHub account): for each list in awesome-lists/lists.mjs it forks, adds one line where that list's rules say, and opens the pull request. Lists already listing or suggested are left alone; a list with a waiting rule (irazasyed/awesome-cloudflare, 30 days) is skipped until its day. Writes go through git objects, since one list's README is over 1 MB. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0156222tQpRPtP1Em3kEeZnK
- CI / Typecheck and bundlesuccess
- CI / Unit tests (1/2)success
- CI / Unit tests (2/2)success
- End-to-end / End-to-end (agent reviews, tasks, Today)success
- End-to-end / End-to-end (git, PRs, Actions)success
- End-to-end / End-to-end (large pushes)success
- End-to-end / End-to-end (passkeys, registry, email)success
- End-to-end / End-to-end (real histories)success
- End-to-end / End-to-end (runner Docker)success
- End-to-end / End-to-end (security)success
3 files changed+387−0
added.github/workflows/awesome-lists.yml+37−0View file
| @@ -1,0 +1,37 @@ | ||
| 1 | +name: Suggest Diffinery to awesome lists | |
| 2 | + | |
| 3 | +# Opens one pull request per curated list in scripts/awesome-lists/lists.mjs, as the account | |
| 4 | +# that owns OUTREACH_TOKEN, so they show Codician rather than a person. Lists where Diffinery is | |
| 5 | +# already listed or suggested are left alone, and lists whose rules don't allow it yet are | |
| 6 | +# skipped until their day, so running it again is safe. Run it by hand from the Actions tab. | |
| 7 | +# | |
| 8 | +# OUTREACH_TOKEN: a classic personal access token of the Codician GitHub account with only the | |
| 9 | +# public_repo scope (fine-grained tokens can't open pull requests on other people's repositories). | |
| 10 | +on: | |
| 11 | + workflow_dispatch: | |
| 12 | + inputs: | |
| 13 | + lists: | |
| 14 | + description: "List ids, comma-separated, or 'due' for every list whose rules allow it today" | |
| 15 | + type: string | |
| 16 | + default: due | |
| 17 | + dry_run: | |
| 18 | + description: Only report what would change | |
| 19 | + type: boolean | |
| 20 | + default: false | |
| 21 | + | |
| 22 | +permissions: | |
| 23 | + contents: read | |
| 24 | + | |
| 25 | +jobs: | |
| 26 | + suggest: | |
| 27 | + name: Open the list pull requests | |
| 28 | + if: ${{ github.server_url != 'https://github.com' && github.repository == 'codician/diffinery' }} | |
| 29 | + runs-on: ubuntu-latest | |
| 30 | + environment: github-mirror | |
| 31 | + timeout-minutes: 10 | |
| 32 | + steps: | |
| 33 | + - uses: actions/checkout@v4 | |
| 34 | + - run: node scripts/awesome-lists.mjs --lists="$LISTS" ${{ inputs.dry_run && '--dry-run' || '' }} | |
| 35 | + env: | |
| 36 | + LISTS: ${{ inputs.lists }} | |
| 37 | + OUTREACH_TOKEN: ${{ secrets.OUTREACH_TOKEN }} |
addedscripts/awesome-lists.mjs+216−0View file
| @@ -1,0 +1,216 @@ | ||
| 1 | +#!/usr/bin/env node | |
| 2 | +// Suggests Diffinery to curated "awesome" lists on GitHub (awesome-lists/lists.mjs), one pull | |
| 3 | +// request per list, as the account that owns OUTREACH_TOKEN, so they show Codician rather than | |
| 4 | +// a person. For each list it forks the repository, adds one line to the right section on a | |
| 5 | +// branch in the fork, and opens the pull request. | |
| 6 | +// | |
| 7 | +// A list where Diffinery is already listed, or where the pull request is already open or was | |
| 8 | +// closed, is left alone, so running it again changes nothing. A list whose rules don't allow | |
| 9 | +// the suggestion yet (notBefore) is skipped until that day. Run by the "Suggest Diffinery to | |
| 10 | +// awesome lists" workflow; by hand: | |
| 11 | +// | |
| 12 | +// OUTREACH_TOKEN=ghp_... node scripts/awesome-lists.mjs [--dry-run] [--lists=id,id] | |
| 13 | +// | |
| 14 | +// OUTREACH_TOKEN must be a classic personal access token with the public_repo scope: GitHub's | |
| 15 | +// fine-grained tokens can't contribute to public repositories the account isn't a member of. | |
| 16 | +import { LISTS, FOOTER } from "./awesome-lists/lists.mjs"; | |
| 17 | + | |
| 18 | +const TOKEN = process.env.OUTREACH_TOKEN; | |
| 19 | +const DRY = process.argv.includes("--dry-run"); | |
| 20 | +const pick = (process.argv.find((a) => a.startsWith("--lists="))?.slice(8) || "due").trim(); | |
| 21 | +const today = (process.env.TODAY || new Date().toISOString()).slice(0, 10); | |
| 22 | +const API = process.env.GITHUB_API_URL_OVERRIDE || "https://api.github.com"; | |
| 23 | +const BRANCH = "add-diffinery"; | |
| 24 | + | |
| 25 | +const results = []; | |
| 26 | +const report = (list, outcome, detail = "") => { | |
| 27 | + results.push({ list, outcome, detail }); | |
| 28 | + const line = `${list}: ${outcome}${detail ? ` (${detail})` : ""}`; | |
| 29 | + console.log(outcome === "failed" ? `::warning::${line}` : line); | |
| 30 | +}; | |
| 31 | + | |
| 32 | +class HttpError extends Error { | |
| 33 | + constructor(status, message) { | |
| 34 | + super(message); | |
| 35 | + this.status = status; | |
| 36 | + } | |
| 37 | +} | |
| 38 | + | |
| 39 | +async function rest(method, url, body) { | |
| 40 | + const res = await fetch(`${API}${url}`, { | |
| 41 | + method, | |
| 42 | + headers: { | |
| 43 | + authorization: `Bearer ${TOKEN}`, | |
| 44 | + accept: "application/vnd.github+json", | |
| 45 | + "x-github-api-version": "2022-11-28", | |
| 46 | + "user-agent": "diffinery-awesome-lists", | |
| 47 | + ...(body ? { "content-type": "application/json" } : {}), | |
| 48 | + }, | |
| 49 | + body: body ? JSON.stringify(body) : undefined, | |
| 50 | + }); | |
| 51 | + const text = await res.text(); | |
| 52 | + if (!res.ok) throw new HttpError(res.status, `${method} ${url}: ${res.status} ${text.slice(0, 300)}`); | |
| 53 | + return text ? JSON.parse(text) : null; | |
| 54 | +} | |
| 55 | + | |
| 56 | +/** A file's text. The raw media type also works for files over 1 MB, where `content` comes back empty. */ | |
| 57 | +async function fileText(repo, file, ref) { | |
| 58 | + const res = await fetch(`${API}/repos/${repo}/contents/${encodeURIComponent(file)}?ref=${encodeURIComponent(ref)}`, { | |
| 59 | + headers: { authorization: `Bearer ${TOKEN}`, accept: "application/vnd.github.raw+json", "x-github-api-version": "2022-11-28", "user-agent": "diffinery-awesome-lists" }, | |
| 60 | + }); | |
| 61 | + const text = await res.text(); | |
| 62 | + if (!res.ok) throw new HttpError(res.status, `GET ${repo}/${file}: ${res.status} ${text.slice(0, 300)}`); | |
| 63 | + return text; | |
| 64 | +} | |
| 65 | + | |
| 66 | +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); | |
| 67 | + | |
| 68 | +/** | |
| 69 | + * The file with `entry.line` added to `entry.section`, or an error message. The section runs | |
| 70 | + * from its heading to the next heading of the same or a higher level. | |
| 71 | + */ | |
| 72 | +export function addLine(text, entry) { | |
| 73 | + const eol = text.includes("\r\n") ? "\r\n" : "\n"; | |
| 74 | + const lines = text.split(eol); | |
| 75 | + const start = lines.findIndex((l) => l.trim() === entry.section); | |
| 76 | + if (start < 0) return { error: `no section "${entry.section}"` }; | |
| 77 | + const level = entry.section.match(/^#+/)?.[0].length ?? 6; | |
| 78 | + let end = lines.length; | |
| 79 | + for (let i = start + 1; i < lines.length; i++) { | |
| 80 | + const h = lines[i].match(/^(#+)\s/); | |
| 81 | + if (h && h[1].length <= level) { | |
| 82 | + end = i; | |
| 83 | + break; | |
| 84 | + } | |
| 85 | + } | |
| 86 | + const items = []; | |
| 87 | + for (let i = start + 1; i < end; i++) if (entry.item.test(lines[i])) items.push(i); | |
| 88 | + if (!items.length) return { error: `no entries in "${entry.section}"` }; | |
| 89 | + let at; | |
| 90 | + if (entry.place === "alpha") { | |
| 91 | + const ours = entry.key(entry.line); | |
| 92 | + const before = items.filter((i) => entry.key(lines[i]) < ours); | |
| 93 | + // Sorts first: goes above the first entry. | |
| 94 | + at = before.length ? before[before.length - 1] + 1 : items[0]; | |
| 95 | + // An entry's indented sub-items stay with it. | |
| 96 | + while (before.length && at < end && /^\s+\S/.test(lines[at]) && !entry.item.test(lines[at])) at++; | |
| 97 | + } else { | |
| 98 | + at = items[items.length - 1] + 1; | |
| 99 | + while (at < end && /^\s+[*-] /.test(lines[at])) at++; | |
| 100 | + } | |
| 101 | + lines.splice(at, 0, entry.line); | |
| 102 | + return { text: lines.join(eol), at: at + 1 }; | |
| 103 | +} | |
| 104 | + | |
| 105 | +async function suggest(entry, me) { | |
| 106 | + if (entry.notBefore && today < entry.notBefore) { | |
| 107 | + report(entry.id, "not yet", `its rules allow it from ${entry.notBefore}`); | |
| 108 | + return; | |
| 109 | + } | |
| 110 | + const upstream = await rest("GET", `/repos/${entry.repo}`); | |
| 111 | + const base = upstream.default_branch; | |
| 112 | + | |
| 113 | + // A pull request from our fork, in any state: open means done, closed means the maintainers said no. | |
| 114 | + const prs = await rest("GET", `/repos/${entry.repo}/pulls?state=all&per_page=100&head=${me}:${BRANCH}`); | |
| 115 | + if (prs.length) { | |
| 116 | + const pr = prs[0]; | |
| 117 | + report(entry.id, pr.merged_at ? "merged" : pr.state === "open" ? "already open" : "closed before, left alone", pr.html_url); | |
| 118 | + return; | |
| 119 | + } | |
| 120 | + | |
| 121 | + const text = await fileText(entry.repo, entry.file, base); | |
| 122 | + if (/codician-team\/diffinery|diffinery\.com/i.test(text)) { | |
| 123 | + report(entry.id, "already listed"); | |
| 124 | + return; | |
| 125 | + } | |
| 126 | + const preview = addLine(text, entry); | |
| 127 | + if (preview.error) { | |
| 128 | + report(entry.id, "failed", `${entry.file}: ${preview.error}; the list changed, so update awesome-lists/lists.mjs`); | |
| 129 | + return; | |
| 130 | + } | |
| 131 | + if (DRY) { | |
| 132 | + report(entry.id, "would open", `${entry.file} line ${preview.at}: ${entry.line}`); | |
| 133 | + return; | |
| 134 | + } | |
| 135 | + | |
| 136 | + // Named after the owner too: two lists can share a repository name (and a fork that already | |
| 137 | + // exists comes back under its own name). | |
| 138 | + const fork = await rest("POST", `/repos/${entry.repo}/forks`, { name: entry.repo.replace("/", "-"), default_branch_only: true }); | |
| 139 | + const forkBase = fork.default_branch || base; | |
| 140 | + // Forking is asynchronous; wait until the fork answers. | |
| 141 | + for (let i = 0; i < 20; i++) { | |
| 142 | + try { | |
| 143 | + await rest("GET", `/repos/${fork.full_name}/branches/${encodeURIComponent(forkBase)}`); | |
| 144 | + break; | |
| 145 | + } catch (e) { | |
| 146 | + if (i === 19) throw e; | |
| 147 | + await sleep(3000); | |
| 148 | + } | |
| 149 | + } | |
| 150 | + // A fork left from an earlier run may be behind; bring it up to date where it can be. | |
| 151 | + await rest("POST", `/repos/${fork.full_name}/merge-upstream`, { branch: forkBase }).catch(() => {}); | |
| 152 | + const head = (await rest("GET", `/repos/${fork.full_name}/git/ref/heads/${encodeURIComponent(forkBase)}`)).object.sha; | |
| 153 | + const tree = (await rest("GET", `/repos/${fork.full_name}/git/commits/${head}`)).tree.sha; | |
| 154 | + const changed = addLine(await fileText(fork.full_name, entry.file, head), entry); | |
| 155 | + if (changed.error) { | |
| 156 | + report(entry.id, "failed", `${entry.file} in ${fork.full_name}: ${changed.error}`); | |
| 157 | + return; | |
| 158 | + } | |
| 159 | + // Through git objects rather than the contents API, which can't write files over 1 MB. | |
| 160 | + const blob = await rest("POST", `/repos/${fork.full_name}/git/blobs`, { content: Buffer.from(changed.text, "utf8").toString("base64"), encoding: "base64" }); | |
| 161 | + const newTree = await rest("POST", `/repos/${fork.full_name}/git/trees`, { base_tree: tree, tree: [{ path: entry.file, mode: "100644", type: "blob", sha: blob.sha }] }); | |
| 162 | + const commit = await rest("POST", `/repos/${fork.full_name}/git/commits`, { message: entry.title.replace(/\s*🤖+$/u, ""), tree: newTree.sha, parents: [head] }); | |
| 163 | + try { | |
| 164 | + await rest("POST", `/repos/${fork.full_name}/git/refs`, { ref: `refs/heads/${BRANCH}`, sha: commit.sha }); | |
| 165 | + } catch (e) { | |
| 166 | + if (!(e instanceof HttpError) || e.status !== 422) throw e; | |
| 167 | + // Left from an earlier run that stopped before opening the pull request. | |
| 168 | + await rest("PATCH", `/repos/${fork.full_name}/git/refs/heads/${BRANCH}`, { sha: commit.sha, force: true }); | |
| 169 | + } | |
| 170 | + const pr = await rest("POST", `/repos/${entry.repo}/pulls`, { | |
| 171 | + title: entry.title, | |
| 172 | + head: `${me}:${BRANCH}`, | |
| 173 | + base, | |
| 174 | + body: entry.body + FOOTER, | |
| 175 | + maintainer_can_modify: true, | |
| 176 | + }); | |
| 177 | + report(entry.id, "opened", pr.html_url); | |
| 178 | +} | |
| 179 | + | |
| 180 | +async function summary() { | |
| 181 | + const file = process.env.GITHUB_STEP_SUMMARY; | |
| 182 | + if (!file) return; | |
| 183 | + const { appendFile } = await import("node:fs/promises"); | |
| 184 | + const rows = results.map((r) => `| ${r.list} | ${r.outcome} | ${r.detail.replace(/\|/g, "\\|")} |`).join("\n"); | |
| 185 | + await appendFile(file, `## Suggest Diffinery to awesome lists${DRY ? " (dry run)" : ""}\n\n| List | Outcome | Detail |\n| --- | --- | --- |\n${rows}\n`); | |
| 186 | +} | |
| 187 | + | |
| 188 | +if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) { | |
| 189 | + const chosen = pick === "due" ? LISTS : LISTS.filter((l) => pick.split(",").map((s) => s.trim()).includes(l.id)); | |
| 190 | + if (!chosen.length) { | |
| 191 | + console.log(`::error::No list matches "${pick}". Lists: ${LISTS.map((l) => l.id).join(", ")}`); | |
| 192 | + process.exit(1); | |
| 193 | + } | |
| 194 | + if (!TOKEN) { | |
| 195 | + console.log("::notice::OUTREACH_TOKEN is not set, so nothing was changed."); | |
| 196 | + process.exit(0); | |
| 197 | + } | |
| 198 | + let me; | |
| 199 | + try { | |
| 200 | + me = (await rest("GET", "/user")).login; | |
| 201 | + } catch (e) { | |
| 202 | + console.log(`::error::OUTREACH_TOKEN can't read /user: ${e.message}`); | |
| 203 | + process.exit(1); | |
| 204 | + } | |
| 205 | + console.log(`Acting as ${me}${DRY ? " (dry run)" : ""}`); | |
| 206 | + for (const entry of chosen) { | |
| 207 | + try { | |
| 208 | + await suggest(entry, me); | |
| 209 | + } catch (e) { | |
| 210 | + const hint = e instanceof HttpError && (e.status === 403 || e.status === 404) ? "the token needs the classic public_repo scope: " : ""; | |
| 211 | + report(entry.id, "failed", hint + e.message); | |
| 212 | + } | |
| 213 | + } | |
| 214 | + await summary(); | |
| 215 | + process.exitCode = results.some((r) => r.outcome === "failed") ? 1 : 0; | |
| 216 | +} |
addedscripts/awesome-lists/lists.mjs+134−0View file
| @@ -1,0 +1,134 @@ | ||
| 1 | +// The curated lists Diffinery is suggested to, one pull request each, opened by | |
| 2 | +// scripts/awesome-lists.mjs. Each entry follows that list's own contributing rules (checked | |
| 3 | +// 2026-10-25): its section, its line format and where in the section new lines go. | |
| 4 | +// | |
| 5 | +// id name for the workflow's `lists` input | |
| 6 | +// repo the list's GitHub repository | |
| 7 | +// file the file the line goes into | |
| 8 | +// section the heading line of the section, exactly as in the file | |
| 9 | +// item which lines in the section are entries (a regex on the line) | |
| 10 | +// place "end": after the section's last entry; "alpha": after the last entry that sorts before ours | |
| 11 | +// key for "alpha": the sort key of an entry line | |
| 12 | +// line the line to add | |
| 13 | +// notBefore first day the list's rules allow the suggestion (YYYY-MM-DD), if it has such a rule | |
| 14 | + | |
| 15 | +const REPO = "https://github.com/codician-team/diffinery"; | |
| 16 | +const linkText = (line) => (line.match(/\[([^\]]+)\]/)?.[1] ?? "").toLowerCase(); | |
| 17 | + | |
| 18 | +export const FOOTER = "\n\n---\n_Generated by [Claude Code](https://claude.ai/code)_\n"; | |
| 19 | + | |
| 20 | +export const LISTS = [ | |
| 21 | + { | |
| 22 | + id: "awesome-mcp-servers", | |
| 23 | + repo: "punkpeye/awesome-mcp-servers", | |
| 24 | + file: "README.md", | |
| 25 | + section: '### 🔄 <a name="version-control"></a>Version Control', | |
| 26 | + item: /^- \[/, | |
| 27 | + place: "alpha", | |
| 28 | + key: linkText, | |
| 29 | + line: `- [codician-team/diffinery](${REPO}) 📇 ☁️ - MCP server built into Diffinery, an open-source git forge you deploy to your own Cloudflare account: files and commits, pull requests and reviews, issues, CI runs and job logs, releases, sandboxes and agent tasks.`, | |
| 30 | + // The list fast-tracks pull requests from automated agents that mark their title like this. | |
| 31 | + title: "Add Diffinery to Version Control 🤖🤖🤖", | |
| 32 | + body: `Adds [Diffinery](${REPO}) to **Version Control**. | |
| 33 | + | |
| 34 | +Diffinery is an open-source (Apache-2.0) git forge with GitHub Actions compatible CI that you deploy to your own Cloudflare account. Every instance serves an MCP server at \`/mcp\` (streamable HTTP, OAuth or token sign-in) with tools for files and commits, branches, pull requests and reviews, issues, workflow runs and job logs, releases, sandboxes and agent tasks. It's installable and self-hosted, so it fits this list rather than the remote-only one. | |
| 35 | + | |
| 36 | +- Docs: https://diffinery.com/docs/agents/mcp | |
| 37 | +- Official MCP Registry: \`com.diffinery/diffinery\` | |
| 38 | + | |
| 39 | +This pull request was opened by an automated agent for the Codician team, which builds Diffinery.`, | |
| 40 | + }, | |
| 41 | + { | |
| 42 | + id: "appcypher-mcp-servers", | |
| 43 | + repo: "appcypher/awesome-mcp-servers", | |
| 44 | + file: "README.md", | |
| 45 | + section: '## 🔄 <a name="version-control"></a>Version Control', | |
| 46 | + item: /^- /, | |
| 47 | + place: "end", | |
| 48 | + line: `- <img src="https://diffinery.com/favicon.svg" height="14"/> [Diffinery](${REPO}) - MCP server built into Diffinery, an open-source git forge on Cloudflare: files and commits, pull requests and reviews, issues, CI runs and job logs, sandboxes and agent tasks`, | |
| 49 | + title: "Add Diffinery to Version Control", | |
| 50 | + body: `Adds [Diffinery](${REPO}) at the bottom of **Version Control**. | |
| 51 | + | |
| 52 | +Diffinery is an open-source (Apache-2.0) git forge with GitHub Actions compatible CI that you deploy to your own Cloudflare account. Every instance serves an MCP server at \`/mcp\` with tools for files and commits, pull requests and reviews, issues, workflow runs and job logs, sandboxes and agent tasks. Docs: https://diffinery.com/docs/agents/mcp | |
| 53 | + | |
| 54 | +Opened by an automated agent for the Codician team, which builds Diffinery.`, | |
| 55 | + }, | |
| 56 | + { | |
| 57 | + id: "awesome-git", | |
| 58 | + repo: "dictcp/awesome-git", | |
| 59 | + file: "README.md", | |
| 60 | + section: "## Self-Hosted Repository", | |
| 61 | + item: /^\s*\* /, | |
| 62 | + place: "end", | |
| 63 | + line: "* [Diffinery](https://diffinery.com/) - an open-source git forge you deploy to your own Cloudflare account, with pull requests, code review and GitHub Actions compatible CI. Apache-2.0.", | |
| 64 | + title: "Add Diffinery to Self-Hosted Repository", | |
| 65 | + body: `Adds [Diffinery](https://diffinery.com/) ([source](${REPO})) to **Self-Hosted Repository**. | |
| 66 | + | |
| 67 | +Diffinery is an open-source (Apache-2.0) git forge: repositories, pull requests with code review, issues and GitHub Actions compatible CI. You host it yourself on your own Cloudflare account (Workers, Durable Objects, D1, R2, Containers) instead of on a server. | |
| 68 | + | |
| 69 | +Opened by an automated agent for the Codician team, which builds Diffinery.`, | |
| 70 | + }, | |
| 71 | + { | |
| 72 | + id: "awesome-ci", | |
| 73 | + repo: "ligurio/awesome-ci", | |
| 74 | + file: "README.md", | |
| 75 | + section: "### Introduction", | |
| 76 | + item: /^\|\[/, | |
| 77 | + place: "alpha", | |
| 78 | + key: linkText, | |
| 79 | + line: `|[Diffinery](https://diffinery.com/) | Git forge with built-in CI that runs GitHub Actions workflow files on Cloudflare Containers. | Languages: everything that runs on Ubuntu 24.04 x64, self-hosted runners for other platforms. Open source (Apache-2.0), self-hosted on your own Cloudflare account | Diffinery | [Documentation](https://diffinery.com/docs/actions/overview) | [Open Source](${REPO}) | [](${REPO}) |`, | |
| 80 | + title: "Add Diffinery", | |
| 81 | + body: `Adds [Diffinery](https://diffinery.com/) to the table, in alphabetical order. | |
| 82 | + | |
| 83 | +Diffinery is an open-source (Apache-2.0) git forge with built-in CI. It runs GitHub Actions workflow files from \`.github/workflows/\` on Cloudflare Containers (Ubuntu 24.04 x64, 4 vCPU and 12 GiB by default), and supports self-hosted runners for other platforms. You host the whole thing on your own Cloudflare account. Differences from GitHub Actions: https://diffinery.com/docs/actions/compatibility | |
| 84 | + | |
| 85 | +Opened by an automated agent for the Codician team, which builds Diffinery.`, | |
| 86 | + }, | |
| 87 | + { | |
| 88 | + id: "awesome-cloudflare-zh", | |
| 89 | + repo: "zhuima/awesome-cloudflare", | |
| 90 | + file: "README.md", | |
| 91 | + section: "## 开发者工具", | |
| 92 | + item: /^\| \[/, | |
| 93 | + place: "end", | |
| 94 | + line: `| [Diffinery](${REPO}) | 完全运行在 Cloudflare 上的开源 Git 代码托管平台:仓库、Pull Request 与代码评审、Issue,兼容 GitHub Actions 的 CI(运行在 Cloudflare Containers 上),以及供 AI 代理使用的 MCP 服务器。基于 Workers + Durable Objects + D1 + R2 + Queues 构建,部署到你自己的 Cloudflare 账户。 | <https://diffinery.com> | 维护中 |`, | |
| 95 | + title: "添加 Diffinery 到开发者工具 / Add Diffinery to developer tools", | |
| 96 | + body: `在「开发者工具」末尾添加 [Diffinery](${REPO})。 | |
| 97 | + | |
| 98 | +Diffinery 是一个开源(Apache-2.0)的 Git 代码托管平台,完全运行在 Cloudflare 上(Workers、Durable Objects、D1、R2、Queues、Containers):仓库、Pull Request 与代码评审、Issue,兼容 GitHub Actions 的 CI,以及供 AI 代理使用的 MCP 服务器。部署到你自己的 Cloudflare 账户。 | |
| 99 | + | |
| 100 | +Adds Diffinery, an open-source git forge that runs entirely on Cloudflare, at the end of the developer tools table. Opened by an automated agent for the Codician team, which builds Diffinery.`, | |
| 101 | + }, | |
| 102 | + { | |
| 103 | + id: "awesome-claude-code-plugins", | |
| 104 | + repo: "ccplugins/awesome-claude-code-plugins", | |
| 105 | + file: "README.md", | |
| 106 | + section: "## External Marketplaces", | |
| 107 | + item: /^\| \[/, | |
| 108 | + place: "end", | |
| 109 | + line: `| [codician-team/diffinery](${REPO}) | Plugins for the Diffinery git forge: diffinery (MCP server for repositories, pull requests, issues, CI and sandboxes, plus a skill) and diffinery-ci (live CI status for the commits Claude pushes) | \`claude plugin marketplace add codician-team/diffinery\` then \`claude plugin install diffinery@diffinery\` |`, | |
| 110 | + title: "Add the Diffinery marketplace", | |
| 111 | + body: `Adds the [Diffinery](${REPO}) plugin marketplace to **External Marketplaces**. | |
| 112 | + | |
| 113 | +Diffinery is an open-source (Apache-2.0) git forge you deploy to your own Cloudflare account. Its marketplace has two plugins: \`diffinery\` (its MCP server and a skill for working with repositories, pull requests, issues, CI and sandboxes) and \`diffinery-ci\` (tells Claude when CI on a commit it pushed has finished). Docs: https://diffinery.com/docs/connect/ci-status-in-claude | |
| 114 | + | |
| 115 | +Opened by an automated agent for the Codician team, which builds Diffinery.`, | |
| 116 | + }, | |
| 117 | + { | |
| 118 | + id: "awesome-cloudflare", | |
| 119 | + repo: "irazasyed/awesome-cloudflare", | |
| 120 | + file: "readme.md", | |
| 121 | + section: "### Open Source", | |
| 122 | + item: /^- \[/, | |
| 123 | + place: "end", | |
| 124 | + line: `- [Diffinery](${REPO}) - Open-source git forge with pull requests, code review and GitHub Actions compatible CI, running on Workers, Durable Objects, D1, R2 and Containers.`, | |
| 125 | + // contributing.md: "If you just created something, wait at least 30 days before submitting." | |
| 126 | + notBefore: "2026-11-24", | |
| 127 | + title: "Add Diffinery", | |
| 128 | + body: `Adds [Diffinery](${REPO}) at the bottom of **Apps → Open Source**. | |
| 129 | + | |
| 130 | +Diffinery is an open-source (Apache-2.0) git forge that runs entirely on Cloudflare: repositories live in Durable Objects and R2, metadata in D1, and its GitHub Actions compatible CI runs on Containers. You deploy it to your own account. It belongs here because it's a complete, self-hostable app built only from Cloudflare's developer platform; https://diffinery.com/built-on-cloudflare explains how. | |
| 131 | + | |
| 132 | +Opened by an automated agent for the Codician team, which builds Diffinery.`, | |
| 133 | + }, | |
| 134 | +]; |