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.

150 lines · 7.9 KBRaw History
1name: Sync to Diffinery
2
3# Keeps diffinery.com/codician-team/diffinery in step with this repository: every
4# branch and tag, plus the Actions secrets and variables our workflows use. On the
5# first run it creates the organization and repository. For main it then waits for
6# the Actions runs on Diffinery and fails if any of them fail.
7#
8# Secrets go straight from GitHub's secret store to Diffinery's over HTTPS and are
9# never printed. Needs DIFFINERY_LIVE_TOKEN (an admin token on diffinery.com).
10#
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.
14on:
15 workflow_dispatch:
16 inputs:
17 allow_secrets:
18 description: Reason for pushing past Diffinery's push protection (admins only, audited). Leave empty normally.
19 type: string
20 default: ""
21 secrets_only:
22 description: Only copy the secrets and variables (to a repository that already has the code, such as a fresh import)
23 type: boolean
24 default: false
25
26permissions:
27 contents: read
28
29# One queue per ref: a push to some other branch never replaces a waiting main sync.
30# Each push event copies only its own ref, so runs for different refs can't undo
31# each other. A manual run copies every branch and tag; it queues with main, and
32# each ref it copies is leased to what Diffinery had just before, so it can't put
33# back an older commit over one another run copied meanwhile.
34concurrency:
35 group: sync-to-diffinery-${{ github.event_name == 'push' && github.ref || 'refs/heads/main' }}
36 cancel-in-progress: false
37
38jobs:
39 move:
40 name: Copy code, secrets and variables, then check Actions on Diffinery
41 # Runs on GitHub only; the copy on Diffinery has nothing to sync to.
42 # 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 runs-on: ubuntu-latest
45 timeout-minutes: 90
46 environment: production
47 env:
48 DIFFINERY: https://diffinery.com
49 TOKEN: ${{ secrets.DIFFINERY_LIVE_TOKEN }}
50 ORG: codician-team
51 NAME: diffinery
52 steps:
53 - name: Check the token
54 run: |
55 [ -n "$TOKEN" ] || { echo "::error::Add DIFFINERY_LIVE_TOKEN (an admin token from $DIFFINERY/settings/tokens) as a repository secret first."; exit 1; }
56 curl -fsS "$DIFFINERY/api/v3/user" -H "Authorization: Bearer $TOKEN" | jq '{login, site_admin}'
57
58 - name: Create the organization and repository
59 env:
60 DESC: ${{ github.event.repository.description }}
61 run: |
62 set -euo pipefail
63 api() { curl -sS -o /tmp/out -w '%{http_code}' -X "$1" "$DIFFINERY/api/v3$2" -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" ${3:+--data "$3"}; }
64 if [ "$(api GET "/users/$ORG")" = 404 ]; then
65 code=$(api POST /user/orgs "{\"login\":\"$ORG\",\"name\":\"Codician\"}"); cat /tmp/out; echo
66 [ "$code" = 201 ] || { echo "::error::could not create the $ORG organization ($code)"; exit 1; }
67 fi
68 if [ "$(api GET "/repos/$ORG/$NAME")" = 404 ]; then
69 desc=$(jq -n --arg d "$DESC" '$d')
70 code=$(api POST "/orgs/$ORG/repos" "{\"name\":\"$NAME\",\"private\":true,\"default_branch\":\"main\",\"description\":$desc}"); cat /tmp/out; echo
71 [ "$code" = 201 ] || { echo "::error::could not create $ORG/$NAME ($code)"; exit 1; }
72 fi
73 echo "$DIFFINERY/$ORG/$NAME is ready"
74
75 - name: Copy Actions secrets and variables
76 env:
77 S_CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
78 S_CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
79 S_DIFFINERY_LIVE_TOKEN: ${{ secrets.DIFFINERY_LIVE_TOKEN }}
80 S_IMPORT_GITHUB_TOKEN: ${{ secrets.IMPORT_GITHUB_TOKEN }}
81 V_ACCESS_TEAM_DOMAIN: ${{ vars.ACCESS_TEAM_DOMAIN }}
82 V_DIFFINERY_DOMAIN: ${{ vars.DIFFINERY_DOMAIN }}
83 V_DIFFINERY_ZONE: ${{ vars.DIFFINERY_ZONE }}
84 V_MAIL_DOMAIN: ${{ vars.MAIL_DOMAIN }}
85 run: |
86 set -euo pipefail
87 # The PUT is idempotent, so a transient 5xx is retried; a failure shows the answer (its request_id finds the error under Settings -> Server errors).
88 put() {
89 local out
90 out=$(curl -sS --fail-with-body --retry 3 --retry-all-errors --retry-delay 2 -X PUT "$DIFFINERY/api/v3/repos/$ORG/$NAME/actions/$1/$2" -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" --data-binary @-) || { echo "::error::PUT actions/$1/$2 failed: $out"; return 1; }
91 }
92 for name in CLOUDFLARE_API_TOKEN CLOUDFLARE_ACCOUNT_ID DIFFINERY_LIVE_TOKEN IMPORT_GITHUB_TOKEN; do
93 v="S_$name"
94 if [ -n "${!v}" ]; then jq -n --arg v "${!v}" '{value: $v}' | put secrets "$name"; echo "secret $name copied"; else echo "::warning::secret $name is empty here, so it was not copied"; fi
95 done
96 for name in ACCESS_TEAM_DOMAIN DIFFINERY_DOMAIN DIFFINERY_ZONE MAIL_DOMAIN; do
97 v="V_$name"
98 if [ -n "${!v}" ]; then jq -n --arg v "${!v}" '{value: $v}' | put variables "$name"; echo "variable $name = ${!v}"; fi
99 done
100
101 - uses: actions/checkout@v4
102 if: ${{ !inputs.secrets_only }}
103 with:
104 fetch-depth: 0
105 lfs: true
106
107 - name: Push the branch or tag (a manual run pushes all of them)
108 if: ${{ !inputs.secrets_only }}
109 env:
110 ALLOW_SECRETS: ${{ inputs.allow_secrets }}
111 run: |
112 set -euo pipefail
113 remote="https://x-token:${TOKEN}@${DIFFINERY#https://}/$ORG/$NAME.git"
114 opts=(-q ${ALLOW_SECRETS:+-o "allow-secrets=$ALLOW_SECRETS"})
115 if [ "$GITHUB_EVENT_NAME" = push ]; then
116 git push "${opts[@]}" "$remote" "+$GITHUB_SHA:$GITHUB_REF"
117 want=$GITHUB_SHA
118 else
119 # Read Diffinery's refs first, then GitHub's: GitHub's are then at least as new.
120 declare -A there
121 while read -r sha ref; do there[$ref]=$sha; done < <(git ls-remote "$remote" 'refs/heads/*' 'refs/tags/*')
122 git fetch -q --tags --force origin '+refs/heads/*:refs/remotes/origin/*'
123 args=()
124 while read -r ref; do
125 dest="refs/heads/${ref#refs/remotes/origin/}"
126 args+=("--force-with-lease=$dest:${there[$dest]:-}" "$ref:$dest")
127 done < <(git for-each-ref --format='%(refname)' refs/remotes/origin | grep -v '/HEAD$')
128 for tag in $(git for-each-ref --format='%(refname)' refs/tags); do
129 args+=("--force-with-lease=$tag:${there[$tag]:-}" "$tag:$tag")
130 done
131 git push "${opts[@]}" "$remote" "${args[@]}"
132 want=$(git rev-parse refs/remotes/origin/main)
133 fi
134 if git lfs ls-files | grep -q .; then git lfs push --all "$remote"; fi
135 echo "branches on Diffinery:"
136 curl -fsS "$DIFFINERY/api/v3/repos/$ORG/$NAME/branches?per_page=100" -H "Authorization: Bearer $TOKEN" | jq -r '.[] | " \(.name) \(.commit.sha)"'
137 if [ "$GITHUB_REF" = refs/heads/main ]; then
138 got=$(curl -fsS "$DIFFINERY/api/v3/repos/$ORG/$NAME/branches/main" -H "Authorization: Bearer $TOKEN" | jq -r '.commit.sha')
139 [ "$want" = "$got" ] || { echo "::error::main on Diffinery is $got, expected $want"; exit 1; }
140 echo "main matches GitHub at $want"
141 fi
142 echo "want=$want" >> "$GITHUB_OUTPUT"
143 id: push
144
145 - name: Wait for Actions on Diffinery
146 # Pushes to main start CI (and, once Diffinery deploys, the deploy) there.
147 if: ${{ github.ref == 'refs/heads/main' && !inputs.secrets_only }}
148 # A backstop above the script's own 75-minute deadline, in case a request hangs.
149 timeout-minutes: 80
150 run: scripts/diffinery-wait-runs.sh "$ORG/$NAME" "${{ steps.push.outputs.want }}" 75