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.

Workflows: production environment for every job with deploy secrets, Deploy only from main's tip, step-scoped Cloudflare secrets, pinned pnpm action, non-admin live-test token

- live-test (live), dind-probe (probe), d1-replication (check) and import-campaign (campaign)
  run in the production environment, so they keep their secrets once those move there.
- deploy.yml: a gate job deploys only main's current tip, so re-running an old CI run can't
  roll production back (ACT-4). The Cloudflare secrets are on the steps that call Cloudflare
  only, not the job (SUP-4).
- pnpm/action-setup pinned to v4.3.0's commit everywhere (SUP-4).
- live-test: a missing token after a deploy is an error, not a silent green skip; the email
  check is skipped with a notice for a non-admin token (SUP-2).
- import-campaign's relay targets codician/diffinery; the retired GitHub sync never runs (SUP-3).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014R3J2i3cD96PEceUzqjks8
Claude authored signed1 parent 12fae42commit da645f6ac06444f104f32e83ea2a427757c3a5cfBrowse files
8 files changed+93−22
Files (8)
modified.github/workflows/ci.yml+2−2View file
@@ -24,7 +24,7 @@
2424 timeout-minutes: 20
2525 steps:
2626 - uses: actions/checkout@v4
27- - uses: pnpm/action-setup@v4
27+ - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
2828 - uses: actions/setup-node@v4
2929 with:
3030 node-version: 22
@@ -49,7 +49,7 @@
4949 shard: [1, 2]
5050 steps:
5151 - uses: actions/checkout@v4
52- - uses: pnpm/action-setup@v4
52+ - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
5353 - uses: actions/setup-node@v4
5454 with:
5555 node-version: 22
modified.github/workflows/d1-replication.yml+3−1View file
@@ -19,6 +19,8 @@
1919 check:
2020 name: Check sessions on a test database
2121 runs-on: ubuntu-latest
22+ # The Cloudflare secrets are the production environment's.
23+ environment: production
2224 timeout-minutes: 20
2325 env:
2426 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
@@ -26,7 +28,7 @@
2628 NAME: diffinery-d1-probe-${{ github.run_id }}
2729 steps:
2830 - uses: actions/checkout@v4
29- - uses: pnpm/action-setup@v4
31+ - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
3032 - uses: actions/setup-node@v4
3133 with:
3234 node-version: 22
modified.github/workflows/deploy.yml+56−5View file
@@ -10,6 +10,11 @@
1010 #
1111 # It deploys a main commit only once CI (typecheck, unit tests and the Worker bundle) has
1212 # passed on it, so it doesn't run those itself. End-to-end tests run alongside the deploy.
13+# Only main's current tip deploys: re-running an old CI run (which anyone with write access
14+# can do) doesn't bring back a version from before a fix.
15+#
16+# The Cloudflare secrets are the production environment's, and only the steps that talk to
17+# Cloudflare get them: installs, builds and third-party actions run without them.
1318 on:
1419 workflow_run:
1520 workflows: [CI]
@@ -25,13 +30,43 @@
2530 cancel-in-progress: false
2631
2732 jobs:
28- deploy:
29- name: Deploy to Cloudflare
33+ gate:
34+ name: Main's current tip
3035 # Production deploys from Diffinery itself, on its own hosted runners (any
3136 # Diffinery, so self-hosted copies deploy themselves too). Running this
3237 # workflow by hand on GitHub still deploys, as a fallback.
3338 if: ${{ github.event_name == 'workflow_dispatch' || (github.server_url != 'https://github.com' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push') }}
3439 runs-on: ubuntu-latest
40+ timeout-minutes: 5
41+ outputs:
42+ deploy: ${{ steps.tip.outputs.deploy }}
43+ steps:
44+ - name: Deploy only main's current tip
45+ id: tip
46+ env:
47+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48+ WANT: ${{ github.event.workflow_run.head_sha || github.sha }}
49+ REF: ${{ github.ref }}
50+ run: |
51+ set -euo pipefail
52+ if [ "$GITHUB_EVENT_NAME" = workflow_dispatch ] && [ "$REF" != refs/heads/main ]; then
53+ echo "::error::Deploy runs from main only (this run is on $REF)"; exit 1
54+ fi
55+ tip=$(curl -fsS "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/branches/main" -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" | jq -r .commit.sha)
56+ if [ "$tip" = "$WANT" ]; then
57+ echo "deploy=true" >> "$GITHUB_OUTPUT"
58+ echo "deploying $WANT, main's tip"
59+ else
60+ # A later commit's CI run deploys it: a re-run of an old CI run deploys nothing.
61+ echo "deploy=false" >> "$GITHUB_OUTPUT"
62+ echo "::notice::not deploying $WANT: main is at $tip now, and that commit's own run deploys it"
63+ fi
64+
65+ deploy:
66+ name: Deploy to Cloudflare
67+ needs: gate
68+ if: ${{ needs.gate.outputs.deploy == 'true' }}
69+ runs-on: ubuntu-latest
3570 timeout-minutes: 45
3671 environment:
3772 name: production
@@ -40,15 +75,13 @@
4075 DIFFINERY_DOMAIN: ${{ vars.DIFFINERY_DOMAIN || 'diffinery.com' }}
4176 DIFFINERY_ZONE: ${{ vars.DIFFINERY_ZONE }}
4277 MAIL_DOMAIN: ${{ vars.MAIL_DOMAIN }}
43- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
44- CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
4578 ACCESS_TEAM_DOMAIN: ${{ vars.ACCESS_TEAM_DOMAIN }}
4679 steps:
4780 - uses: actions/checkout@v4
4881 with:
4982 # The commit CI passed on, not whatever main is by now.
5083 ref: ${{ github.event.workflow_run.head_sha || github.sha }}
51- - uses: pnpm/action-setup@v4
84+ - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
5285 - uses: actions/setup-node@v4
5386 with:
5487 node-version: 22
@@ -57,27 +90,45 @@
5790 - name: Build runner agent and web islands
5891 run: pnpm -r --if-present build
5992 - name: Runner image (built only when the runner changed)
93+ env:
94+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
95+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
6096 run: scripts/runner-image.sh
6197
6298 - name: Create or update Cloudflare resources
6399 id: bootstrap
100+ env:
101+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
102+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
64103 run: scripts/bootstrap.sh
65104
66105 - name: Deploy Worker and runner image
67106 working-directory: apps/diffinery
107+ env:
108+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
109+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
68110 run: pnpm exec wrangler deploy -c wrangler.deploy.jsonc
69111
70112 - name: Wait for new containers to get the new image
113+ env:
114+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
115+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
71116 # The Live test follows this workflow, and its checks must run on the new image.
72117 # This job runs on an old-image container, so finishing after the rollout's last step
73118 # also shows that a deploy doesn't stop a running job.
74119 run: scripts/wait-container-rollout.sh
75120
76121 - name: Worker secrets (generated on the first deploy; TURNSTILE_SECRET when missing)
122+ env:
123+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
124+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
77125 run: scripts/ensure-secrets.sh
78126
79127 - name: Finish Access setup on first deploy
80128 if: steps.bootstrap.outputs.access_team == ''
129+ env:
130+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
131+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
81132 run: |
82133 scripts/bootstrap.sh
83134 cd apps/diffinery && pnpm exec wrangler deploy -c wrangler.deploy.jsonc --containers-rollout none
modified.github/workflows/dind-probe.yml+3−1View file
@@ -14,6 +14,8 @@
1414 probe:
1515 name: Probe Docker inside a container
1616 runs-on: ubuntu-latest
17+ # The Cloudflare secrets are the production environment's.
18+ environment: production
1719 timeout-minutes: 40
1820 env:
1921 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
@@ -21,7 +23,7 @@
2123 NAME: diffinery-dind-probe-${{ github.run_id }}
2224 steps:
2325 - uses: actions/checkout@v4
24- - uses: pnpm/action-setup@v4
26+ - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
2527 - uses: actions/setup-node@v4
2628 with:
2729 node-version: 22
modified.github/workflows/e2e.yml+1−1View file
@@ -39,7 +39,7 @@
3939 shard: rest
4040 steps:
4141 - uses: actions/checkout@v4
42- - uses: pnpm/action-setup@v4
42+ - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
4343 - uses: actions/setup-node@v4
4444 with:
4545 node-version: 22
modified.github/workflows/import-campaign.yml+3−1View file
@@ -66,7 +66,7 @@
6666 env:
6767 DIFFINERY: https://diffinery.com
6868 TOKEN: ${{ secrets.DIFFINERY_LIVE_TOKEN }}
69- R: codician-team/diffinery
69+ R: codician/diffinery
7070 steps:
7171 - name: Copy IMPORT_GITHUB_TOKEN to Diffinery
7272 if: ${{ inputs.follow_run == '' && !inputs.peek && inputs.rerun_failed == '' && inputs.inspect_run == '' && inputs.push_test == '' }}
@@ -229,6 +229,8 @@
229229 if: ${{ github.server_url != 'https://github.com' }}
230230 runs-on: ubuntu-latest
231231 timeout-minutes: 360
232+ # DIFFINERY_LIVE_TOKEN and IMPORT_GITHUB_TOKEN are the production environment's.
233+ environment: production
232234 env:
233235 DIFFINERY: ${{ github.server_url }}
234236 TOKEN: ${{ secrets.DIFFINERY_LIVE_TOKEN }}
modified.github/workflows/live-test.yml+18−5View file
@@ -6,8 +6,10 @@
66 # queue). Each run works in a throwaway private repository that it
77 # deletes at the end.
88 #
9-# Needs the repository secret DIFFINERY_LIVE_TOKEN: a Diffinery token (Settings ->
10-# Tokens) with the admin scope and access to all repositories.
9+# Needs DIFFINERY_LIVE_TOKEN in the production environment: a write-scope token
10+# (Settings -> Tokens) with access to all repositories, for a dedicated account that isn't
11+# a site administrator (give it a quota override). The checks that need a site
12+# administrator are skipped with a notice when the token isn't one's.
1113 on:
1214 workflow_dispatch:
1315 workflow_run:
@@ -29,6 +31,7 @@
2931 if: ${{ github.event_name == 'workflow_dispatch' || (github.server_url == 'https://diffinery.com' && github.event.workflow_run.conclusion == 'success') }}
3032 runs-on: ubuntu-latest
3133 timeout-minutes: 45
34+ environment: production
3235 env:
3336 DIFFINERY: https://diffinery.com
3437 TOKEN: ${{ secrets.DIFFINERY_LIVE_TOKEN }}
@@ -39,7 +42,12 @@
3942 id: token
4043 run: |
4144 if [ -z "$TOKEN" ]; then
42- echo "::warning::DIFFINERY_LIVE_TOKEN is not set, so the live test is skipped. Create an admin token at $DIFFINERY/settings/tokens and save it as a repository secret."
45+ # After a deploy of diffinery.com a missing token is an error: a green run would mean nothing was tested.
46+ if [ "$GITHUB_EVENT_NAME" = workflow_run ]; then
47+ echo "::error::DIFFINERY_LIVE_TOKEN is not set in the production environment, so nothing was tested. Create a write token at $DIFFINERY/settings/tokens and save it as a production environment secret."
48+ exit 1
49+ fi
50+ echo "::warning::DIFFINERY_LIVE_TOKEN is not set, so the live test is skipped. Create a write token at $DIFFINERY/settings/tokens and save it as a production environment secret."
4351 echo "skip=true" >> "$GITHUB_OUTPUT"
4452 exit 0
4553 fi
@@ -145,7 +153,11 @@
145153 if: steps.token.outputs.skip != 'true'
146154 run: |
147155 set -euo pipefail
148- s=$(curl -fsS "$DIFFINERY/api/v3/diffinery/admin/email" -H "Authorization: Bearer $TOKEN")
156+ s=$(curl -sS -w '\n%{http_code}' "$DIFFINERY/api/v3/diffinery/admin/email" -H "Authorization: Bearer $TOKEN")
157+ code=${s##*$'\n'}; s=${s%$'\n'*}
158+ # Only a site administrator's token reads the email settings, and the live-test account isn't one.
159+ if [ "$code" = 403 ]; then echo "::notice::skipped: the live-test token isn't a site administrator's"; exit 0; fi
160+ [ "$code" = 200 ] || { echo "::error::/diffinery/admin/email answered $code"; exit 1; }
149161 echo "$s" | jq '{sending, queue, from, last_24h}'
150162 [ "$(jq -r .sending <<<"$s")" = true ] || { echo "::error::the Worker has no Email Sending binding"; exit 1; }
151163 [ "$(jq -r .queue <<<"$s")" = true ] || { echo "::error::the Worker has no mail queue binding"; exit 1; }
@@ -770,7 +782,8 @@
770782 sleep 10
771783 done
772784 if [ -z "$run" ]; then
773- # Whether the scheduler stopped ticking (last_tick_at is old) or ticked without starting the run.
785+ # Whether the scheduler stopped ticking (last_tick_at is old) or ticked without starting the run
786+ # (site administrators only: another token prints the 403).
774787 echo "scheduler: $(curl -sS "$DIFFINERY/api/v3/diffinery/admin/scheduler" -H "Authorization: Bearer $TOKEN") (now $(date -u +%FT%TZ))"
775788 echo "::error::no scheduled run started"
776789 exit 1
modified.github/workflows/sync-to-diffinery.yml+7−6View file
@@ -1,6 +1,6 @@
11 name: Sync to Diffinery
22
3-# Keeps diffinery.com/codician-team/diffinery in step with this repository: every
3+# Kept diffinery.com/codician-team/diffinery (now codician/diffinery) in step with this repository: every
44 # branch and tag, plus the Actions secrets and variables our workflows use. On the
55 # first run it creates the organization and repository. For main it then waits for
66 # the Actions runs on Diffinery and fails if any of them fail.
@@ -8,9 +8,10 @@
88 # Secrets go straight from GitHub's secret store to Diffinery's over HTTPS and are
99 # never printed. Needs DIFFINERY_LIVE_TOKEN (an admin token on diffinery.com).
1010 #
11-# Manual only since the cut-off: Diffinery (codician/diffinery) is this repository's home,
12-# GitHub's copy is frozen, and pushes here are no longer copied over. The file stays as a
13-# record and for one last manual run if ever needed.
11+# Retired since the cut-off: Diffinery (codician/diffinery) is this repository's home,
12+# GitHub's copy is frozen, and nothing is copied over any more. The file stays as a record
13+# only; its job never runs (it would copy GitHub's frozen branches over Diffinery's and write
14+# repository-level secrets, which now live in the production environment).
1415 on:
1516 workflow_dispatch:
1617 inputs:
@@ -40,14 +41,14 @@
4041 name: Copy code, secrets and variables, then check Actions on Diffinery
4142 # Runs on GitHub only; the copy on Diffinery has nothing to sync to.
4243 # Branch deletions aren't copied (Diffinery keeps the branch until it's deleted there).
43- if: ${{ github.server_url == 'https://github.com' && !github.event.deleted }}
44+ if: false
4445 runs-on: ubuntu-latest
4546 timeout-minutes: 90
4647 environment: production
4748 env:
4849 DIFFINERY: https://diffinery.com
4950 TOKEN: ${{ secrets.DIFFINERY_LIVE_TOKEN }}
50- ORG: codician-team
51+ ORG: codician
5152 NAME: diffinery
5253 steps:
5354 - name: Check the token