Skip to content
107 linesCodeBlameRaw
1# Rebuilds the base image of g1t's sandboxes (services/runner/base/Dockerfile:
2# the OS, toolchains and the Claude Code CLI), pushes it to Cloudflare's
3# registry, and opens a pull request that records it in
4# services/runner/base.json. Merging that pull request is what rolls it out:
5# the next deploy builds the runner's image on it, in seconds.
6#
7# Weekly, for security updates and new stable toolchains; when the base's
8# folder changes on main (a change that forgot to rebuild it); and by hand.
9#
10# It runs on a self-hosted runner with the `docker` label. g1t's own
11# machines have Docker now, but this build's own downloads (Docker's apt
12# repository over HTTPS) do not yet trust a guarded job's egress
13# certificate, so it stays where the network is open. Until a runner is
14# registered, run the same thing by hand on a machine with Docker:
15#
16# node scripts/deploy.mjs build-base
17#
18# and commit services/runner/base.json. "The runner's images" in
19# docs.g1t.sh/guides/deploy-to-cloudflare/ explains both.
20# Once a runner is registered, set the repository variable
21# RUNNER_BASE_SELF_HOSTED to "true" to turn this workflow's job on.
22name: Runner base image
23
24on:
25 schedule:
26 - cron: "17 6 * * 1"
27 push:
28 branches: [main]
29 paths:
30 - services/runner/base/**
31 workflow_dispatch:
32 inputs:
33 no_cache:
34 description: "Build every layer again, ignoring the previous base"
35 type: boolean
36 default: false
37
38# Its token pushes the branch with base.json and opens the pull request.
39# What a job's token does starts no workflows, so that pull request's
40# checks start when someone pushes to it or runs CI by hand.
41permissions:
42 contents: write
43 pull-requests: write
44
45concurrency:
46 group: runner-base
47 cancel-in-progress: false
48
49env:
50 CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
51 WRANGLER_SEND_METRICS: "false"
52
53jobs:
54 build:
55 name: Build and push the base
56 runs-on: [self-hosted, docker]
57 # Skipped until a runner with these labels is registered and the
58 # repository variable RUNNER_BASE_SELF_HOSTED is "true": without one,
59 # each run waited in the queue for a day and a half and then failed.
60 if: ${{ vars.RUNNER_BASE_SELF_HOSTED == 'true' }}
61 # Production's secrets, without a deployment: a release is not a deploy of
62 # g1t.sh, and would otherwise show on its card as one in progress.
63 environment:
64 name: production
65 deployment: false
66 timeout-minutes: 60
67 steps:
68 - uses: actions/checkout@v5
69 - name: Install Wrangler
70 run: npm ci --workspaces=false --no-audit --no-fund
71 - name: Build and push
72 env:
73 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
74 CI: "true"
75 NO_CACHE: ${{ inputs.no_cache }}
76 run: |
77 args=()
78 if [ "$NO_CACHE" = "true" ]; then args+=(--no-cache); fi
79 node scripts/deploy.mjs build-base "${args[@]}"
80 # The runner's own image on the new base, so the deploy after the
81 # merge finds it in the registry instead of building it.
82 - name: Build and push the runner's image on it
83 env:
84 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
85 CI: "true"
86 run: node scripts/deploy.mjs image
87 - name: Open a pull request with base.json
88 env:
89 G1T_TOKEN: ${{ github.token }}
90 REPO: ${{ github.repository }}
91 run: |
92 if git diff --quiet -- services/runner/base.json; then
93 echo "The base is unchanged."
94 exit 0
95 fi
96 branch="runner-base/$(date -u +%Y%m%d-%H%M)"
97 git config user.name "g1t"
98 git config user.email "g1t@users.noreply.g1t.sh"
99 git checkout -b "$branch"
100 git add services/runner/base.json
101 git commit -m "A new base image for g1t's sandboxes"
102 git push origin "$branch"
103 tag=$(node -e 'console.log(require("./services/runner/base.json").image.split(":").pop())')
104 body=$(node -e 'const b=require("./services/runner/base.json"); console.log("Built and pushed by the Runner base image workflow.\n\n| | |\n| --- | --- |\n" + Object.entries(b.versions).map(([k,v]) => `| ${k} | ${v} |`).join("\n") + `\n| size | ${(b.size_bytes/1e9).toFixed(2)} GB unpacked |`)')
105 curl -fsS -X POST "https://api.g1t.sh/repos/$REPO/pulls" \
106 -H "Authorization: Bearer $G1T_TOKEN" -H "Content-Type: application/json" \
107 -d "$(node -e 'console.log(JSON.stringify({ title: `Runner base image ${process.argv[1]}`, branch: process.argv[2], body: process.argv[3] }))' "$tag" "$branch" "$body")"