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