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.

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