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.

307 lines · 37.1 KBRaw History

title: Configuration description: Every setting of a self-hosted Diffinery: Worker variables and secrets, bindings, the dev environment, and the secrets and variables the deploy workflows use.

A Diffinery instance is configured in four places: Worker variables in apps/diffinery/wrangler.jsonc, Worker secrets set with wrangler secret put, the Cloudflare bindings in the same wrangler.jsonc, and the secrets and variables of the repository whose workflows deploy it. This page lists every setting, its default and what it changes. To deploy for the first time, follow Deploy your own Diffinery.

Where configuration lives

File or placeWhat it holdsEdit it?
apps/diffinery/wrangler.jsoncWorker name, route, variables, bindings, container settings, cron, and the dev environment.Yes. This is the source of truth.
apps/diffinery/wrangler.deploy.jsoncwrangler.jsonc without the dev environment, plus the real D1 database ID, ACCESS_TEAM_DOMAIN, ACCESS_AUD, and the route, PUBLIC_URL and MAIL_FROM for your hostname. Written by scripts/render-config.mjs on every deploy and ignored by git.No. It is regenerated on each deploy.
Worker secretsMASTER_KEY and OIDC_PRIVATE_KEY, created by scripts/ensure-secrets.sh, and optionally GITHUB_OAUTH_CLIENT_SECRET and TURNSTILE_SECRET, which you set yourself.Only to restore them, or to add the optional one. Never change MASTER_KEY.
Repository secrets and variablesCloudflare credentials and options for the Deploy workflow, and a token for the live test and sync workflows.Yes, in your forge's repository settings.
apps/diffinery/.dev.varsLocal secrets for wrangler dev, such as MASTER_KEY. Ignored by git.Yes, for local development only.

Worker variables

Set under vars in wrangler.jsonc. All values are strings.

