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.

178 lines · 11.3 KBRaw History
1// Diffinery: one Worker, everything on Cloudflare.
2// `scripts/bootstrap.sh` creates the account resources and writes
3// wrangler.deploy.jsonc (this file plus real IDs) for CI deploys.
4{
5 "$schema": "node_modules/wrangler/config-schema.json",
6 "name": "diffinery",
7 "main": "src/index.ts",
8 "compatibility_date": "2026-10-01",
9 "compatibility_flags": ["nodejs_compat"],
10 "workers_dev": false,
11 // A zone route, not a Custom Domain: diffinery.com already has DNS records, and a
12 // route runs the Worker in front of them as long as the record is proxied (orange cloud).
13 "routes": [{ "pattern": "diffinery.com/*", "zone_name": "diffinery.com" }],
14 "observability": { "enabled": true, "head_sampling_rate": 1 },
15 // The deployed version's id and time: IndexNow checks the site's pages once per new version (src/web/indexnow.ts).
16 "version_metadata": { "binding": "CF_VERSION_METADATA" },
17 "upload_source_maps": true,
18 "limits": { "cpu_ms": 300000 },
19
20 "vars": {
21 "PUBLIC_URL": "https://diffinery.com",
22 "MAIL_FROM": "notifications@diffinery.com",
23 // Emails that become site administrators on first sign-in. Left empty here: the deploy takes the
24 // list from the BOOTSTRAP_ADMINS environment variable, else keeps the deployed Worker's (scripts/bootstrap-admins.sh).
25 "BOOTSTRAP_ADMINS": "",
26 "ACCESS_TEAM_DOMAIN": "",
27 "ACCESS_AUD": ""
28 },
29
30 "d1_databases": [
31 { "binding": "DB", "database_name": "diffinery", "database_id": "00000000-0000-0000-0000-000000000000", "migrations_dir": "migrations" }
32 ],
33 "r2_buckets": [{ "binding": "BUCKET", "bucket_name": "diffinery-data" }],
34 "queues": {
35 "producers": [
36 { "binding": "EVENTS", "queue": "diffinery-events" },
37 { "binding": "MAIL", "queue": "diffinery-mail" }
38 ],
39 "consumers": [
40 // 20 events per batch: each makes D1 queries (webhooks, notifications, audit), and one consumer
41 // invocation may make at most 1,000.
42 { "queue": "diffinery-events", "max_batch_size": 20, "max_batch_timeout": 2, "max_retries": 8, "dead_letter_queue": "diffinery-events-dlq" },
43 { "queue": "diffinery-mail", "max_batch_size": 20, "max_batch_timeout": 2, "max_retries": 8, "dead_letter_queue": "diffinery-mail-dlq" },
44 // Bounces and complaints: subscribe this queue to Email Sending's events (docs/admin/site-administration.md).
45 { "queue": "diffinery-mail-events", "max_batch_size": 50, "max_batch_timeout": 5, "max_retries": 8 }
46 ]
47 },
48 // Cloudflare Email Sending (notifications, security alerts, invites). The sending
49 // domain is onboarded under Compute > Email Service > Email Sending (see scripts/email-sending.sh).
50 "send_email": [{ "name": "EMAIL" }],
51 // Request rate limits per minute (src/lib/rate-limit.ts; RATE_LIMITS there must match). Local
52 // development declares none, so nothing is limited there.
53 "ratelimits": [
54 { "name": "RL_AUTH", "namespace_id": "1001", "simple": { "limit": 20, "period": 60 } },
55 { "name": "RL_ANON", "namespace_id": "1002", "simple": { "limit": 300, "period": 60 } },
56 { "name": "RL_ACCOUNT", "namespace_id": "1003", "simple": { "limit": 2000, "period": 60 } },
57 { "name": "RL_CREATE", "namespace_id": "1004", "simple": { "limit": 60, "period": 60 } },
58 // Model requests through the proxy (/-/model/*), per paying person (agentic/proxy.ts MODEL_RATE_LIMIT).
59 { "name": "RL_MODEL", "namespace_id": "1005", "simple": { "limit": 600, "period": 60 } }
60 ],
61 "durable_objects": {
62 "bindings": [
63 { "name": "REPO", "class_name": "RepoDO" },
64 { "name": "SCHEDULER", "class_name": "SchedulerDO" },
65 { "name": "COMPOSE", "class_name": "ComposeDO" },
66 { "name": "RUNNER", "class_name": "RunnerContainer" },
67 // Smaller hosted runners (src/runner/sizes.ts), the same runner on smaller instance types.
68 { "name": "RUNNER_MEDIUM", "class_name": "RunnerContainerMedium" },
69 { "name": "RUNNER_SMALL", "class_name": "RunnerContainerSmall" },
70 { "name": "RUNNER_TINY", "class_name": "RunnerContainerTiny" },
71 { "name": "SANDBOX", "class_name": "SandboxContainer" },
72 { "name": "IMPORTER", "class_name": "ImportContainer" },
73 { "name": "PROJECT", "class_name": "ProjectDO" }
74 ]
75 },
76 "migrations": [
77 { "tag": "v1", "new_sqlite_classes": ["RepoDO", "RunnerContainer", "SandboxContainer"] },
78 { "tag": "v2", "new_sqlite_classes": ["ImportContainer"] },
79 // Agent tasks, one Durable Object per repository (agentic/ProjectDO.ts).
80 { "tag": "v3", "new_sqlite_classes": ["ProjectDO"] },
81 // Workflow schedules (scheduler.ts).
82 { "tag": "v4", "new_sqlite_classes": ["SchedulerDO"] },
83 // Smaller hosted runner sizes (src/runner/sizes.ts).
84 { "tag": "v5", "new_sqlite_classes": ["RunnerContainerMedium", "RunnerContainerSmall", "RunnerContainerTiny"] },
85 // Joining large uploads' blocks, a step per object (compose.ts).
86 { "tag": "v6", "new_sqlite_classes": ["ComposeDO"] }
87 ],
88 "workflows": [
89 { "name": "diffinery-run", "binding": "RUN_WORKFLOW", "class_name": "RunWorkflow" },
90 { "name": "diffinery-import", "binding": "IMPORT_WORKFLOW", "class_name": "ImportWorkflow" },
91 // Account exports and deletions (src/accounts/workflow.ts).
92 { "name": "diffinery-account", "binding": "ACCOUNT_WORKFLOW", "class_name": "AccountWorkflow" }
93 ],
94 // A deploy that changes the image rolls the running containers: once a container has been up
95 // longer than rollout_active_grace_period seconds, it gets SIGTERM and is killed 15 minutes later.
96 // Runner and import containers are covered for their whole life (a job is capped at 6 hours, an
97 // import transfer too), so a deploy shouldn't cut one short; they pick up the new image next start.
98 // The platform doesn't guarantee it: a job that started on an old-image container during a rollout
99 // was replaced minutes in. So a runner on a replaced image is turned away before it runs anything
100 // (repo/actions.ts STALE_IMAGE_TRIES), and a job whose runner is replaced before its steps ran
101 // starts again on a new one (MAX_RUNNER_RESTARTS).
102 // The rollout is one step (rollout_step_percentage 100): with the default [10, 100], the first
103 // step still starts 90% of new containers on the old image, and the next step replaced such a
104 // job's container seconds after it started. The rollout's SIGTERM reaches every process in the
105 // container, so the job's steps died with it.
106 "containers": [
107 // standard-4 (4 vCPU, 12 GiB, 20 GB): standard-2 has 1 vCPU, on which typecheck and test steps took
108 // 2.3 to 2.6 times as long. Active CPU is what's billed, so a faster job costs about the same.
109 { "class_name": "RunnerContainer", "image": "../../containers/runner/Dockerfile", "image_build_context": "../../containers/runner", "instance_type": "standard-4", "max_instances": 100, "rollout_active_grace_period": 23400, "rollout_step_percentage": 100 },
110 // Smaller sizes for jobs that use little of a large runner (src/runner/sizes.ts): Cloudflare bills a container's
111 // memory and disk as provisioned, used or not, so a job that waits on the network costs a large runner's memory.
112 // A job gets one when runs-on asks for it, or when its own earlier runs show it fits with room to spare.
113 { "class_name": "RunnerContainerMedium", "image": "../../containers/runner/Dockerfile", "image_build_context": "../../containers/runner", "instance_type": "standard-3", "max_instances": 100, "rollout_active_grace_period": 23400, "rollout_step_percentage": 100 },
114 { "class_name": "RunnerContainerSmall", "image": "../../containers/runner/Dockerfile", "image_build_context": "../../containers/runner", "instance_type": "standard-1", "max_instances": 100, "rollout_active_grace_period": 23400, "rollout_step_percentage": 100 },
115 { "class_name": "RunnerContainerTiny", "image": "../../containers/runner/Dockerfile", "image_build_context": "../../containers/runner", "instance_type": "basic", "max_instances": 100, "rollout_active_grace_period": 23400, "rollout_step_percentage": 100 },
116 { "class_name": "SandboxContainer", "image": "../../containers/runner/Dockerfile", "image_build_context": "../../containers/runner", "instance_type": "standard-1", "max_instances": 50, "rollout_active_grace_period": 3600, "rollout_step_percentage": 100 },
117 // GitHub imports: a mirror clone of a large repository needs disk and memory.
118 { "class_name": "ImportContainer", "image": "../../containers/runner/Dockerfile", "image_build_context": "../../containers/runner", "instance_type": "standard-4", "max_instances": 20, "rollout_active_grace_period": 25200, "rollout_step_percentage": 100 }
119 ],
120 // Housekeeping, and a check that the workflow scheduler's alarm is set (src/scheduler.ts).
121 "triggers": { "crons": ["*/5 * * * *"] },
122
123 "env": {
124 // Local development and end-to-end tests: no Access, a fixed dev identity, no containers.
125 "dev": {
126 "routes": [],
127 "workers_dev": true,
128 "vars": {
129 "PUBLIC_URL": "http://localhost:8787",
130 "MAIL_FROM": "notifications@diffinery.com",
131 "BOOTSTRAP_ADMINS": "dev@diffinery.local",
132 "ACCESS_TEAM_DOMAIN": "",
133 "ACCESS_AUD": "",
134 "DEV_AUTH": "dev@diffinery.local",
135 // Containers do not run under wrangler dev: an import agent started by hand (or by the e2e test) claims Git transfers.
136 "IMPORT_GIT_AGENT": "external"
137 },
138 "version_metadata": { "binding": "CF_VERSION_METADATA" },
139 "d1_databases": [{ "binding": "DB", "database_name": "diffinery-dev", "database_id": "00000000-0000-0000-0000-000000000000", "migrations_dir": "migrations" }],
140 "r2_buckets": [{ "binding": "BUCKET", "bucket_name": "diffinery-data-dev" }],
141 "queues": {
142 "producers": [
143 { "binding": "EVENTS", "queue": "diffinery-events-dev" },
144 { "binding": "MAIL", "queue": "diffinery-mail-dev" }
145 ],
146 "consumers": [
147 { "queue": "diffinery-events-dev", "max_batch_size": 50, "max_batch_timeout": 1 },
148 { "queue": "diffinery-mail-dev", "max_batch_size": 20, "max_batch_timeout": 1 },
149 { "queue": "diffinery-mail-events-dev", "max_batch_size": 50, "max_batch_timeout": 1 }
150 ]
151 },
152 // Local email: Miniflare captures sent messages instead of delivering them.
153 "send_email": [{ "name": "EMAIL" }],
154 "durable_objects": {
155 "bindings": [
156 { "name": "REPO", "class_name": "RepoDO" },
157 { "name": "SCHEDULER", "class_name": "SchedulerDO" },
158 { "name": "COMPOSE", "class_name": "ComposeDO" },
159 { "name": "RUNNER", "class_name": "RunnerContainer" },
160 { "name": "RUNNER_MEDIUM", "class_name": "RunnerContainerMedium" },
161 { "name": "RUNNER_SMALL", "class_name": "RunnerContainerSmall" },
162 { "name": "RUNNER_TINY", "class_name": "RunnerContainerTiny" },
163 // Sandboxes run in an `agent.mjs --sandbox-pool` process (SANDBOX_POOL_URL), not containers.
164 { "name": "SANDBOX", "class_name": "SandboxPoolDO" },
165 { "name": "IMPORTER", "class_name": "ImportContainer" },
166 { "name": "PROJECT", "class_name": "ProjectDO" }
167 ]
168 },
169 "workflows": [
170 { "name": "diffinery-run-dev", "binding": "RUN_WORKFLOW", "class_name": "RunWorkflow" },
171 { "name": "diffinery-import-dev", "binding": "IMPORT_WORKFLOW", "class_name": "ImportWorkflow" },
172 { "name": "diffinery-account-dev", "binding": "ACCOUNT_WORKFLOW", "class_name": "AccountWorkflow" }
173 ],
174 "triggers": { "crons": [] },
175 "containers": []
176 }
177 }
178}