Skip to content
258 linesCodeBlameRaw
1# Releases the self-hosted runner, g1t-runner (crates/runner): builds it for
2# Linux, macOS and Windows on x64 and arm64, pushes its container image,
3# builds the images `g1t-runner sandbox` runs on people's own computers,
4# signs the release (binaries and sandbox images in one manifest), and
5# publishes it to g1t.sh/downloads/runner/ (the g1t-downloads R2 bucket).
6# Runners already out there update themselves to it.
7#
8# A release is a tag `runner-v<version>`, where the version is the one in
9# crates/runner/Cargo.toml; or run it by hand. scripts/runner-release.mjs
10# and scripts/sandbox-image.mjs do the work, and the first's header says how
11# to make the release key the first time; CONTRIBUTING.md ("Deploying")
12# says how a release is made.
13#
14# The image jobs run on g1t's own machines (g1t-4core), whose jobs have a
15# Docker Engine of their own (crates/runner/src/docker/engine.rs). arm64
16# images need QEMU's emulators registered with the kernel; each image job
17# tries that, and when the machine does not allow it, builds amd64 alone
18# and says so in a warning rather than failing the release.
19name: Runner release
20
21on:
22 push:
23 tags: ["runner-v*"]
24 workflow_dispatch:
25
26permissions:
27 contents: read
28
29concurrency:
30 group: runner-release
31 cancel-in-progress: false
32
33env:
34 CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
35 WRANGLER_SEND_METRICS: "false"
36
37jobs:
38 binaries:
39 # One job per target, side by side: each cross-compile takes about six
40 # minutes, and one after another they were most of a release's wait.
41 name: Build ${{ matrix.target }}
42 runs-on: ubuntu-latest
43 timeout-minutes: 60
44 strategy:
45 fail-fast: true
46 matrix:
47 target: [linux-x64, linux-arm64, windows-x64]
48 steps:
49 - uses: actions/checkout@v5
50 - name: The tag names this version
51 if: startsWith(github.ref, 'refs/tags/runner-v')
52 run: |
53 version="$(sed -n 's/^version = "\(.*\)"/\1/p' crates/runner/Cargo.toml | head -1)"
54 [ "runner-v$version" = "${GITHUB_REF_NAME}" ] || { echo "::error::The tag is ${GITHUB_REF_NAME}, but crates/runner is $version"; exit 1; }
55 # zig and cargo-zigbuild, kept between releases: installing them took
56 # four minutes of every one. Change the key to take newer versions.
57 - name: Cache zig and cargo-zigbuild
58 id: zig
59 uses: actions/cache@v4
60 with:
61 path: |
62 ~/.local/lib/python3*/site-packages/ziglang
63 ~/.local/bin
64 ~/.cargo/bin/cargo-zigbuild
65 key: zig-0.13.0-zigbuild-${{ runner.os }}-1
66 - name: Install zig and cargo-zigbuild
67 if: steps.zig.outputs.cache-hit != 'true'
68 run: |
69 # The runner's Debian marks its Python as externally managed (PEP 668); zig goes under ~/.local anyway.
70 pip install --user --break-system-packages ziglang==0.13.0
71 cargo install --locked cargo-zigbuild
72 - name: zig on the PATH
73 run: echo "$HOME/.local/bin" >> "$GITHUB_PATH"
74 # Each target's build, kept between releases; a release after a
75 # Cargo.lock change starts from the last one's rather than from nothing.
76 - uses: actions/cache@v4
77 with:
78 path: |
79 ~/.cargo/registry
80 target
81 key: runner-release-${{ matrix.target }}-${{ hashFiles('Cargo.lock') }}
82 restore-keys: runner-release-${{ matrix.target }}-
83 # Crates whose inputs did not change come back from the Actions cache
84 # instead of being compiled (scripts/sccache.sh); without it, the job
85 # builds as before.
86 - name: sccache
87 run: bash scripts/sccache.sh install
88 # The cache above brings back the last release's target/runner-release
89 # too: left there, it would go out with this one's, and the jobs after
90 # would find two of each file.
91 - name: Only this release's files
92 run: rm -rf target/runner-release
93 - name: Build Linux and Windows
94 # The macOS targets need Apple's SDK to link (the Security and
95 # CoreFoundation frameworks), which zig does not carry, so they are
96 # left out until this builds in the rust-cross/cargo-zigbuild image
97 # as the CLI's release does (scripts/cli-release.mjs). A Mac runner
98 # keeps the version it has, since the manifest names no file for it.
99 env:
100 G1T_RUNNER_RELEASE_KEY: ${{ vars.RUNNER_RELEASE_PUBLIC_KEY }}
101 run: node scripts/runner-release.mjs build --targets ${{ matrix.target }}
102 - name: sccache's hits
103 if: always()
104 run: bash scripts/sccache.sh stats
105 - uses: actions/upload-artifact@v4
106 with:
107 name: binaries-${{ matrix.target }}
108 path: target/runner-release/
109 overwrite: true
110
111 image:
112 name: Container image
113 needs: binaries
114 runs-on: g1t-4core
115 # Production's secrets, without a deployment: a release is not a deploy of
116 # g1t.sh, and would otherwise show on its card as one in progress.
117 environment:
118 name: production
119 deployment: false
120 timeout-minutes: 45
121 # Its token pushes the image to g1t's registry.
122 permissions:
123 contents: read
124 packages: write
125 outputs:
126 # The pushed image by digest, which the sandbox's manifest names.
127 runner_image: ${{ steps.push.outputs.runner_image }}
128 steps:
129 - uses: actions/checkout@v5
130 - uses: actions/download-artifact@v4
131 with:
132 pattern: binaries-*
133 merge-multiple: true
134 path: release
135 - name: QEMU for arm64, if this machine allows it
136 id: qemu
137 run: |
138 if docker run --privileged --rm tonistiigi/binfmt --install arm64 >/dev/null 2>&1 \
139 && [ "$(docker run --rm --platform linux/arm64 debian:bookworm-slim uname -m 2>/dev/null)" = aarch64 ]; then
140 echo "arches=amd64 arm64" >> "$GITHUB_OUTPUT"
141 else
142 echo "::warning::This machine cannot run arm64 programs (QEMU's emulators could not be registered), so the runner's image is built for amd64 alone this time."
143 echo "arches=amd64" >> "$GITHUB_OUTPUT"
144 fi
145 - name: Build and push
146 id: push
147 # To g1t's own registry, where flagon-io keeps it public.
148 env:
149 IMAGE: g1t.sh/flagon-io/g1t-runner
150 ARCHES: ${{ steps.qemu.outputs.arches }}
151 run: |
152 version="$(sed -n 's/^version = "\(.*\)"/\1/p' crates/runner/Cargo.toml | head -1)"
153 echo "${{ secrets.G1T_TOKEN }}" | docker login g1t.sh -u g1t --password-stdin
154 tags=()
155 for arch in $ARCHES; do
156 mkdir -p "context-$arch"
157 cp deploy/runner/Dockerfile "context-$arch/"
158 name="g1t-runner-linux-$([ "$arch" = amd64 ] && echo x64 || echo arm64)"
159 cp "release/$version/$name" "context-$arch/g1t-runner"
160 docker buildx build --platform "linux/$arch" -t "$IMAGE:$version-$arch" --load "context-$arch"
161 docker push "$IMAGE:$version-$arch"
162 tags+=("$IMAGE:$version-$arch")
163 done
164 docker buildx imagetools create -t "$IMAGE:$version" -t "$IMAGE:latest" "${tags[@]}"
165 digest="$(docker buildx imagetools inspect "$IMAGE:$version" --format '{{json .Manifest}}' | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s).digest))')"
166 echo "runner_image=$IMAGE@$digest" >> "$GITHUB_OUTPUT"
167
168 sandbox:
169 name: Sandbox images
170 needs: [binaries, image]
171 runs-on: g1t-4core
172 timeout-minutes: 60
173 steps:
174 - uses: actions/checkout@v5
175 - uses: actions/download-artifact@v4
176 with:
177 pattern: binaries-*
178 merge-multiple: true
179 path: target/runner-release
180 - name: QEMU for arm64, if this machine allows it
181 id: qemu
182 run: |
183 if docker run --privileged --rm tonistiigi/binfmt --install arm64 >/dev/null 2>&1 \
184 && [ "$(docker run --rm --platform linux/arm64 debian:bookworm-slim uname -m 2>/dev/null)" = aarch64 ]; then
185 echo "arches=x64 arm64" >> "$GITHUB_OUTPUT"
186 else
187 echo "::warning::This machine cannot run arm64 programs, so the sandbox's images are built for x64 alone this time: an arm64 Mac or Linux machine keeps the sandbox image it has."
188 echo "arches=x64" >> "$GITHUB_OUTPUT"
189 fi
190 - name: Build the WSL root filesystem and the VM's disk
191 env:
192 ARCHES: ${{ steps.qemu.outputs.arches }}
193 run: |
194 version="$(sed -n 's/^version = "\(.*\)"/\1/p' crates/runner/Cargo.toml | head -1)"
195 dir="target/runner-release/$version/"
196 for arch in $ARCHES; do
197 node scripts/sandbox-image.mjs build --arch "$arch" --runner "${dir}g1t-runner-linux-$arch" --out "$dir"
198 done
199 - name: The manifest's sandbox section
200 env:
201 RUNNER_SANDBOX_IMAGE: ${{ needs.image.outputs.runner_image }}
202 run: |
203 version="$(sed -n 's/^version = "\(.*\)"/\1/p' crates/runner/Cargo.toml | head -1)"
204 node scripts/sandbox-image.mjs section --dir "target/runner-release/$version/"
205 - uses: actions/upload-artifact@v4
206 with:
207 name: sandbox
208 path: |
209 target/runner-release/*/g1t-sandbox-*
210 target/runner-release/*/sandbox.json
211 overwrite: true
212
213 publish:
214 name: Sign and publish
215 needs: [binaries, image, sandbox]
216 # The binaries go out even when an image job failed: the manifest then
217 # names no sandbox images, and computers keep the sandbox they have.
218 if: ${{ always() && needs.binaries.result == 'success' }}
219 runs-on: ubuntu-latest
220 environment:
221 name: production
222 deployment: false
223 timeout-minutes: 30
224 steps:
225 - uses: actions/checkout@v5
226 - uses: actions/download-artifact@v4
227 with:
228 pattern: binaries-*
229 merge-multiple: true
230 path: target/runner-release
231 - uses: actions/download-artifact@v4
232 if: ${{ needs.sandbox.result == 'success' }}
233 with:
234 name: sandbox
235 path: target/runner-release
236 - name: Sign
237 env:
238 RUNNER_RELEASE_KEY: ${{ secrets.RUNNER_RELEASE_KEY }}
239 G1T_RUNNER_RELEASE_KEY: ${{ vars.RUNNER_RELEASE_PUBLIC_KEY }}
240 run: |
241 node scripts/runner-release.mjs manifest
242 node scripts/runner-release.mjs sign
243 node scripts/runner-release.mjs verify
244 # npm's downloads, kept between runs: an install reads them first
245 # and asks the registry only for what changed, so a dropped
246 # connection to it rarely stops the job.
247 - name: Cache npm's downloads
248 uses: actions/cache@v4
249 with:
250 path: ~/.npm
251 key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
252 restore-keys: npm-${{ runner.os }}-
253 - name: Install Wrangler
254 run: npm ci --prefer-offline --workspaces=false --no-audit --no-fund
255 - name: Publish to g1t.sh/downloads/runner
256 env:
257 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
258 run: node scripts/runner-release.mjs publish