VariableProduction value in the repositoryRequiredWhat it does
PUBLIC_URLhttps://diffinery.comYesThe canonical origin of the instance. See PUBLIC_URL. On deploy, scripts/bootstrap.sh replaces it with https://$DIFFINERY_DOMAIN (default https://diffinery.com), whatever wrangler.jsonc says.
MAIL_FROMnotifications@diffinery.comNoSender address of every email. The display name is "Diffinery", or <actor> (Diffinery) for emails about someone's action. On deploy, scripts/bootstrap.sh replaces it with notifications@$MAIL_DOMAIN (default notifications@$DIFFINERY_DOMAIN). When empty, Diffinery sends from notifications@<hostname of PUBLIC_URL>. The domain must be onboarded to Email Sending.
BOOTSTRAP_ADMINSA comma-separated list of emailsFor the first sign-inEmails (case-insensitive) that become site administrators. See BOOTSTRAP_ADMINS.
ACCESS_TEAM_DOMAINEmpty (filled in on deploy)For email sign-inYour Cloudflare Zero Trust team domain, like example.cloudflareaccess.com. Used to fetch the Access signing keys and to check the token issuer.
ACCESS_AUDEmpty (filled in on deploy)For email sign-inThe audience tag of the Access application that guards /auth/access.
ARTIFACT_QUOTA_BYTESNot setNoWorkflow artifact storage per repository, in bytes: artifacts that haven't expired plus uploads in progress. Default 10737418240 (10 GB). Anything that isn't a positive whole number counts as not set. Add it under vars in wrangler.jsonc; the deploy keeps it. When a repository reaches it, uploads fail with 413 the repository's artifact storage quota (N bytes) is used up; delete artifacts or wait for them to expire. See Caches and artifacts.
DEV_AUTHNot set in production; dev@diffinery.local in the dev environmentNever in productionLocal development only. See DEV_AUTH.
SIGNUPNot setNoopen lets anyone create an account at /signup. See SIGNUP. Any other value, or none, keeps the instance invite-only.
TURNSTILE_SITE_KEYNot setWith SIGNUP=openThe site key of the Cloudflare Turnstile widget on the sign-up page. See SIGNUP.
IMPORT_MAX_REPO_KBNot setNoThe largest GitHub repository a one-repository import accepts, in KB as GitHub reports a repository's size. Default 10485760 (10 GB). An empty value, 0, a negative number or text that isn't a number counts as not set. A larger source is refused before the import starts with 422 the repository is about N GB, over the M GB import limit; ask an administrator to raise IMPORT_MAX_REPO_KB or split the repository. See GitHub imports.
QUOTA_HOSTED_MINUTES, QUOTA_HOSTED_JOBS, QUOTA_SANDBOXES, QUOTA_REPOSITORIES, QUOTA_STORAGE_BYTESNot setNoThe default per-account quotas: hosted minutes per month (default 3000), hosted Actions jobs at once (5), agent sandboxes open at once (5), repositories (500) and storage in bytes (10737418240, 10 GB). -1 is unlimited; text that isn't a whole number counts as not set. Site administrators change them for one account with PATCH /api/v3/admin/accounts/{login}/quotas, and accounts run by a site administrator have none. See Limits.
IMPORT_GHE_HOSTNot setNoThe host name of a GitHub Enterprise Server (like ghe.example.com) that imports may download release assets from. Without it, imports download release assets only from github.com and its asset hosts, and assets of an Enterprise import are skipped with a warning.
GITHUB_OAUTH_CLIENT_IDNot setNoThe client ID of a GitHub OAuth app, for linking GitHub accounts with a GitHub sign-in instead of a gist. Needs GITHUB_OAUTH_CLIENT_SECRET too. See GitHub imports.
IMPORT_GIT_AGENTNot set in production; external in the dev environmentNever in productionWith external, an import's Git transfer doesn't start a container: it waits for an import agent started by hand (see The dev environment).
SCHEDULE_TICK_MINUTESNot setNoMinutes between the workflow scheduler's checks, a whole number from 1 to 60. Default 5, GitHub's shortest schedule interval. Anything else counts as not set. The end-to-end tests use 1.
WAIT_TIMER_MINUTE_MSNot setNever in productionLocal development and the end-to-end tests only: how long one minute of a deployment environment's wait timer lasts, in milliseconds, from 1 to 60000 (the default). Anything else counts as not set. It counts only when PUBLIC_URL is on this machine (localhost, 127.x or [::1]) and ACCESS_AUD is empty, as for DEV_AUTH; a deployed Worker ignores it. The end-to-end tests use 10000 so a wait timer passes in seconds.
AGENT_POLL_MSNot setNever in productionThe end-to-end tests only: how often a running agent task is checked, in milliseconds, from 200 to 3000 (the default). Like WAIT_TIMER_MINUTE_MS, it counts only on a Worker on this machine; a deployed Worker ignores it.
TERMINAL_POLL_MSNot setNever in productionThe end-to-end tests only: how often open web terminals are checked for their limits and their agent's access, in milliseconds, from 2000 to 60000 (the default). It counts only on a Worker on this machine; a deployed Worker ignores it.
EGRESSNot setNoweb limits hosted Actions jobs and agent sandboxes (including tasks, reviews and terminals) to HTTP and HTTPS (ports 80 and 443) and DNS. Any other value, or none, leaves them open to the whole internet. Accounts run by a site administrator stay open. See EGRESS.
TERMINALSNot setNooff switches terminals off: nobody can open one, and open ones end at their next check, within a minute. Anything else, or not set, leaves them on.

When either ACCESS_TEAM_DOMAIN or ACCESS_AUD is empty, email sign-in at /auth/access is off: the page answers "Account recovery by email is not set up on this instance. Ask an administrator for a sign-in link.", and the sign-in page doesn't offer Recover your account by email. Passkeys, invitations and admin sign-in links keep working, but the first administrator can't sign in, because email sign-in is how an account in BOOTSTRAP_ADMINS is created.

PUBLIC_URL

PUBLIC_URL must be the exact origin people and tools use, with https:// and no path. You don't set it by hand for a deploy: scripts/render-config.mjs sets it from --public-url, which bootstrap fills in as https://<DIFFINERY_DOMAIN>, and refuses a URL that isn't https:// or has a path (--public-url must be an https origin, like https://git.example.com). The same flag points the Worker's route at that hostname, so the route and PUBLIC_URL always agree. Diffinery uses it for:

  • Passkeys. The WebAuthn relying party ID is the hostname of PUBLIC_URL, and the expected origin is its origin. Passkeys only work on that hostname.
  • Every generated link: emails, invitation and sign-in links, html_url and clone_url in API responses, webhook payloads, check run details links, the sitemap and canonical tags.
  • Workflows: github.server_url is PUBLIC_URL, github.api_url is PUBLIC_URL/api/v3, and the runner agent and sandboxes call back to it.
  • OIDC: the issuer (iss) of workflow ID tokens, and their default audience.
  • The container registry host name, the hostname of PUBLIC_URL (images are <hostname>/<owner>/<name>).
  • The cross-site check: a cookie-authenticated write with an Origin other than this origin is refused.

