Skip to content
270 linesCodeBlameRaw
1# Deploys g1t.sh from main, with g1t's own Actions. What it does is
2# scripts/deploy.mjs, the same tool a person runs; docs/DEPLOYING.md is the
3# guide.
4#
5# check the deploy manifest is consistent, and the tool's tests pass
6# plan what changed since each Worker's live commit, and pending migrations
7# migrate pending D1 migrations, before any code
8# core, edge, front the units of each stage, in jobs that share a build;
9# a stage starts only when the one before it succeeded
10# smoke sign-in, sign-up and the waitlist still work on g1t.sh
11#
12# Each run that deploys is one production deployment of g1t.sh, made by the
13# jobs that name `environment: production` (one per run, however many jobs):
14# in progress when the first starts, then a success or a failure when the
15# run ends. It shows on the project's Deployments page and as the commit's
16# `deploy / production` check. The plan job reads production's secrets
17# with `deployment: false`, so a dry run or a change that deploys nothing
18# makes no deployment.
19#
20# Needs the repository secret CLOUDFLARE_API_TOKEN (a Production row, with
21# Containers write), the variable CLOUDFLARE_ACCOUNT_ID, and
22# api.cloudflare.com among the project's workflow-only domains for
23# deploy.yml in production (Settings, Guardrails), and
24# registry.cloudflare.com there too, to find, pull and push the runner's
25# image. A job that must build that image (the `runner-image` group) does
26# so with its own Docker Engine, on a larger machine. Optionally, the
27# secret STATUS_DEPLOY_TOKEN (the status Worker's secret of the same name)
28# and status.g1t.sh among the same workflow-only domains, so status.g1t.sh
29# hears each deploy start and finish. See docs/DEPLOYING.md.
30name: Deploy
31
32on:
33 push:
34 branches: [main]
35 workflow_dispatch:
36 inputs:
37 units:
38 description: "Units to deploy whether or not they changed, comma separated (empty: what changed)"
39 type: string
40 default: ""
41 all:
42 description: "Deploy every unit"
43 type: boolean
44 default: false
45 dry_run:
46 description: "Plan only: deploy nothing"
47 type: boolean
48 default: false
49
50# Its token only reads: deploying uses CLOUDFLARE_API_TOKEN, and g1t
51# records the deployments itself.
52permissions:
53 contents: read
54
55# One deploy at a time, and never one cut off halfway: the next waits.
56concurrency:
57 group: deploy-production
58 cancel-in-progress: false
59
60env:
61 CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
62 CARGO_TERM_COLOR: never
63 WRANGLER_SEND_METRICS: "false"
64
65jobs:
66 check:
67 name: Check
68 runs-on: ubuntu-latest
69 timeout-minutes: 20
70 steps:
71 - uses: actions/checkout@v5
72 - name: Install Wrangler
73 run: npm ci --workspaces=false --no-audit --no-fund
74 - name: The manifest matches every wrangler.jsonc
75 run: node scripts/deploy.mjs manifest --check
76 - name: The deploy tool's tests
77 run: npm run test:deploy
78
79 plan:
80 name: Plan
81 needs: check
82 runs-on: ubuntu-latest
83 # Production's secrets, without a deployment: planning deploys nothing.
84 environment:
85 name: production
86 deployment: false
87 timeout-minutes: 15
88 outputs:
89 migrate: ${{ steps.plan.outputs.migrate }}
90 migrate_units: ${{ steps.plan.outputs.migrate_units }}
91 has_core: ${{ steps.plan.outputs.has_core }}
92 core: ${{ steps.plan.outputs.core }}
93 has_edge: ${{ steps.plan.outputs.has_edge }}
94 edge: ${{ steps.plan.outputs.edge }}
95 has_front: ${{ steps.plan.outputs.has_front }}
96 front: ${{ steps.plan.outputs.front }}
97 steps:
98 - uses: actions/checkout@v5
99 with:
100 # Each Worker's live commit is compared with this one.
101 fetch-depth: 0
102 - name: Install Wrangler
103 run: npm ci --workspaces=false --no-audit --no-fund
104 - name: Plan
105 id: plan
106 env:
107 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
108 UNITS: ${{ inputs.units }}
109 ALL: ${{ inputs.all }}
110 run: |
111 args=()
112 if [ -n "$UNITS" ]; then args+=(--only "$UNITS" --force); fi
113 if [ "$ALL" = "true" ]; then args+=(--all); fi
114 node scripts/deploy.mjs plan "${args[@]}" --github-output
115
116 migrate:
117 name: Migrations
118 needs: plan
119 if: ${{ needs.plan.outputs.migrate == 'true' && inputs.dry_run != true }}
120 runs-on: ubuntu-latest
121 environment:
122 name: production
123 url: https://g1t.sh
124 timeout-minutes: 20
125 steps:
126 - uses: actions/checkout@v5
127 - name: Install Wrangler
128 run: npm ci --workspaces=false --no-audit --no-fund
129 - name: Apply pending migrations
130 env:
131 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
132 run: node scripts/deploy.mjs migrate --only "${{ needs.plan.outputs.migrate_units }}"
133
134 core:
135 name: core (${{ matrix.group }})
136 needs: [plan, migrate]
137 # Runs when nothing before it failed: a migrate job skipped for having
138 # nothing to apply is not a failure.
139 if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_core == 'true' && inputs.dry_run != true }}
140 # Rust builds and the runner's image get 4 vCPUs; everything else the
141 # standard machine.
142 runs-on: ${{ (matrix.rust || matrix.image) && 'g1t-4core' || 'ubuntu-latest' }}
143 environment:
144 name: production
145 url: https://g1t.sh
146 timeout-minutes: 60
147 strategy:
148 # A deploy cut off halfway is worse than one that finishes: the other
149 # jobs of a stage run on when one fails, and the next stage does not.
150 fail-fast: false
151 max-parallel: 4
152 matrix: ${{ fromJSON(needs.plan.outputs.core) }}
153 steps: &deploy
154 - uses: actions/checkout@v5
155 with:
156 fetch-depth: 0
157 # Rust workers: the wasm target, and worker-build kept between runs
158 # (its version is pinned in scripts/build-rust-worker.mjs).
159 - name: Rust for Workers
160 if: ${{ matrix.rust }}
161 run: rustup target add wasm32-unknown-unknown
162 - name: Cache worker-build
163 if: ${{ matrix.rust }}
164 uses: actions/cache@v4
165 with:
166 path: ~/.cargo/bin/worker-build
167 key: worker-build-${{ runner.os }}-${{ hashFiles('scripts/build-rust-worker.mjs') }}
168 - name: Cache worker-build's tools (wasm-bindgen, esbuild)
169 if: ${{ matrix.rust }}
170 uses: actions/cache@v4
171 with:
172 path: ~/.cache/worker-build
173 key: worker-build-tools-${{ runner.os }}-${{ hashFiles('scripts/build-rust-worker.mjs') }}
174 - name: Cache crates
175 if: ${{ matrix.rust }}
176 uses: actions/cache@v4
177 with:
178 path: ~/.cargo/registry/cache
179 key: cargo-crates-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
180 restore-keys: cargo-crates-${{ runner.os }}-
181 # The compiled dependencies of this job's units, for wasm32 and the
182 # build scripts and proc macros they run. The workspace's own crates
183 # are compiled again whatever is cached (a checkout's sources are
184 # newer), so an entry is saved only when the dependencies change: a
185 # new Cargo.lock, or a new base image (base.json names its Rust).
186 # Otherwise the nearest earlier entry, of any group, is a start.
187 - name: Cache the Cargo target
188 if: ${{ matrix.rust }}
189 uses: actions/cache@v4
190 with:
191 path: |
192 target/release
193 target/wasm32-unknown-unknown/release
194 !target/**/incremental
195 !target/**/*.wasm
196 key: cargo-target-${{ runner.os }}-${{ matrix.group }}-${{ hashFiles('Cargo.lock', 'services/runner/base.json') }}
197 restore-keys: |
198 cargo-target-${{ runner.os }}-${{ matrix.group }}-
199 cargo-target-${{ runner.os }}-
200 # The runner's image: its binary, built natively for musl (the base
201 # has musl-gcc; the target is added here), with its Cargo target kept
202 # between runs. The image itself is built and pushed with the job's
203 # own Docker Engine (scripts/deploy/image.mjs).
204 - name: Rust for the runner
205 if: ${{ matrix.image }}
206 run: rustup target add x86_64-unknown-linux-musl
207 - name: Cache the runner's build
208 if: ${{ matrix.image }}
209 uses: actions/cache@v4
210 with:
211 path: |
212 ~/.cargo/registry/cache
213 target/x86_64-unknown-linux-musl/release
214 !target/**/incremental
215 key: runner-musl-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'services/runner/base.json') }}
216 restore-keys: runner-musl-${{ runner.os }}-
217 - name: Install
218 run: node scripts/deploy.mjs install --only "${{ matrix.units }}"
219 - name: Deploy ${{ matrix.units }}
220 env:
221 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
222 # status.g1t.sh hears the deploy start and finish, so its restarts
223 # are not drafted as incidents. Optional: without it, nothing is sent.
224 STATUS_DEPLOY_TOKEN: ${{ secrets.STATUS_DEPLOY_TOKEN }}
225 run: node scripts/deploy.mjs deploy --only "${{ matrix.units }}" --force --no-migrations --concurrency 2
226
227 edge:
228 name: edge (${{ matrix.group }})
229 needs: [plan, migrate, core]
230 if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_edge == 'true' && inputs.dry_run != true }}
231 runs-on: ${{ (matrix.rust || matrix.image) && 'g1t-4core' || 'ubuntu-latest' }}
232 environment:
233 name: production
234 url: https://g1t.sh
235 timeout-minutes: 60
236 strategy:
237 fail-fast: false
238 max-parallel: 4
239 matrix: ${{ fromJSON(needs.plan.outputs.edge) }}
240 steps: *deploy
241
242 front:
243 name: front (${{ matrix.group }})
244 needs: [plan, migrate, core, edge]
245 if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_front == 'true' && inputs.dry_run != true }}
246 runs-on: ${{ (matrix.rust || matrix.image) && 'g1t-4core' || 'ubuntu-latest' }}
247 environment:
248 name: production
249 url: https://g1t.sh
250 timeout-minutes: 60
251 strategy:
252 fail-fast: false
253 max-parallel: 4
254 matrix: ${{ fromJSON(needs.plan.outputs.front) }}
255 steps: *deploy
256
257 # Once everything has deployed: the ways in for someone new still work.
258 # The pages a visitor lands on load, and the waitlist form reaches
259 # identity, sent an address it refuses before keeping anything, so the
260 # real waitlist is never touched. scripts/ops/smoke.mjs.
261 smoke:
262 name: Smoke
263 needs: [plan, core, edge, front]
264 if: ${{ !failure() && !cancelled() && inputs.dry_run != true && (needs.plan.outputs.has_core == 'true' || needs.plan.outputs.has_edge == 'true' || needs.plan.outputs.has_front == 'true') }}
265 runs-on: ubuntu-latest
266 timeout-minutes: 5
267 steps:
268 - uses: actions/checkout@v5
269 - name: Sign-in, sign-up and the waitlist work
270 run: node scripts/ops/smoke.mjs