Skip to content

Commit

Merge runner image: Java 21, .NET 8, Ruby 3.3

syntaqxcommitted Parents8ede70eb7281b5Browse files
7 files+239−180/7 viewed
+24−0
229229 macOS and Windows, on x64 and arm64.
230230 - **Status.** Not scheduled.
231231
232+### No `gh` command in jobs
233+
234+The runner does not include the `gh` command, and pointing it at g1t
235+(`GH_HOST=g1t.sh`) does not work.
236+
237+- **Why.** Most of `gh`'s commands use a GraphQL API, and the rest expect
238+ the REST API under `/api/v3` on the same host. g1t's API is REST, at
239+ `api.g1t.sh`.
240+- **Instead.** Call the API with `curl` and the job's token. See
241+ [calling g1t's API from a job](/guides/actions/#calling-g1ts-api-from-a-job).
242+- **Status.** Not scheduled.
243+
244+### One Ruby for `ruby/setup-ruby`
245+
246+On g1t's machines, `ruby/setup-ruby` finds Ruby 3.3, which the runner
247+includes, and fails for any other version.
248+
249+- **Why.** Its prebuilt Rubies are for other Linux systems, so on Debian it
250+ uses only the Rubies already in `RUNNER_TOOL_CACHE`.
251+- **Instead.** Use 3.3, run the job in a `container:` with the Ruby you
252+ need (such as `ruby:3.4`), or build it in a step with `ruby-build`. See
253+ [languages and their setup actions](/guides/actions/#languages-and-their-setup-actions).
254+- **Status.** Not scheduled.
255+
232256 ### Machine sizes, time and storage
233257
234258 | Limit | Value |
+58−3
189189 ## The runner
190190
191191 Jobs run in a fresh sandbox each: Debian with Node 24, Python 3, Go, Rust,
192−`build-essential`, `git`, `curl`, `jq`, Docker (with Buildx and Compose)
193−and passwordless `sudo`, in GitHub's layout (`/home/runner/work`,
194−`RUNNER_TEMP`, `RUNNER_TOOL_CACHE`).
192+Java 21, .NET 8, Ruby 3.3, `build-essential`, `git`, `curl`, `jq`, Docker
193+(with Buildx and Compose) and passwordless `sudo`, in GitHub's layout
194+(`/home/runner/work`, `RUNNER_TEMP`, `RUNNER_TOOL_CACHE`).
195195 `runner.os` is `Linux`. `ubuntu-latest`, `ubuntu-24.04` and other Linux
196196 labels all run here. A job whose `runs-on` names `self-hosted` waits for one
197197 of your [self-hosted runners](/guides/self-hosted-runners/) instead. Setup actions such as
198198 `actions/setup-node` and `actions/setup-python` install other versions as
199199 they do on GitHub.
200200
201+### Languages and their setup actions
202+
203+Each language below is on `PATH` from the job's first step, so a workflow
204+that only runs `java`, `dotnet` or `ruby` needs no setup step. When it has
205+one, the setup action finds the version that is already there and
206+downloads nothing.
207+
208+| Language | Version | Where | Setup action |
209+| --- | --- | --- | --- |
210+| Java | Eclipse Temurin 21 (LTS), JDK | `JAVA_HOME` (also `JAVA_HOME_21_X64`), in `RUNNER_TOOL_CACHE` | `actions/setup-java` with `distribution: temurin` and `java-version: 21` uses it. Other versions and distributions are downloaded. |
211+| .NET | SDK 8 (LTS) | `DOTNET_ROOT`, `/usr/share/dotnet` | `actions/setup-dotnet` with `dotnet-version: 8.0.x` keeps it when it is the newest 8.0 SDK, and installs other SDKs beside it. |
212+| Ruby | 3.3, with Bundler | in `RUNNER_TOOL_CACHE` | `ruby/setup-ruby` with `ruby-version: '3.3'` (or a `.ruby-version` naming 3.3) uses it. |
213+| Node | 24 | `/usr/local/bin` | `actions/setup-node` installs other versions. |
214+| Python | 3.11 | `/usr/bin/python3` | `actions/setup-python` installs other versions. |
215+| Go | 1.27 | `/usr/local/go` | `actions/setup-go` installs other versions. |
216+| Rust | stable, with `rustfmt`, `clippy` and the `wasm32-unknown-unknown` target | `~/.cargo/bin` | `rustup` is there to add toolchains and targets. |
217+
218+```yaml
219+steps:
220+ - uses: actions/checkout@v5
221+ - uses: actions/setup-java@v5
222+ with:
223+ distribution: temurin
224+ java-version: 21
225+ - run: ./gradlew build
226+```
227+
228+Because the sandbox runs Debian, `ruby/setup-ruby` treats it as a
229+self-hosted runner and uses only the Rubies in `RUNNER_TOOL_CACHE`. A version other than 3.3 fails at that step; install
230+it in a `run` step instead (for example with `ruby-build`) or run the job
231+in a `container:` with the Ruby you need, such as `ruby:3.4`.
232+
233+The headers that gems and .NET need to build native code (`libyaml`,
234+`libffi`, `zlib`, OpenSSL, ICU) are installed too.
235+
236+### Calling g1t's API from a job
237+
238+The `gh` command is not installed: it needs a GraphQL API, and g1t's API
239+is REST. Call it with `curl`, using the job's token and the API's address,
240+which every job has as `GITHUB_API_URL`:
241+
242+```yaml
243+- name: Comment on the pull request
244+ env:
245+ TOKEN: ${{ github.token }}
246+ run: |
247+ curl -fsS -X POST "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/issues/${{ github.event.number }}/comments" \
248+ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
249+ -d '{"body": "Built."}'
250+```
251+
252+The token reaches this repository and does what the job's `permissions:`
253+say; see [the job's token](#the-jobs-token). The
254+[API reference](/reference/api/) lists every endpoint.
255+
201256 ### Machine sizes
202257
203258 A job runs on the standard machine unless its `runs-on` names a larger
+17−5
221221 builds the runner's image on it. `npm run test:deploy` fails while the
222222 folder and the file disagree, so a change to the base's Dockerfile cannot
223223 merge without the base it describes. Layers go from what changes least to
224−most (system packages, Go, Rust, the Claude Code CLI), and the apt and npm
225−caches stay in BuildKit's cache, out of the image.
224+most (system packages, Go, Rust, Java, .NET and Ruby, the Claude Code
225+CLI), and the apt and npm caches stay in BuildKit's cache, out of the
226+image. Ruby is compiled from source in a single step (a few minutes on a
227+cold cache) that removes its source tree before the layer is written.
226228
227229 The base's build cache is the base itself: it is built with
228230 `BUILDKIT_INLINE_CACHE`, which records in the image how each layer was
444446 ### What the sandbox has
445447
446448 The base image (`services/runner/base/Dockerfile`) has Node 24, npm, git,
447−Go, zstd, Docker, musl-tools, and Rust stable for the `node` user with
448−rustfmt, clippy and the `wasm32-unknown-unknown` target, but not
449−worker-build. The workflow's `rustup target add wasm32-unknown-unknown` is
449+Go, zstd, Docker, musl-tools, Rust stable for the `node` user with
450+rustfmt, clippy and the `wasm32-unknown-unknown` target, Java (Temurin 21),
451+the .NET 8 SDK and Ruby 3.3, but not worker-build or `gh`. Java and Ruby
452+sit in the tool cache (`/home/runner/_tool/Java_Temurin-Hotspot_jdk/<semver
453+with + as ->/x64` and `/home/runner/_tool/Ruby/<version>/x64`, each with
454+an `x64.complete` marker), which is where `actions/setup-java` and
455+`ruby/setup-ruby` look; .NET is in `/usr/share/dotnet`, owned by `node`,
456+which is where `actions/setup-dotnet` installs. Bumping one means changing
457+its version and checksum `ARG`s together (Java's tool cache name is
458+Adoptium's `version_data.semver`; .NET's SHA-512 is in its release
459+metadata; Ruby's SHA-256 is in `cache.ruby-lang.org/pub/ruby/index.txt`).
460+The user docs list what jobs get in
461+`apps/docs/src/content/docs/guides/actions.md` (The runner). The workflow's `rustup target add wasm32-unknown-unknown` is
450462 then a no-op, and worker-build is restored from the cache, installed on a
451463 miss. The image job adds `x86_64-unknown-linux-musl` (about 30 MB from
452464 `static.rust-lang.org`) and keeps its Cargo target in the cache. worker-build
+38−0
813813 - Not yet: re-running one combination of a matrix alone; signals into a
814814 job container's processes on cancel (they reach `docker exec` only).
815815
816+### Runner image: Java, .NET and Ruby (built 2026-10-08)
817+
818+The base (`services/runner/base/Dockerfile`) adds Temurin 21, the .NET 8
819+SDK and Ruby 3.3, placed where the setup actions look so common workflows
820+run unmodified and download nothing:
821+
822+- **Java** in `RUNNER_TOOL_CACHE/Java_Temurin-Hotspot_jdk/<semver, + as
823+ ->/x64` with `x64.complete`; `JAVA_HOME`, `JAVA_HOME_21_X64` and `PATH`
824+ set. `actions/setup-java` (`temurin`, `21`) resolves it from the cache.
825+- **.NET** in `/usr/share/dotnet` (setup-dotnet's Linux install dir),
826+ owned by `node`; `DOTNET_ROOT` set, telemetry off. `setup-dotnet` with
827+ `8.0.x` keeps it while it is the newest 8.0 SDK, else installs beside it.
828+- **Ruby** built from source into `RUNNER_TOOL_CACHE/Ruby/<v>/x64` with
829+ `x64.complete`, on `PATH`. On Debian, `ruby/setup-ruby` counts as
830+ self-hosted and uses only the tool cache, so any version but 3.3 fails
831+ (documented in limitations).
832+
833+**`gh`: not installed.** With `GH_HOST=g1t.sh`, `gh` treats g1t as
834+GitHub Enterprise Server: REST under `https://g1t.sh/api/v3` and GraphQL at
835+`https://g1t.sh/api/graphql`. Both are 404 today (the API is REST at
836+`api.g1t.sh`, and `GITHUB_GRAPHQL_URL` is empty), and `pr`, `issue`, `repo`
837+and `run` are GraphQL-first, so even `gh api` alone would need the
838+`/api/v3` prefix. Jobs call the API with `curl` and `$GITHUB_API_URL`
839+(documented in the Actions guide).
840+
841+**Later:** to make `gh` useful, `g1t.sh/api/v3/*` proxying to the REST API
842+(enough for `gh api` and `gh auth status`), then a GraphQL subset for the
843+`pr`/`issue` read paths. Ship `gh` in the base only once those exist.
844+More preinstalled Rubies (3.2, 3.4) if workflows ask, at roughly 75 MB
845+each. `setup-dotnet@v5` first installs the current LTS .NET runtime (10,
846+a 36 MB download) every run before finding SDK 8 already there;
847+preinstalling that runtime would skip it.
848+
849+**Size:** the base went from 3.18 GB to 4.52 GB as `docker image inspect`
850+reports it (813 MB to about 1.2 GB compressed): Temurin 308 MB (without
851+`src.zip`), .NET 512 MB (without the SDK's translations), Ruby 74 MB,
852+headers and ICU 44 MB.
853+
816854 ## A repository that maintains itself
817855
818856 > **2026-10-04:** the user asked for Dependabot, GitHub Advanced Security and
+3−0
213213 'echo "python=$(python3 --version | cut -d" " -f2)"',
214214 'echo "go=$(go version | cut -d" " -f3)"',
215215 'echo "rust=$(rustc --version | cut -d" " -f2)"',
216+ 'echo "java=$(java --version | head -n1 | cut -d" " -f2)"',
217+ 'echo "dotnet=$(dotnet --version)"',
218+ 'echo "ruby=$(ruby --version | cut -d" " -f2)"',
216219 'echo "git=$(git --version | cut -d" " -f3)"',
217220 'echo "claude_code=$(claude --version | cut -d" " -f1)"',
218221 'echo "docker=$(dockerd --version | cut -d" " -f3 | tr -d ,)"',
+9−6
11 {
2− "image": "registry.cloudflare.com/1e6f2cffa3f445920836e8ebe446bb58/g1t-runner:base-20261008-072528b93a0f",
3− "digest": "sha256:4353ff9d0c113297d487e807b7b4c5d87a475f222345fbf5eb097271553311f5",
4− "pushed": true,
5− "inputs": "072528b93a0f6468ab876770e0f4a4a9b10969e6fc49b4cbe9264e1824244914",
6− "built_at": "2026-10-08T08:13:38.448Z",
7− "size_bytes": 3175486075,
2+ "image": "registry.cloudflare.com/1e6f2cffa3f445920836e8ebe446bb58/g1t-runner:base-20261008-a314499af03b",
3+ "digest": null,
4+ "pushed": false,
5+ "inputs": "a314499af03bda6a12f906332827ba1b2c23ea781c32e69599050762e5c2ce05",
6+ "built_at": "2026-10-08T19:12:40.565Z",
7+ "size_bytes": 4521302378,
88 "versions": {
99 "node": "v24.21.0",
1010 "npm": "11.19.0",
1111 "python": "3.11.2",
1212 "go": "go1.27.1",
1313 "rust": "1.99.0",
14+ "java": "21.0.12.1",
15+ "dotnet": "8.0.425",
16+ "ruby": "3.3.12",
1417 "git": "2.39.5",
1518 "claude_code": "2.1.291",
1619 "docker": "29.8.2",
+90−4
33 # g1t-runner-base: everything a g1t sandbox has apart from the g1t runner
44 # itself. Agents, checks, the merge queue, workflow jobs and g1t.page
55 # builds all run in it: git, Node, Python, Go, Rust (with the formatter,
6−# the linter, and the wasm32 and musl targets), the usual build tools,
7−# Docker (Engine, Buildx, Compose), and the Claude Code CLI.
6+# the linter, and the wasm32 and musl targets), Java (Temurin 21), .NET
7+# (SDK 8), Ruby (3.3), the usual build tools, Docker (Engine, Buildx,
8+# Compose), and the Claude Code CLI.
89 #
910 # Built and pushed by `node scripts/deploy.mjs build-base` (by hand, or by
1011 # .g1t/workflows/runner-base.yml), which records what it pushed in
1314 # runner builds in seconds. docs/DEPLOYING.md explains the two.
1415 #
1516 # Layers go from what changes least to what changes most: the system's
16−# packages, Docker, then Go, then Rust, then the Claude Code CLI on top, so
17−# a new CLI version rebuilds and pushes one layer.
17+# packages, Docker, then Go, then Rust, then Java, .NET and Ruby, then the
18+# Claude Code CLI on top, so a new CLI version rebuilds and pushes one
19+# layer.
20+#
21+# Java and Ruby live in the runner's tool cache (RUNNER_TOOL_CACHE,
22+# /home/runner/_tool), laid out as the setup actions look for them, so
23+# `actions/setup-java` (temurin, 21) and `ruby/setup-ruby` (3.3) find them
24+# there instead of downloading. .NET is in /usr/share/dotnet, where
25+# `actions/setup-dotnet` installs, so it finds the SDK already there.
1826 #
1927 # Build context: this folder (nothing is copied from it).
2028
108116 /home/node/.rustup/toolchains/*/share/doc \
109117 /home/node/.rustup/toolchains/*/share/man
110118
119+USER root
120+
121+# What Java, .NET and Ruby need from the system: ICU for .NET, and the
122+# headers that gems with native extensions compile against (Ruby's own
123+# were built with them).
124+RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
125+ --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
126+ apt-get update \
127+ && apt-get install -y --no-install-recommends -o Dpkg::Options::=--force-unsafe-io \
128+ libicu72 libyaml-dev libffi-dev zlib1g-dev libgmp-dev libreadline-dev \
129+ && rm -rf /var/log/apt /var/log/dpkg.log /var/cache/debconf/*-old
130+
131+# Java: Eclipse Temurin 21 (LTS), from Adoptium, in the tool cache under
132+# the name `actions/setup-java` gives it (Adoptium's semver with `+`
133+# written as `-`), with the `x64.complete` marker it checks for. The JDK's
134+# source archive is left out.
135+ARG TEMURIN_RELEASE=jdk-21.0.12.1+1
136+ARG TEMURIN_TOOLCACHE_VERSION=21.0.12-101.0.LTS
137+ARG TEMURIN_SHA256=ce79869e1307ed8ee1e2baa86a412b1eb5b75d10a01006d788a6f968bcfaee94
138+RUN file="$(echo "${TEMURIN_RELEASE#jdk-}" | tr + _)" \
139+ && tag="$(echo "${TEMURIN_RELEASE}" | sed 's/+/%2B/')" \
140+ && curl -fsSLo /tmp/jdk.tgz "https://github.com/adoptium/temurin21-binaries/releases/download/${tag}/OpenJDK21U-jdk_x64_linux_hotspot_${file}.tar.gz" \
141+ && echo "${TEMURIN_SHA256} /tmp/jdk.tgz" | sha256sum -c - \
142+ && dir="/home/runner/_tool/Java_Temurin-Hotspot_jdk/${TEMURIN_TOOLCACHE_VERSION}" \
143+ && mkdir -p "$dir/x64" \
144+ && tar -C "$dir/x64" --strip-components=1 -xzf /tmp/jdk.tgz \
145+ && rm -f /tmp/jdk.tgz "$dir/x64/lib/src.zip" \
146+ && touch "$dir/x64.complete" \
147+ && chown -R node:node /home/runner/_tool
148+ENV JAVA_HOME=/home/runner/_tool/Java_Temurin-Hotspot_jdk/${TEMURIN_TOOLCACHE_VERSION}/x64
149+ENV JAVA_HOME_21_X64=${JAVA_HOME}
150+
151+# .NET: the SDK 8 (LTS), from Microsoft's builds, checked against the
152+# SHA-512 in its release metadata. In /usr/share/dotnet, owned by the
153+# job's user, so `actions/setup-dotnet` can add other SDKs beside it. The
154+# SDK's translated messages (about 100 MB) are left out: it speaks English.
155+ARG DOTNET_SDK_VERSION=8.0.425
156+ARG DOTNET_SDK_SHA512=934b8060a7190e5909ad1fd0785db542f487b3bbf6cdd14826b02095fdd0d0394298b1634085eff302928fccc33f7c1a7253e9b87df555fc36fce819bcd2e798
157+RUN curl -fsSLo /tmp/dotnet.tgz "https://builds.dotnet.microsoft.com/dotnet/Sdk/${DOTNET_SDK_VERSION}/dotnet-sdk-${DOTNET_SDK_VERSION}-linux-x64.tar.gz" \
158+ && echo "${DOTNET_SDK_SHA512} /tmp/dotnet.tgz" | sha512sum -c - \
159+ && mkdir -p /usr/share/dotnet \
160+ && tar -C /usr/share/dotnet -xzf /tmp/dotnet.tgz \
161+ && rm /tmp/dotnet.tgz \
162+ && find /usr/share/dotnet/sdk -type d \( -name cs -o -name de -o -name es -o -name fr -o -name it \
163+ -o -name ja -o -name ko -o -name pl -o -name pt-BR -o -name ru -o -name tr -o -name zh-Hans -o -name zh-Hant \) \
164+ -prune -exec rm -rf {} + \
165+ && chown -R node:node /usr/share/dotnet \
166+ && ln -s /usr/share/dotnet/dotnet /usr/local/bin/dotnet
167+ENV DOTNET_ROOT=/usr/share/dotnet \
168+ DOTNET_NOLOGO=1 \
169+ DOTNET_CLI_TELEMETRY_OPTOUT=1 \
170+ DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 \
171+ DOTNET_MULTILEVEL_LOOKUP=0
172+
173+# Ruby 3.3, built from ruby-lang.org's source in the tool cache, where
174+# `ruby/setup-ruby` looks on a Debian machine (its prebuilt Rubies are for
175+# other systems), with the `x64.complete` marker it checks for. The prefix
176+# is fixed when Ruby is built, so it is built in place. One step, so the
177+# source and objects never reach a layer, and the previous base's inline
178+# cache covers it.
179+ARG RUBY_VERSION=3.3.12
180+ARG RUBY_SHA256=b06d63beae271933033e27f0a389bc582a009e7845357d44365c39de525a051b
181+RUN prefix="/home/runner/_tool/Ruby/${RUBY_VERSION}/x64" \
182+ && curl -fsSLo /tmp/ruby.tgz "https://cache.ruby-lang.org/pub/ruby/${RUBY_VERSION%.*}/ruby-${RUBY_VERSION}.tar.gz" \
183+ && echo "${RUBY_SHA256} /tmp/ruby.tgz" | sha256sum -c - \
184+ && mkdir /tmp/ruby && tar -C /tmp/ruby --strip-components=1 -xzf /tmp/ruby.tgz \
185+ && cd /tmp/ruby \
186+ && ./configure --prefix="$prefix" --enable-shared --disable-install-doc \
187+ && make -j"$(nproc)" \
188+ && make install \
189+ && cd / && rm -rf /tmp/ruby /tmp/ruby.tgz \
190+ && "$prefix/bin/ruby" -ropenssl -rpsych -rzlib -rfiddle -e 'puts RUBY_DESCRIPTION' \
191+ && touch "$prefix.complete" \
192+ && chown -R node:node "/home/runner/_tool/Ruby"
193+ENV PATH=${JAVA_HOME}/bin:/home/runner/_tool/Ruby/${RUBY_VERSION}/x64/bin:$PATH
194+
195+USER node
196+
111197 # Commits are the g1t agent's; the harness does not sign them as its own.
112198 RUN mkdir -p /home/node/.claude \
113199 && echo '{"includeCoAuthoredBy": false}' > /home/node/.claude/settings.json