Important: Changing PUBLIC_URL on a running instance (by deploying with another DIFFINERY_DOMAIN) breaks every existing passkey (they are bound to the old hostname), changes the OIDC issuer that cloud providers trust, and changes every image name. Choose the hostname before your first sign-in.

BOOTSTRAP_ADMINS

A comma-separated list of email addresses; spaces around entries are ignored and matching is case-insensitive. Logins in the list are ignored, because anyone could pick a free login, while an email address has to be proven (by email sign-in or by opening the link in an invitation email).

An account becomes a site administrator from this list only when it is created (through email sign-in at /auth/access, by joining from an invitation or through open sign-up, or through DEV_AUTH locally), or when an account that is still waiting for an invite signs in through /auth/access. Accounts that already exist are not promoted or demoted when you change the list later, and there is no page for granting or removing site administration. See Site administration.

SIGNUP

With SIGNUP=open, the sign-in page links to Create an account (/signup). Someone enters their email address, gets a 6-digit code by email, enters it, picks a username and creates a passkey; their account is active at once (see Open sign-up). Unset, or any other value, the instance is invite-only as before: /signup answers 404 and the sign-in page says so.

Open sign-up needs a Cloudflare Turnstile widget, so scripts can't use it to send email:

  1. In the Cloudflare dashboard, open Turnstile, add a widget for your hostname (the one in PUBLIC_URL) and copy its site key and secret key.
  2. Add "SIGNUP": "open" and "TURNSTILE_SITE_KEY": "<site key>" under vars in wrangler.jsonc; the deploy keeps them.
  3. Store the secret: printf '%s' "$TURNSTILE_SECRET" | pnpm exec wrangler secret put TURNSTILE_SECRET -c wrangler.deploy.jsonc.

Each code request is checked with Turnstile's siteverify API, with the visitor's IP address. Without both keys, /signup answers 503 with "Sign-up needs Turnstile configured", except when PUBLIC_URL is on the local machine (localhost, 127.x.x.x or [::1]): local development and the end-to-end tests skip the check. Only the sign-up page's content security policy allows scripts and frames from https://challenges.cloudflare.com.

The limits on codes (5 a day per address, 20 a day per IP address, 10 minutes and 5 tries each) are in Limits. Sign-up emails always send: they need the EMAIL binding and the MAIL queue like every other email.

EGRESS

Experimental: web hasn't been tried on a live instance yet. The setting covers the whole instance, so try it on a test instance first, or check it with an account no site administrator runs, as in Check it below. diffinery.com doesn't set it.

With EGRESS=web, hosted job and sandbox containers start without direct internet access. Cloudflare then lets only ports 80 and 443, and DNS to Cloudflare's resolvers, leave the container, and Diffinery forwards HTTP and HTTPS requests to any host through the Worker. Unset, or any other value, hosted compute reaches the whole internet as before. The setting is read when a container starts: jobs and sandboxes already running keep the access they started with.

Under web:

  • Works: git clone https://..., curl, apt-get, npm, pnpm and pip installs, docker pull, and calls to the Diffinery and GitHub APIs.
  • Refused: every other port. That includes SSH (git@... remotes, ssh, scp), SMTP (25, 465 and 587), IRC, databases and HTTP on other ports.
  • HTTPS is intercepted. Requests leave through the instance's own Worker, which sees them decrypted, as the operator already can for the container itself. Cloudflare re-signs them with a CA it puts in the container at /etc/cloudflare/certs/cloudflare-containers-ca.crt. The runner agent adds that CA to the system store and sets NODE_EXTRA_CA_CERTS and REQUESTS_CA_BUNDLE, so tools that use the system store, Node.js and Python's requests trust it. Tools with their own fixed CA list or certificate pinning, client certificates (mutual TLS), and HTTPS from inside Docker containers a job starts (docker run, services:, RUN steps of a build) fail their certificate checks.

