Skip to content
217 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 name: Build
40 runs-on: ubuntu-latest
41 timeout-minutes: 60
42 steps:
43 - uses: actions/checkout@v5
44 - name: The tag names this version
45 if: startsWith(github.ref, 'refs/tags/runner-v')
46 run: |
47 version="$(sed -n 's/^version = "\(.*\)"/\1/p' crates/runner/Cargo.toml | head -1)"
48 [ "runner-v$version" = "${GITHUB_REF_NAME}" ] || { echo "::error::The tag is ${GITHUB_REF_NAME}, but crates/runner is $version"; exit 1; }
49 - name: Install zig and cargo-zigbuild
50 run: |
51 # The runner's Debian marks its Python as externally managed (PEP 668); zig goes under ~/.local anyway.
52 pip install --user --break-system-packages ziglang==0.13.0
53 echo "$HOME/.local/bin" >> "$GITHUB_PATH"
54 cargo install --locked cargo-zigbuild
55 - uses: actions/cache@v4
56 with:
57 path: |
58 ~/.cargo/registry
59 target
60 key: runner-release-${{ hashFiles('Cargo.lock') }}
61 - name: Build Linux and Windows
62 # The macOS targets need Apple's SDK to link (the Security and
63 # CoreFoundation frameworks), which zig does not carry, so they are
64 # left out until this builds in the rust-cross/cargo-zigbuild image
65 # as the CLI's release does (scripts/cli-release.mjs). A Mac runner
66 # keeps the version it has, since the manifest names no file for it.
67 env:
68 G1T_RUNNER_RELEASE_KEY: ${{ vars.RUNNER_RELEASE_PUBLIC_KEY }}
69 RUNNER_AGENT_IMAGE: ${{ vars.RUNNER_AGENT_IMAGE }}
70 run: node scripts/runner-release.mjs build --targets linux-x64,linux-arm64,windows-x64
71 - uses: actions/upload-artifact@v4
72 with:
73 name: binaries
74 path: target/runner-release/
75
76 image:
77 name: Container image
78 needs: binaries
79 runs-on: g1t-4core
80 # Production's secrets, without a deployment: a release is not a deploy of
81 # g1t.sh, and would otherwise show on its card as one in progress.
82 environment:
83 name: production
84 deployment: false
85 timeout-minutes: 45
86 # Its token pushes the image to g1t's registry.
87 permissions:
88 contents: read
89 packages: write
90 outputs:
91 # The pushed image by digest, which the sandbox's manifest names.
92 runner_image: ${{ steps.push.outputs.runner_image }}
93 steps:
94 - uses: actions/checkout@v5
95 - uses: actions/download-artifact@v4
96 with:
97 name: binaries
98 path: release
99 - name: QEMU for arm64, if this machine allows it
100 id: qemu
101 run: |
102 if docker run --privileged --rm tonistiigi/binfmt --install arm64 >/dev/null 2>&1 \
103 && [ "$(docker run --rm --platform linux/arm64 debian:bookworm-slim uname -m 2>/dev/null)" = aarch64 ]; then
104 echo "arches=amd64 arm64" >> "$GITHUB_OUTPUT"
105 else
106 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."
107 echo "arches=amd64" >> "$GITHUB_OUTPUT"
108 fi
109 - name: Build and push
110 id: push
111 # To g1t's own registry, where flagon-io keeps it public.
112 env:
113 IMAGE: g1t.sh/flagon-io/g1t-runner
114 ARCHES: ${{ steps.qemu.outputs.arches }}
115 run: |
116 version="$(sed -n 's/^version = "\(.*\)"/\1/p' crates/runner/Cargo.toml | head -1)"
117 echo "${{ secrets.G1T_TOKEN }}" | docker login g1t.sh -u g1t --password-stdin
118 tags=()
119 for arch in $ARCHES; do
120 mkdir -p "context-$arch"
121 cp deploy/runner/Dockerfile "context-$arch/"
122 name="g1t-runner-linux-$([ "$arch" = amd64 ] && echo x64 || echo arm64)"
123 cp release/*/"$name" "context-$arch/g1t-runner"
124 docker buildx build --platform "linux/$arch" -t "$IMAGE:$version-$arch" --load "context-$arch"
125 docker push "$IMAGE:$version-$arch"
126 tags+=("$IMAGE:$version-$arch")
127 done
128 docker buildx imagetools create -t "$IMAGE:$version" -t "$IMAGE:latest" "${tags[@]}"
129 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))')"
130 echo "runner_image=$IMAGE@$digest" >> "$GITHUB_OUTPUT"
131
132 sandbox:
133 name: Sandbox images
134 needs: [binaries, image]
135 runs-on: g1t-4core
136 timeout-minutes: 60
137 steps:
138 - uses: actions/checkout@v5
139 - uses: actions/download-artifact@v4
140 with:
141 name: binaries
142 path: target/runner-release
143 - name: QEMU for arm64, if this machine allows it
144 id: qemu
145 run: |
146 if docker run --privileged --rm tonistiigi/binfmt --install arm64 >/dev/null 2>&1 \
147 && [ "$(docker run --rm --platform linux/arm64 debian:bookworm-slim uname -m 2>/dev/null)" = aarch64 ]; then
148 echo "arches=x64 arm64" >> "$GITHUB_OUTPUT"
149 else
150 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."
151 echo "arches=x64" >> "$GITHUB_OUTPUT"
152 fi
153 - name: Build the WSL root filesystem and the VM's disk
154 env:
155 ARCHES: ${{ steps.qemu.outputs.arches }}
156 run: |
157 dir="$(echo target/runner-release/*/)"
158 for arch in $ARCHES; do
159 node scripts/sandbox-image.mjs build --arch "$arch" --runner "${dir}g1t-runner-linux-$arch" --out "$dir"
160 done
161 - name: The manifest's sandbox section
162 env:
163 RUNNER_SANDBOX_IMAGE: ${{ needs.image.outputs.runner_image }}
164 run: node scripts/sandbox-image.mjs section --dir "$(echo target/runner-release/*/)"
165 - uses: actions/upload-artifact@v4
166 with:
167 name: sandbox
168 path: |
169 target/runner-release/*/g1t-sandbox-*
170 target/runner-release/*/sandbox.json
171
172 publish:
173 name: Sign and publish
174 needs: [binaries, image, sandbox]
175 # The binaries go out even when an image job failed: the manifest then
176 # names no sandbox images, and computers keep the sandbox they have.
177 if: ${{ always() && needs.binaries.result == 'success' }}
178 runs-on: ubuntu-latest
179 environment:
180 name: production
181 deployment: false
182 timeout-minutes: 30
183 steps:
184 - uses: actions/checkout@v5
185 - uses: actions/download-artifact@v4
186 with:
187 name: binaries
188 path: target/runner-release
189 - uses: actions/download-artifact@v4
190 if: ${{ needs.sandbox.result == 'success' }}
191 with:
192 name: sandbox
193 path: target/runner-release
194 - name: Sign
195 env:
196 RUNNER_RELEASE_KEY: ${{ secrets.RUNNER_RELEASE_KEY }}
197 G1T_RUNNER_RELEASE_KEY: ${{ vars.RUNNER_RELEASE_PUBLIC_KEY }}
198 RUNNER_AGENT_IMAGE: ${{ vars.RUNNER_AGENT_IMAGE }}
199 run: |
200 node scripts/runner-release.mjs manifest
201 node scripts/runner-release.mjs sign
202 node scripts/runner-release.mjs verify
203 # npm's downloads, kept between runs: an install reads them first
204 # and asks the registry only for what changed, so a dropped
205 # connection to it rarely stops the job.
206 - name: Cache npm's downloads
207 uses: actions/cache@v4
208 with:
209 path: ~/.npm
210 key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
211 restore-keys: npm-${{ runner.os }}-
212 - name: Install Wrangler
213 run: npm ci --prefer-offline --workspaces=false --no-audit --no-fund
214 - name: Publish to g1t.sh/downloads/runner
215 env:
216 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
217 run: node scripts/runner-release.mjs publish