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.

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