Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

347 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 # ubuntu-latest: 4 vCPU in this public repository, as on GitHub.
160 runs-on: ubuntu-latest
161 environment:
162 name: production
163 url: https://g1t.sh
164 timeout-minutes: 60
165 strategy:
166 # A deploy cut off halfway is worse than one that finishes: the other
167 # jobs of a stage run on when one fails, and the next stage does not.
168 fail-fast: false
169 max-parallel: 4
170 matrix: ${{ fromJSON(needs.plan.outputs.core) }}
171 steps: &deploy
172 - uses: actions/checkout@v5
173 with:
174 fetch-depth: 0
175 # Rust workers: the wasm target, and worker-build kept between runs
176 # (its version is pinned in scripts/build-rust-worker.mjs).
177 - name: Rust for Workers
178 if: ${{ matrix.rust }}
179 run: rustup target add wasm32-unknown-unknown
180 - name: Cache worker-build
181 if: ${{ matrix.rust }}
182 uses: actions/cache@v4
183 with:
184 path: ~/.cargo/bin/worker-build
185 key: worker-build-${{ runner.os }}-${{ hashFiles('scripts/build-rust-worker.mjs') }}
186 - name: Cache worker-build's tools (wasm-bindgen, esbuild)
187 if: ${{ matrix.rust }}
188 uses: actions/cache@v4
189 with:
190 path: ~/.cache/worker-build
191 key: worker-build-tools-${{ runner.os }}-${{ hashFiles('scripts/build-rust-worker.mjs') }}
192 # rust: the toolchain; cargo: Cargo.lock's crates.io and git packages,
193 # not the workspace's own versions (scripts/deploy/ci-cache.mjs).
194 - name: Cache keys
195 id: keys
196 if: ${{ matrix.rust || matrix.image }}
197 run: node scripts/deploy/ci-cache.mjs keys
198 # Every crate the lockfile names, as CI's Rust job saves them on
199 # main. Only restored: a stage downloads the crates of its own units,
200 # and saving those would stand in for the whole under that key.
201 - name: Cache crates
202 if: ${{ matrix.rust || matrix.image }}
203 uses: actions/cache/restore@v4
204 with:
205 path: |
206 ~/.cargo/registry/index
207 ~/.cargo/registry/cache
208 key: cargo-crates-${{ runner.os }}-${{ steps.keys.outputs.cargo }}
209 restore-keys: cargo-crates-${{ runner.os }}-
210 # The compiled dependencies of this job's units, for wasm32 and the
211 # build scripts and proc macros they run. Cargo calls rustc again for
212 # the workspace's own crates whatever is cached (a checkout's sources
213 # are newer); sccache, below, answers those calls. So an entry is
214 # saved only when the dependencies or the toolchain change, not when
215 # a release bumps a workspace crate's version; otherwise this group's
216 # last entry for the toolchain is a start, else any group's.
217 - name: Cache the Cargo target
218 if: ${{ matrix.rust }}
219 uses: actions/cache@v4
220 with:
221 path: |
222 target/release
223 target/wasm32-unknown-unknown/release
224 !target/**/incremental
225 !target/**/*.wasm
226 key: cargo-workers-${{ runner.os }}-${{ steps.keys.outputs.rust }}-${{ matrix.group }}-${{ steps.keys.outputs.cargo }}
227 restore-keys: |
228 cargo-workers-${{ runner.os }}-${{ steps.keys.outputs.rust }}-${{ matrix.group }}-
229 cargo-workers-${{ runner.os }}-${{ steps.keys.outputs.rust }}-
230 # The runner's image: its binary, built natively for musl (the base
231 # has musl-gcc; the target is added here), with its Cargo target kept
232 # between runs: the musl target's, and target/release, where Cargo
233 # puts the build scripts and proc macros it runs on this machine
234 # (without it, 87 crates were compiled again on every deploy). The
235 # image itself is made in the registry, without Docker: the base's
236 # layers and one with the binary (scripts/deploy/registry.mjs).
237 # Building it with the job's Docker Engine, only if that fails, pulls
238 # the 4.5 GB base onto the job's disk.
239 - name: Rust for the runner
240 if: ${{ matrix.image }}
241 run: rustup target add x86_64-unknown-linux-musl
242 - name: Cache the runner's build
243 if: ${{ matrix.image }}
244 uses: actions/cache@v4
245 with:
246 path: |
247 target/release
248 target/x86_64-unknown-linux-musl/release
249 !target/**/incremental
250 key: runner-musl-${{ runner.os }}-${{ steps.keys.outputs.rust }}-${{ steps.keys.outputs.cargo }}
251 restore-keys: runner-musl-${{ runner.os }}-${{ steps.keys.outputs.rust }}-
252 # Every rustc call that makes a library (the workspace's crates,
253 # which Cargo compiles again on every checkout, and any dependency
254 # not restored above) is looked up by its inputs in the repository's
255 # Actions cache: unchanged crates come back from it instead of being
256 # compiled. A Worker's own crate (a cdylib) and the runner's binary
257 # are still compiled. worker-build runs Cargo, so its wasm32 builds
258 # go through it too; wasm-bindgen and wasm-opt are not rustc.
259 # Pinned by version and sha256 in scripts/sccache.sh; without the
260 # cache, the job builds as before.
261 - name: sccache
262 if: ${{ matrix.rust || matrix.image }}
263 run: bash scripts/sccache.sh install
264 # npm's downloads for the whole lockfile, only restored, as the
265 # check job restores them: the install reads them first (--prefer-offline, in
266 # scripts/deploy/stack.mjs) and asks the registry only for what
267 # changed, so a connection it drops mid-install rarely stops a stage.
268 - name: Cache npm's downloads
269 uses: actions/cache/restore@v4
270 with:
271 path: ~/.npm
272 key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
273 restore-keys: npm-full-${{ runner.os }}-
274 - name: Install
275 run: node scripts/deploy.mjs install --only "${{ matrix.units }}"
276 # The runner's image job: npm's downloads are only restored here, and
277 # the install has read them, so they go, leaving the disk to the
278 # runner's build (and to Docker, should the image need it). What the
279 # disk holds is logged before and after, so a job that runs out of
280 # room says what took it.
281 - name: Room for the runner's image
282 if: ${{ matrix.image }}
283 run: |
284 rm -rf ~/.npm
285 df -h / "$HOME" /tmp
286 du -sh "$HOME"/.cargo target node_modules 2>/dev/null || true
287 - name: Deploy ${{ matrix.units }}
288 env:
289 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
290 # status.g1t.sh hears the deploy start and finish, so its restarts
291 # are not drafted as incidents. Optional: without it, nothing is sent.
292 STATUS_DEPLOY_TOKEN: ${{ secrets.STATUS_DEPLOY_TOKEN }}
293 run: node scripts/deploy.mjs deploy --only "${{ matrix.units }}" --force --no-migrations --concurrency 2
294 - name: The disk after the runner's image
295 if: ${{ always() && matrix.image }}
296 run: |
297 df -h / "$HOME" /tmp
298 du -sh "$HOME"/.cargo target node_modules 2>/dev/null || true
299 # Hits and misses, on the log and in the run's summary.
300 - name: sccache's hits and misses
301 if: ${{ always() && (matrix.rust || matrix.image) }}
302 run: bash scripts/sccache.sh stats
303
304 edge:
305 name: edge (${{ matrix.group }})
306 needs: [check, plan, migrate, core]
307 if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_edge == 'true' && inputs.dry_run != true }}
308 runs-on: ubuntu-latest
309 environment:
310 name: production
311 url: https://g1t.sh
312 timeout-minutes: 60
313 strategy:
314 fail-fast: false
315 max-parallel: 4
316 matrix: ${{ fromJSON(needs.plan.outputs.edge) }}
317 steps: *deploy
318
319 front:
320 name: front (${{ matrix.group }})
321 needs: [check, plan, migrate, core, edge]
322 if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_front == 'true' && inputs.dry_run != true }}
323 runs-on: ubuntu-latest
324 environment:
325 name: production
326 url: https://g1t.sh
327 timeout-minutes: 60
328 strategy:
329 fail-fast: false
330 max-parallel: 4
331 matrix: ${{ fromJSON(needs.plan.outputs.front) }}
332 steps: *deploy
333
334 # Once everything has deployed: the ways in for someone new still work.
335 # The pages a visitor lands on load, and the sign-up form reaches
336 # identity, sent an address it refuses before keeping or counting
337 # anything, so no account is made. scripts/ops/smoke.mjs.
338 smoke:
339 name: Smoke
340 needs: [check, plan, core, edge, front]
341 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') }}
342 runs-on: ubuntu-latest
343 timeout-minutes: 5
344 steps:
345 - uses: actions/checkout@v5
346 - name: Sign-in and sign-up work
347 run: node scripts/ops/smoke.mjs