Skip to content
357 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; the guide is
3# docs.g1t.sh/guides/deploy-to-cloudflare/.
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# (beside check, not after it: neither waits for the other)
8# migrate pending D1 migrations, before any code, once check and plan pass
9# core, edge, front the units of each stage, in jobs that share a build;
10# a stage starts only when the one before it succeeded
11# smoke sign-in and sign-up still work on g1t.sh
12#
13# Each run that deploys is one production deployment of g1t.sh, made by the
14# jobs that name `environment: production` (one per run, however many jobs):
15# in progress when the first starts, then a success or a failure when the
16# run ends. It shows on the project's Deployments page and as the commit's
17# `deploy / production` check. The plan job reads production's secrets
18# with `deployment: false`, so a dry run or a change that deploys nothing
19# makes no deployment.
20#
21# Needs the repository secret CLOUDFLARE_API_TOKEN (a Production row, with
22# Containers write), the variable CLOUDFLARE_ACCOUNT_ID, and
23# api.cloudflare.com among the project's workflow-only domains for
24# deploy.yml in production (Settings, Guardrails), and
25# registry.cloudflare.com there too, to find, pull and push the runner's
26# image. A job that must build that image (the `runner-image` group) does
27# so with its own Docker Engine, on a larger machine. Optionally, the
28# secret STATUS_DEPLOY_TOKEN (the status Worker's secret of the same name)
29# and status.g1t.sh among the same workflow-only domains, so status.g1t.sh
30# hears each deploy start and finish.
31name: Deploy
32
33on:
34 push:
35 branches: [main]
36 workflow_dispatch:
37 inputs:
38 units:
39 description: "Units to deploy whether or not they changed, comma separated (empty: what changed)"
40 type: string
41 default: ""
42 all:
43 description: "Deploy every unit"
44 type: boolean
45 default: false
46 dry_run:
47 description: "Plan only: deploy nothing"
48 type: boolean
49 default: false
50
51# Its token only reads: deploying uses CLOUDFLARE_API_TOKEN, and g1t
52# records the deployments itself.
53permissions:
54 contents: read
55
56# One deploy at a time, and never one cut off halfway: the next waits.
57concurrency:
58 group: deploy-production
59 cancel-in-progress: false
60
61env:
62 CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
63 CARGO_TERM_COLOR: never
64 WRANGLER_SEND_METRICS: "false"
65
66jobs:
67 check:
68 name: Check
69 runs-on: ubuntu-latest
70 timeout-minutes: 20
71 steps:
72 - uses: actions/checkout@v5
73 # npm's downloads for the whole lockfile, which the npm-cache job
74 # keeps: an install reads them first and asks the registry only for
75 # what changed, so a dropped connection to it rarely stops the job.
76 # Only restored: a job that installs part of the lockfile never
77 # saves, or its part would stand in for the whole under that key.
78 - name: Cache npm's downloads
79 uses: actions/cache/restore@v4
80 with:
81 path: ~/.npm
82 key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
83 restore-keys: npm-full-${{ runner.os }}-
84 - name: Install Wrangler
85 run: node scripts/deploy/npm-ci.mjs --prefer-offline --workspaces=false --no-audit --no-fund
86 - name: The manifest matches every wrangler.jsonc
87 run: node scripts/deploy.mjs manifest --check
88 - name: The deploy tool's tests
89 run: npm run test:deploy
90
91 # npm's downloads for the whole lockfile, saved once for each lockfile
92 # on main, which every install of this and the other workflows starts
93 # from. Before, every job saved its own part under one key, and the
94 # first to finish (Wrangler alone, 73 MB) stood in for the rest: each
95 # stage then downloaded hundreds of MB through the runner's egress,
96 # 50 MB tarballs among them. Nothing waits for this job, and a failed
97 # download only leaves the cache as it was.
98 npm-cache:
99 name: npm's downloads
100 runs-on: ubuntu-latest
101 timeout-minutes: 20
102 steps:
103 - uses: actions/checkout@v5
104 - name: Look for this lockfile's downloads
105 id: saved
106 uses: actions/cache/restore@v4
107 with:
108 path: ~/.npm
109 key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
110 lookup-only: true
111 - name: Start from the last lockfile's
112 if: ${{ steps.saved.outputs.cache-hit != 'true' }}
113 uses: actions/cache/restore@v4
114 with:
115 path: ~/.npm
116 key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
117 restore-keys: npm-full-${{ runner.os }}-
118 - name: Download every package
119 id: download
120 if: ${{ steps.saved.outputs.cache-hit != 'true' }}
121 continue-on-error: true
122 run: node scripts/deploy/npm-ci.mjs --prefer-offline --ignore-scripts --no-audit --no-fund
123 - name: Save them
124 if: ${{ steps.saved.outputs.cache-hit != 'true' && steps.download.outcome == 'success' }}
125 uses: actions/cache/save@v4
126 with:
127 path: ~/.npm
128 key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
129
130 # Runs beside check: nothing deploys until both have succeeded.
131 plan:
132 name: Plan
133 runs-on: ubuntu-latest
134 # Production's secrets, without a deployment: planning deploys nothing.
135 environment:
136 name: production
137 deployment: false
138 timeout-minutes: 15
139 outputs:
140 migrate: ${{ steps.plan.outputs.migrate }}
141 migrate_units: ${{ steps.plan.outputs.migrate_units }}
142 has_core: ${{ steps.plan.outputs.has_core }}
143 core: ${{ steps.plan.outputs.core }}
144 has_edge: ${{ steps.plan.outputs.has_edge }}
145 edge: ${{ steps.plan.outputs.edge }}
146 has_front: ${{ steps.plan.outputs.has_front }}
147 front: ${{ steps.plan.outputs.front }}
148 steps:
149 - uses: actions/checkout@v5
150 with:
151 # Each Worker's live commit is compared with this one.
152 fetch-depth: 0
153 # No npm ci: with CLOUDFLARE_API_TOKEN the plan reads Cloudflare's API
154 # itself (scripts/deploy/cloudflare.mjs), and needs no Wrangler.
155 - name: Plan
156 id: plan
157 env:
158 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
159 UNITS: ${{ inputs.units }}
160 ALL: ${{ inputs.all }}
161 run: |
162 args=()
163 if [ -n "$UNITS" ]; then args+=(--only "$UNITS" --force); fi
164 if [ "$ALL" = "true" ]; then args+=(--all); fi
165 node scripts/deploy.mjs plan "${args[@]}" --github-output
166
167 migrate:
168 name: Migrations
169 needs: [check, plan]
170 if: ${{ needs.plan.outputs.migrate == 'true' && inputs.dry_run != true }}
171 runs-on: ubuntu-latest
172 environment:
173 name: production
174 url: https://g1t.sh
175 timeout-minutes: 20
176 steps:
177 - uses: actions/checkout@v5
178 # npm's downloads, kept between runs: an install reads them first
179 # and asks the registry only for what changed, so a dropped
180 # connection to it rarely stops the job.
181 - name: Cache npm's downloads
182 uses: actions/cache/restore@v4
183 with:
184 path: ~/.npm
185 key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
186 restore-keys: npm-full-${{ runner.os }}-
187 - name: Install Wrangler
188 run: node scripts/deploy/npm-ci.mjs --prefer-offline --workspaces=false --no-audit --no-fund
189 - name: Apply pending migrations
190 env:
191 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
192 run: node scripts/deploy.mjs migrate --only "${{ needs.plan.outputs.migrate_units }}"
193
194 core:
195 name: core (${{ matrix.group }})
196 needs: [check, plan, migrate]
197 # Runs when nothing before it failed: a migrate job skipped for having
198 # nothing to apply is not a failure.
199 if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_core == 'true' && inputs.dry_run != true }}
200 # Rust builds and the runner's image get 4 vCPUs; everything else the
201 # standard machine.
202 runs-on: ${{ (matrix.rust || matrix.image) && 'g1t-4core' || 'ubuntu-latest' }}
203 environment:
204 name: production
205 url: https://g1t.sh
206 timeout-minutes: 60
207 strategy:
208 # A deploy cut off halfway is worse than one that finishes: the other
209 # jobs of a stage run on when one fails, and the next stage does not.
210 fail-fast: false
211 max-parallel: 4
212 matrix: ${{ fromJSON(needs.plan.outputs.core) }}
213 steps: &deploy
214 - uses: actions/checkout@v5
215 with:
216 fetch-depth: 0
217 # Rust workers: the wasm target, and worker-build kept between runs
218 # (its version is pinned in scripts/build-rust-worker.mjs).
219 - name: Rust for Workers
220 if: ${{ matrix.rust }}
221 run: rustup target add wasm32-unknown-unknown
222 - name: Cache worker-build
223 if: ${{ matrix.rust }}
224 uses: actions/cache@v4
225 with:
226 path: ~/.cargo/bin/worker-build
227 key: worker-build-${{ runner.os }}-${{ hashFiles('scripts/build-rust-worker.mjs') }}
228 - name: Cache worker-build's tools (wasm-bindgen, esbuild)
229 if: ${{ matrix.rust }}
230 uses: actions/cache@v4
231 with:
232 path: ~/.cache/worker-build
233 key: worker-build-tools-${{ runner.os }}-${{ hashFiles('scripts/build-rust-worker.mjs') }}
234 - name: Cache crates
235 if: ${{ matrix.rust }}
236 uses: actions/cache@v4
237 with:
238 path: ~/.cargo/registry/cache
239 key: cargo-crates-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
240 restore-keys: cargo-crates-${{ runner.os }}-
241 # The compiled dependencies of this job's units, for wasm32 and the
242 # build scripts and proc macros they run. Cargo calls rustc again for
243 # the workspace's own crates whatever is cached (a checkout's sources
244 # are newer); sccache, below, answers those calls. So an entry is
245 # saved only when the dependencies change: a new Cargo.lock, or a new
246 # base image (base.json names its Rust). Otherwise the nearest earlier
247 # entry, of any group, is a start.
248 - name: Cache the Cargo target
249 if: ${{ matrix.rust }}
250 uses: actions/cache@v4
251 with:
252 path: |
253 target/release
254 target/wasm32-unknown-unknown/release
255 !target/**/incremental
256 !target/**/*.wasm
257 key: cargo-target-${{ runner.os }}-${{ matrix.group }}-${{ hashFiles('Cargo.lock', 'services/runner/base.json') }}
258 restore-keys: |
259 cargo-target-${{ runner.os }}-${{ matrix.group }}-
260 cargo-target-${{ runner.os }}-
261 # The runner's image: its binary, built natively for musl (the base
262 # has musl-gcc; the target is added here), with its Cargo target kept
263 # between runs. The image itself is built and pushed with the job's
264 # own Docker Engine (scripts/deploy/image.mjs).
265 - name: Rust for the runner
266 if: ${{ matrix.image }}
267 run: rustup target add x86_64-unknown-linux-musl
268 - name: Cache the runner's build
269 if: ${{ matrix.image }}
270 uses: actions/cache@v4
271 with:
272 path: |
273 ~/.cargo/registry/cache
274 target/x86_64-unknown-linux-musl/release
275 !target/**/incremental
276 key: runner-musl-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'services/runner/base.json') }}
277 restore-keys: runner-musl-${{ runner.os }}-
278 # Every rustc call that makes a library (the workspace's crates,
279 # which Cargo compiles again on every checkout, and any dependency
280 # not restored above) is looked up by its inputs in the repository's
281 # Actions cache: unchanged crates come back from it instead of being
282 # compiled. A Worker's own crate (a cdylib) and the runner's binary
283 # are still compiled. worker-build runs Cargo, so its wasm32 builds
284 # go through it too; wasm-bindgen and wasm-opt are not rustc.
285 # Pinned by version and sha256 in scripts/sccache.sh; without the
286 # cache, the job builds as before.
287 - name: sccache
288 if: ${{ matrix.rust || matrix.image }}
289 run: bash scripts/sccache.sh install
290 # npm's downloads for the whole lockfile, only restored, as the
291 # check job restores them: the install reads them first (--prefer-offline, in
292 # scripts/deploy/stack.mjs) and asks the registry only for what
293 # changed, so a connection it drops mid-install rarely stops a stage.
294 - name: Cache npm's downloads
295 uses: actions/cache/restore@v4
296 with:
297 path: ~/.npm
298 key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
299 restore-keys: npm-full-${{ runner.os }}-
300 - name: Install
301 run: node scripts/deploy.mjs install --only "${{ matrix.units }}"
302 - name: Deploy ${{ matrix.units }}
303 env:
304 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
305 # status.g1t.sh hears the deploy start and finish, so its restarts
306 # are not drafted as incidents. Optional: without it, nothing is sent.
307 STATUS_DEPLOY_TOKEN: ${{ secrets.STATUS_DEPLOY_TOKEN }}
308 run: node scripts/deploy.mjs deploy --only "${{ matrix.units }}" --force --no-migrations --concurrency 2
309 # Hits and misses, on the log and in the run's summary.
310 - name: sccache's hits and misses
311 if: ${{ always() && (matrix.rust || matrix.image) }}
312 run: bash scripts/sccache.sh stats
313
314 edge:
315 name: edge (${{ matrix.group }})
316 needs: [check, plan, migrate, core]
317 if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_edge == 'true' && inputs.dry_run != true }}
318 runs-on: ${{ (matrix.rust || matrix.image) && 'g1t-4core' || 'ubuntu-latest' }}
319 environment:
320 name: production
321 url: https://g1t.sh
322 timeout-minutes: 60
323 strategy:
324 fail-fast: false
325 max-parallel: 4
326 matrix: ${{ fromJSON(needs.plan.outputs.edge) }}
327 steps: *deploy
328
329 front:
330 name: front (${{ matrix.group }})
331 needs: [check, plan, migrate, core, edge]
332 if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_front == 'true' && inputs.dry_run != true }}
333 runs-on: ${{ (matrix.rust || matrix.image) && 'g1t-4core' || 'ubuntu-latest' }}
334 environment:
335 name: production
336 url: https://g1t.sh
337 timeout-minutes: 60
338 strategy:
339 fail-fast: false
340 max-parallel: 4
341 matrix: ${{ fromJSON(needs.plan.outputs.front) }}
342 steps: *deploy
343
344 # Once everything has deployed: the ways in for someone new still work.
345 # The pages a visitor lands on load, and the sign-up form reaches
346 # identity, sent an address it refuses before keeping or counting
347 # anything, so no account is made. scripts/ops/smoke.mjs.
348 smoke:
349 name: Smoke
350 needs: [check, plan, core, edge, front]
351 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') }}
352 runs-on: ubuntu-latest
353 timeout-minutes: 5
354 steps:
355 - uses: actions/checkout@v5
356 - name: Sign-in and sign-up work
357 run: node scripts/ops/smoke.mjs