Skip to content
252 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 - name: Build Linux and Windows
89 # The macOS targets need Apple's SDK to link (the Security and
90 # CoreFoundation frameworks), which zig does not carry, so they are
91 # left out until this builds in the rust-cross/cargo-zigbuild image
92 # as the CLI's release does (scripts/cli-release.mjs). A Mac runner
93 # keeps the version it has, since the manifest names no file for it.
94 env:
95 G1T_RUNNER_RELEASE_KEY: ${{ vars.RUNNER_RELEASE_PUBLIC_KEY }}
96 RUNNER_AGENT_IMAGE: ${{ vars.RUNNER_AGENT_IMAGE }}
97 run: node scripts/runner-release.mjs build --targets ${{ matrix.target }}
98 - name: sccache's hits
99 if: always()
100 run: bash scripts/sccache.sh stats
101 - uses: actions/upload-artifact@v4
102 with:
103 name: binaries-${{ matrix.target }}
104 path: target/runner-release/
105 overwrite: true
106
107 image:
108 name: Container image
109 needs: binaries
110 runs-on: g1t-4core
111 # Production's secrets, without a deployment: a release is not a deploy of
112 # g1t.sh, and would otherwise show on its card as one in progress.
113 environment:
114 name: production
115 deployment: false
116 timeout-minutes: 45
117 # Its token pushes the image to g1t's registry.
118 permissions:
119 contents: read
120 packages: write
121 outputs:
122 # The pushed image by digest, which the sandbox's manifest names.
123 runner_image: ${{ steps.push.outputs.runner_image }}
124 steps:
125 - uses: actions/checkout@v5
126 - uses: actions/download-artifact@v4
127 with:
128 pattern: binaries-*
129 merge-multiple: true
130 path: release
131 - name: QEMU for arm64, if this machine allows it
132 id: qemu
133 run: |
134 if docker run --privileged --rm tonistiigi/binfmt --install arm64 >/dev/null 2>&1 \
135 && [ "$(docker run --rm --platform linux/arm64 debian:bookworm-slim uname -m 2>/dev/null)" = aarch64 ]; then
136 echo "arches=amd64 arm64" >> "$GITHUB_OUTPUT"
137 else
138 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."
139 echo "arches=amd64" >> "$GITHUB_OUTPUT"
140 fi
141 - name: Build and push
142 id: push
143 # To g1t's own registry, where flagon-io keeps it public.
144 env:
145 IMAGE: g1t.sh/flagon-io/g1t-runner
146 ARCHES: ${{ steps.qemu.outputs.arches }}
147 run: |
148 version="$(sed -n 's/^version = "\(.*\)"/\1/p' crates/runner/Cargo.toml | head -1)"
149 echo "${{ secrets.G1T_TOKEN }}" | docker login g1t.sh -u g1t --password-stdin
150 tags=()
151 for arch in $ARCHES; do
152 mkdir -p "context-$arch"
153 cp deploy/runner/Dockerfile "context-$arch/"
154 name="g1t-runner-linux-$([ "$arch" = amd64 ] && echo x64 || echo arm64)"
155 cp release/*/"$name" "context-$arch/g1t-runner"
156 docker buildx build --platform "linux/$arch" -t "$IMAGE:$version-$arch" --load "context-$arch"
157 docker push "$IMAGE:$version-$arch"
158 tags+=("$IMAGE:$version-$arch")
159 done
160 docker buildx imagetools create -t "$IMAGE:$version" -t "$IMAGE:latest" "${tags[@]}"
161 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))')"
162 echo "runner_image=$IMAGE@$digest" >> "$GITHUB_OUTPUT"
163
164 sandbox:
165 name: Sandbox images
166 needs: [binaries, image]
167 runs-on: g1t-4core
168 timeout-minutes: 60
169 steps:
170 - uses: actions/checkout@v5
171 - uses: actions/download-artifact@v4
172 with:
173 pattern: binaries-*
174 merge-multiple: true
175 path: target/runner-release
176 - name: QEMU for arm64, if this machine allows it
177 id: qemu
178 run: |
179 if docker run --privileged --rm tonistiigi/binfmt --install arm64 >/dev/null 2>&1 \
180 && [ "$(docker run --rm --platform linux/arm64 debian:bookworm-slim uname -m 2>/dev/null)" = aarch64 ]; then
181 echo "arches=x64 arm64" >> "$GITHUB_OUTPUT"
182 else
183 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."
184 echo "arches=x64" >> "$GITHUB_OUTPUT"
185 fi
186 - name: Build the WSL root filesystem and the VM's disk
187 env:
188 ARCHES: ${{ steps.qemu.outputs.arches }}
189 run: |
190 dir="$(echo target/runner-release/*/)"
191 for arch in $ARCHES; do
192 node scripts/sandbox-image.mjs build --arch "$arch" --runner "${dir}g1t-runner-linux-$arch" --out "$dir"
193 done
194 - name: The manifest's sandbox section
195 env:
196 RUNNER_SANDBOX_IMAGE: ${{ needs.image.outputs.runner_image }}
197 run: node scripts/sandbox-image.mjs section --dir "$(echo target/runner-release/*/)"
198 - uses: actions/upload-artifact@v4
199 with:
200 name: sandbox
201 path: |
202 target/runner-release/*/g1t-sandbox-*
203 target/runner-release/*/sandbox.json
204 overwrite: true
205
206 publish:
207 name: Sign and publish
208 needs: [binaries, image, sandbox]
209 # The binaries go out even when an image job failed: the manifest then
210 # names no sandbox images, and computers keep the sandbox they have.
211 if: ${{ always() && needs.binaries.result == 'success' }}
212 runs-on: ubuntu-latest
213 environment:
214 name: production
215 deployment: false
216 timeout-minutes: 30
217 steps:
218 - uses: actions/checkout@v5
219 - uses: actions/download-artifact@v4
220 with:
221 pattern: binaries-*
222 merge-multiple: true
223 path: target/runner-release
224 - uses: actions/download-artifact@v4
225 if: ${{ needs.sandbox.result == 'success' }}
226 with:
227 name: sandbox
228 path: target/runner-release
229 - name: Sign
230 env:
231 RUNNER_RELEASE_KEY: ${{ secrets.RUNNER_RELEASE_KEY }}
232 G1T_RUNNER_RELEASE_KEY: ${{ vars.RUNNER_RELEASE_PUBLIC_KEY }}
233 RUNNER_AGENT_IMAGE: ${{ vars.RUNNER_AGENT_IMAGE }}
234 run: |
235 node scripts/runner-release.mjs manifest
236 node scripts/runner-release.mjs sign
237 node scripts/runner-release.mjs verify
238 # npm's downloads, kept between runs: an install reads them first
239 # and asks the registry only for what changed, so a dropped
240 # connection to it rarely stops the job.
241 - name: Cache npm's downloads
242 uses: actions/cache@v4
243 with:
244 path: ~/.npm
245 key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
246 restore-keys: npm-${{ runner.os }}-
247 - name: Install Wrangler
248 run: npm ci --prefer-offline --workspaces=false --no-audit --no-fund
249 - name: Publish to g1t.sh/downloads/runner
250 env:
251 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
252 run: node scripts/runner-release.mjs publish