| 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. |
| 19 | name: Runner release |
| 20 | |
| 21 | on: |
| 22 | push: |
| 23 | tags: ["runner-v*"] |
| 24 | workflow_dispatch: |
| 25 | |
| 26 | permissions: |
| 27 | contents: read |
| 28 | |
| 29 | concurrency: |
| 30 | group: runner-release |
| 31 | cancel-in-progress: false |
| 32 | |
| 33 | env: |
| 34 | CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} |
| 35 | WRANGLER_SEND_METRICS: "false" |
| 36 | |
| 37 | jobs: |
| 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 |