The account that owns the repository decides, as for quotas. Accounts run by a site administrator (their own account, and organizations with a site administrator as an owner) stay open even with EGRESS=web, so the instance's own Deploy and live test workflows keep their access. Self-hosted runners and GitHub imports are not affected. Under wrangler dev sandboxes run in a local process, not containers, so the setting changes nothing there.

To turn it on, add "EGRESS": "web" under vars in wrangler.jsonc; the deploy keeps it. To turn it off, remove it (or set any other value) and deploy again; containers started after that are open.

How a blocked connection looks

Diffinery doesn't add a line to the job or terminal output when it limits a connection. The tool that made it reports an ordinary network error:

  • A port other than 80 or 443 fails to connect, as behind a firewall: for example ssh: connect to host github.com port 22: Connection refused (or Connection timed out), or a mail or database client that can't reach its server.
  • HTTPS from a tool that doesn't trust the Cloudflare CA fails its certificate check: for example SSL certificate problem: unable to get local issuer certificate (curl), x509: certificate signed by unknown authority (Go), PKIX path building failed (Java), or self-signed certificate in certificate chain (Node.js with its own CA list). Inside a Docker container a job starts, add the CA from /etc/cloudflare/certs/cloudflare-containers-ca.crt to the image, or run that step on the host.

If the CA never appears in the container, the runner agent writes web-only egress, but /etc/cloudflare/certs/cloudflare-containers-ca.crt did not appear: HTTPS will fail to the container's own log (not the job log), and every HTTPS request from the job fails its certificate check.

Check it

