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