With EGRESS=web deployed, run a workflow in a repository owned by an account that no site administrator runs (a site administrator's own repositories stay open):

on: workflow_dispatch
jobs:
  egress:
    runs-on: ubuntu-latest
    steps:
      - run: curl -fsSI https://example.com            # works
      - run: git ls-remote https://github.com/git/git HEAD   # works
      - run: npm view left-pad version                 # works
      - shell: bash
        run: "! timeout 10 bash -c '</dev/tcp/github.com/22'"  # SSH port: must not connect

All four steps pass. The last one fails if port 22 is reachable, which means the account is open (a site administrator runs it) or the setting isn't deployed.

DEV_AUTH

When DEV_AUTH is set, ACCESS_AUD is empty and both PUBLIC_URL and the request's own address are on the local machine (localhost, 127.x.x.x or [::1]), every request without credentials (no token, no session) acts as the account with the email in DEV_AUTH, with full access. That account is created on first use and is a site administrator if the email is also in BOOTSTRAP_ADMINS. While DEV_AUTH is set, the email log also keeps each whole message, and GET /api/v3/diffinery/emails returns it, so tests can read emails without delivering them.

Important: Never set DEV_AUTH on a deployed instance. As a safeguard it does nothing there: a request that reaches the Worker under any other host name, such as workers.dev or your domain, or a PUBLIC_URL that isn't on the local machine, turns it off, and so does Cloudflare Access.

Worker secrets

Set with wrangler secret put (the deploy does this for you with scripts/ensure-secrets.sh), never in wrangler.jsonc.

SecretFormatCreated byWhat it doesIf it is missing
MASTER_KEY32 random bytes, base64ensure-secrets.sh, onceRoot key for envelope encryption of Actions secrets: each repository, organization and deployment environment gets its own AES-GCM key derived from it with HKDF. The GitHub token of a running import is encrypted the same way. It also signs the unsubscribe links in emails, the codes and OAuth state for linking GitHub accounts, keys the stored hashes of sign-up codes, and a key derived from it (HKDF) signs each job's ACTIONS_RUNTIME_TOKEN and the upload and download URLs of the Actions cache and artifact services.Repository, organization and environment secrets can't be stored or read (could not store the secret: ... (is MASTER_KEY configured?)). Notification emails that carry unsubscribe links can't be built, so they end up in the email log as failed; security and account emails still send. Jobs get no ACTIONS_RUNTIME_TOKEN: actions/cache and the built-in cache step skip caching (The cache service is not available to this job; skipping.), and uploading or downloading artifacts fails (The artifact service is not available to this job.). Imports that come with a GitHub token can't be started. Open sign-up can't send codes.
OIDC_PRIVATE_KEYAn ECDSA P-256 private key as a JWK (JSON), with a kidensure-secrets.sh, onceSigns the ID tokens that workflow jobs request with permissions: id-token: write (ES256). Its public half is served at /.well-known/jwks.Jobs that request an ID token fail with OIDC is not configured on this Diffinery instance, /.well-known/jwks returns no keys, and GET /api/v3/meta returns "oidc": false.
GITHUB_OAUTH_CLIENT_SECRETThe client secret of a GitHub OAuth appYou, optionallyWith GITHUB_OAUTH_CLIENT_ID, turns on the Link with GitHub option on Settings → Linked accounts.People link GitHub accounts with the gist code only.
TURNSTILE_SECRETThe secret key of a Cloudflare Turnstile widgetYou, optionallyWith SIGNUP=open and TURNSTILE_SITE_KEY, checks that whoever asks for a sign-up code passed the Turnstile widget. See SIGNUP.With SIGNUP=open, sign-up answers 503 "Sign-up needs Turnstile configured" (local development skips the check). Without SIGNUP=open it isn't used.

Neither secret is rotated automatically. Changing MASTER_KEY makes every stored secret undecryptable and invalidates unsubscribe links already sent, along with the runtime tokens of running jobs and cache and artifact links already handed out; keep a copy somewhere safe. To rotate the OIDC key, delete OIDC_PRIVATE_KEY and deploy: a new key with a new kid (diffinery-<date>) is generated, and relying parties pick it up from /.well-known/jwks.

To set a secret by hand (for example to restore a backup):

cd apps/diffinery
printf '%s' "$MASTER_KEY" | pnpm exec wrangler secret put MASTER_KEY -c wrangler.deploy.jsonc

Bindings

Defined in wrangler.jsonc. The deploy creates the underlying resources if they don't exist.

BindingKindResource (production)What Diffinery uses it for
DBD1 databasediffinery (migrations in migrations/)Accounts (including each account's appearance setting and profile picture version), organizations, tokens, repository list, topics and access, invitations, sessions and passkeys, notifications, audit log, schedules, organization secrets and variables, the registry index, email settings and log, GitHub imports and linked GitHub accounts.
BUCKETR2 bucketdiffinery-dataLarge git objects, LFS objects, archived job logs, caches, artifacts, container image layers and manifests, the event archive, release assets, cached source archives (zipball and tarball downloads) and profile pictures.
EVENTSQueue producer, with a consumer on the same Workerdiffinery-events, dead-letter queue diffinery-events-dlqRepository events for webhooks, notifications and the audit log. Consumer: batches of up to 50, waiting up to 2 seconds, 8 retries.
MAILQueue producer, with a consumer on the same Workerdiffinery-mail, dead-letter queue diffinery-mail-dlqEmail jobs. Consumer: batches of up to 20, waiting up to 2 seconds, 8 retries. Optional: without it emails are dropped with a warning in the logs.
EMAILsend_email (Email Sending)Your onboarded domainSending email. Optional: without it every email is logged as skipped.
RL_AUTH, RL_ANON, RL_ACCOUNT, RL_CREATERate limiting (ratelimits)Namespaces 1001 to 1004; 20, 300, 2,000 and 60 requests per 60 secondsRequest rate limits: sign-in per IP address, anonymous requests per IP address, signed-in requests per account, and new content per account. Optional: a missing binding (the dev environment declares none) or one that fails lets requests through. If you change a limit, change RATE_LIMITS in src/lib/rate-limit.ts to match, since the 429 messages quote it. Namespace IDs are shared across your Cloudflare account, so pick others if another Worker already uses these.
REPODurable Object namespaceClass RepoDO (SQLite)One object per repository.
SCHEDULERDurable Object namespaceClass SchedulerDO (SQLite)One object that starts scheduled workflow runs on its own alarm.
RUNNERDurable Object namespace with a containerClass RunnerContainer (SQLite)One container per hosted workflow job.
SANDBOXDurable Object namespace with a containerClass SandboxContainer (SQLite)One container per agent sandbox.
RUN_WORKFLOWWorkflowdiffinery-run, class RunWorkflowOne durable instance per workflow run.
IMPORTERDurable Object namespace with a containerClass ImportContainer (SQLite)One container per Git transfer of a GitHub import.
IMPORT_WORKFLOWWorkflowdiffinery-import, class ImportWorkflowOne durable instance per attempt of a GitHub import.

The Durable Object classes are declared in four migrations (new_sqlite_classes): tag v1 for RepoDO, RunnerContainer and SandboxContainer, tag v2 for ImportContainer, tag v3 for ProjectDO and tag v4 for SchedulerDO. wrangler deploy applies a migration tag the account doesn't have yet.

Other Worker settings

SettingValueNotes
namediffineryThe Worker's name in your account.
mainsrc/index.ts
compatibility_date2026-10-01
compatibility_flagsnodejs_compat
workers_devfalseThe instance is only served on your route.
routes[{ "pattern": "diffinery.com/*", "zone_name": "diffinery.com" }]A zone route, not a Custom Domain. The hostname's DNS record must be proxied. On deploy, scripts/render-config.mjs replaces it with one route, <DIFFINERY_DOMAIN>/* in the zone DIFFINERY_ZONE (by default the hostname's last two labels), so you don't edit it by hand.
observabilityEnabled, head_sampling_rate: 1Every request is logged to Workers Logs.
upload_source_mapstrueReadable stack traces in the logs.
limits.cpu_ms300000Up to 5 minutes of CPU time per request (large pushes, diffs and merges).
containersRunnerContainer: standard-2, max_instances: 100; SandboxContainer: standard-1, max_instances: 50; ImportContainer: standard-4, max_instances: 20; rollout_active_grace_period: 23400 for runners and 25200 for imports (longer than a job or transfer can run, so a deploy never replaces a running one), 3600 for sandboxes; rollout_step_percentage: 100, so containers started after a deploy get the new imageAll three build containers/runner/Dockerfile with containers/runner as context. Raise max_instances for more concurrent jobs, sandboxes or import transfers.
triggers.crons["*/5 * * * *"]Token expiry reminders, email log pruning, registry cleanup, cancelling draft imports older than 7 days, and setting the workflow scheduler's alarm if it isn't set. Scheduled workflows run from that alarm, not from the cron trigger.

The dev environment

wrangler.jsonc has an env.dev section that pnpm dev (wrangler dev -e dev) and the end-to-end tests use. It is removed from the deployed config.

Settingdev value
routes, workers_devNo routes; workers_dev: true
PUBLIC_URLhttp://localhost:8787
BOOTSTRAP_ADMINS, DEV_AUTHdev@diffinery.local
ACCESS_TEAM_DOMAIN, ACCESS_AUDEmpty
DBdiffinery-dev
BUCKETdiffinery-data-dev
Queuesdiffinery-events-dev and diffinery-mail-dev, batches waiting up to 1 second, no dead-letter queues
EMAILCaptured locally by Miniflare, not delivered
RUN_WORKFLOWdiffinery-run-dev
IMPORT_WORKFLOWdiffinery-import-dev
IMPORT_GIT_AGENTexternal
containersNone: hosted runners, sandboxes and import containers are unavailable locally; use a self-hosted runner, and run an import's Git transfer by hand
triggers.cronsNone

To run an import locally, start it as usual, wait until its git.phase is waiting, claim the transfer, and run the runner agent's import mode against your instance (build it first with pnpm -r build):

TOKEN=$(curl -sS -X POST http://localhost:8787/api/v3/diffinery/imports/<id>/claim | jq -r .token)
DIFFINERY_API_URL=http://localhost:8787 DIFFINERY_IMPORT_ID=<id> DIFFINERY_IMPORT_TOKEN=$TOKEN \
  DIFFINERY_WORK_ROOT=/tmp/diffinery-import node containers/runner/dist/agent.mjs

Only the account that started the import can claim it. In local development (DEV_AUTH in effect, as described in DEV_AUTH), imports also accept plain http:// and loopback GitHub API URLs, for test doubles.

MASTER_KEY is not set in dev. Add it to apps/diffinery/.dev.vars (MASTER_KEY=<base64 of 32 random bytes>) or pass --var MASTER_KEY:<value> to wrangler dev to store Actions secrets locally. The end-to-end tests pass a random one with --var, together with PUBLIC_URL.

GitHub imports

Imports from GitHub need no setup beyond a normal deploy: the IMPORTER container, the IMPORT_WORKFLOW Workflow and the D1 tables of migration 0010_imports.sql are created by the deploy. What you can tune:

SettingWhereEffect
IMPORT_MAX_REPO_KBvars in wrangler.jsoncThe size ceiling for one-repository imports (default 10 GB). The Git transfer makes a full mirror clone (git clone --mirror) inside the import container (standard-4), so a higher ceiling needs room for that clone.
max_instances of ImportContainercontainers in wrangler.jsoncHow many Git transfers run at once across the instance (default 20).
IMPORT_GHE_HOSTvars in wrangler.jsoncLets imports download release assets from your GitHub Enterprise Server host.
GITHUB_OAUTH_CLIENT_ID and GITHUB_OAUTH_CLIENT_SECRETvars (or a secret) and a Worker secretAdds Link with GitHub to Settings → Linked accounts.

To offer linking through GitHub sign-in, create an OAuth app on github.com (Settings → Developer settings → OAuth Apps) with the authorization callback URL https://git.example.com/settings/linked/github/callback (your PUBLIC_URL plus /settings/linked/github/callback). Then add the client ID under vars in wrangler.jsonc and store the secret:

cd apps/diffinery
printf '%s' "$CLIENT_SECRET" | pnpm exec wrangler secret put GITHUB_OAUTH_CLIENT_SECRET -c wrangler.deploy.jsonc

The OAuth flow asks GitHub for the read:user scope only, uses github.com (not an Enterprise host), and binds its state to the signed-in account. Without the two settings, people link accounts with the gist code, which needs no configuration.

Imports reach GitHub only on HTTPS hosts with public names; a GitHub API URL on a private address, localhost, .local or .internal is refused with 422 github_api_url must be a public host.

Deploy script settings

Environment variables the scripts in scripts/ read:

VariableUsed byDefaultMeaning
CLOUDFLARE_API_TOKENbootstrap.sh, email-sending.sh, ensure-secrets.sh (through wrangler), wrangler deployRequiredThe API token.
CLOUDFLARE_ACCOUNT_IDbootstrap.sh, wranglerRequiredThe account to deploy to.
DIFFINERY_DOMAINbootstrap.sh, email-sending.shdiffinery.comYour instance's hostname, without a scheme or path: the Worker's route becomes <domain>/*, PUBLIC_URL becomes https://<domain>, email is sent from notifications@<domain> (unless MAIL_DOMAIN is set), the Access application covers <domain>/auth/access, and email checks look at this domain.
DIFFINERY_ZONEbootstrap.shThe hostname's last two labelsThe Cloudflare zone that holds the hostname, for the Worker's route. Set it when the last two labels aren't the zone, for example example.co.uk for git.example.co.uk. The API token needs Workers Routes Edit on this zone.
ACCESS_TEAM_DOMAINbootstrap.shDiscoveredYour Zero Trust team domain, when the token can't read it.
MAIL_DOMAINbootstrap.sh, email-sending.shDIFFINERY_DOMAINThe domain email is sent from: MAIL_FROM becomes notifications@<MAIL_DOMAIN>, and the Email Sending check looks at this domain.
GITHUB_OUTPUTbootstrap.shSet by ActionsWhere bootstrap writes d1_id, access_team and access_aud_set.

scripts/render-config.mjs takes flags instead:

FlagEffect
--d1 <database id>Required. The D1 database_id.
--team <team domain>Sets ACCESS_TEAM_DOMAIN.
--aud <audience tag>Sets ACCESS_AUD.
--public-url <origin>Sets PUBLIC_URL to the origin, routes to [{ "pattern": "<hostname>/*", "zone_name": "<zone>" }] and MAIL_FROM to notifications@<hostname>. Must be an https:// URL without a path.
--zone <zone>The route's zone, with --public-url. Default: the hostname's last two labels.
--mail-from <address>Sets MAIL_FROM, overriding the one --public-url sets.

Bootstrap calls it with --public-url https://$DIFFINERY_DOMAIN, --zone $DIFFINERY_ZONE when that is set, and --mail-from notifications@${MAIL_DOMAIN:-$DIFFINERY_DOMAIN}. See Deploy your own Diffinery.

Repository secrets and variables for the workflows

The workflows in .github/workflows/ read these from the repository (or its production environment):

NameKindUsed byValue
CLOUDFLARE_API_TOKENSecretDeployCloudflare API token with the permissions listed in Deploy your own Diffinery.
CLOUDFLARE_ACCOUNT_IDSecretDeployYour Cloudflare account ID.
DIFFINERY_DOMAINVariableDeploy, Sync to DiffineryOptional. Your instance's hostname, default diffinery.com. Deploy passes it to bootstrap (see Deploy script settings), uses it as the production environment's URL and runs its checks against https://<DIFFINERY_DOMAIN>.
DIFFINERY_ZONEVariableDeploy, Sync to DiffineryOptional. The Cloudflare zone for the route, when it isn't the hostname's last two labels.
MAIL_DOMAINVariableDeploy, Sync to DiffineryOptional. The domain email is sent from, when it isn't DIFFINERY_DOMAIN.
ACCESS_TEAM_DOMAINVariableDeploy, Sync to DiffineryOptional. Your Zero Trust team domain, for example example.cloudflareaccess.com, when the API token can't read it.
DIFFINERY_LIVE_TOKENSecretLive test, Sync to DiffineryA Diffinery token with the admin scope and access to all repositories, created under Settings → Access tokens on the instance. The live test creates and deletes throwaway repositories with it, and uploads a test profile picture to the token's account (putting the previous one back afterwards). When the secret is not set, those steps are skipped with a warning; the check that /docs, /llms.txt and the CLI download are served without signing in still runs. The sync workflow uses it to create the organization and repository, copy secrets and variables, and push.
IMPORT_GITHUB_TOKENSecretImport campaignOptional. A read-only GitHub token for public repositories. When it is set on GitHub, the campaign copies it to the Diffinery repository's secrets before it starts; without it, imports use GitHub's anonymous rate limit, and large repositories take hours. The main sync doesn't copy it.

Deploy reads DIFFINERY_DOMAIN, DIFFINERY_ZONE and MAIL_DOMAIN from the repository's variables and passes them to bootstrap, so you point it at your own hostname without editing the workflow (see Deploy your own Diffinery). Deploy runs on pushes to main on any Diffinery instance, and when started by hand on GitHub.

The Deploy job and the Sync job run in an environment named production. The Live test and Sync workflows target https://diffinery.com and its codician-team/diffinery repository; adapt or delete them for your own instance.

The Import campaign workflow is a manual test of the importer. Started on GitHub (Run workflow), it starts the same workflow on Diffinery and prints its report. On Diffinery it imports a list of public GitHub repositories (the repos input) as private repositories named <owner>/campaign-<github owner>-<github repo>, replacing copies from earlier campaigns. It can then run their push workflows (run_workflows, off by default, because that is third-party code) and writes a report of what came across and what ran.

  • Queueing. Diffinery runs at most 3 repository imports per person at a time and answers 429 beyond that, so the campaign queues the rest and starts each one as an earlier one finishes.
  • Time limit. Imports still waiting when minutes runs out are reported as not started.
  • Failures. When a run fails before writing its report, the GitHub side prints the last 150 lines of the job's log instead.
  • Following a run. Set follow_run to a Diffinery run id to print the report of a run that already started.

It needs DIFFINERY_LIVE_TOKEN and, optionally, IMPORT_GITHUB_TOKEN.

Sync to Diffinery copies the four variables above to the repository on Diffinery (only those that are set), together with the secrets CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID and DIFFINERY_LIVE_TOKEN, so the copy deploys to the same place. Each push copies only the branch or tag it pushed, and runs are queued per ref, so a push to another branch never replaces a waiting sync of main. A run started by hand copies every branch and tag; it queues together with main, and moves each ref only if it still points where it did when the run read it, so it never puts back an older commit that another run copied in the meantime. Branch deletions aren't copied. Only a sync of main checks that main on Diffinery matches and waits for its Actions runs.

scripts/diffinery-wait-runs.sh, which the sync workflow uses to wait for Actions runs on a commit, reads DIFFINERY (the instance origin) and TOKEN (a Diffinery token) from the environment:

DIFFINERY=https://diffinery.com TOKEN=dfy_... scripts/diffinery-wait-runs.sh acme/website <sha> 60

Settings that are not configuration

Some values are fixed in code and can't be changed without editing it, for example the job timeout ceiling, matrix size, the 10 GB cache budget per repository and session lifetime. They are listed in Limits.