Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
209 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

The workspace cost alert compares only days that carry their value, not the days before it was kept1# syntax=docker/dockerfile:1.7
2#
3# g1t-runner-base: everything a g1t sandbox has apart from the g1t runner
4# itself. Agents, checks, the merge queue, workflow jobs and g1t.page
5# builds all run in it: git, Node, Python, Go, Rust (with the formatter,
Merge runner image: Java 21, .NET 8, Ruby 3.36# 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.
The workspace cost alert compares only days that carry their value, not the days before it was kept9#
10# Built and pushed by `node scripts/deploy.mjs build-base` (by hand, or by
11# .g1t/workflows/runner-base.yml), which records what it pushed in
12# services/runner/base.json. The runner's own image
13# (services/runner/Dockerfile) is this plus one file, so a change to the
14# runner builds in seconds. docs/DEPLOYING.md explains the two.
15#
16# Layers go from what changes least to what changes most: the system's
Merge runner image: Java 21, .NET 8, Ruby 3.317# 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.
The workspace cost alert compares only days that carry their value, not the days before it was kept26#
27# Build context: this folder (nothing is copied from it).
28
29FROM node:24-bookworm-slim
30
31# Docs, man pages and translations are never read in a sandbox; dpkg
32# leaves them out of every package installed from here on (copyright
33# files stay). Downloaded packages are kept for the build's apt cache.
34RUN printf '%s\n' \
35 'path-exclude=/usr/share/doc/*' \
36 'path-include=/usr/share/doc/*/copyright' \
37 'path-exclude=/usr/share/man/*' \
38 'path-exclude=/usr/share/info/*' \
39 'path-exclude=/usr/share/groff/*' \
40 'path-exclude=/usr/share/lintian/*' \
41 'path-exclude=/usr/share/linda/*' \
42 'path-exclude=/usr/share/locale/*' \
43 'path-include=/usr/share/locale/locale.alias' \
44 > /etc/dpkg/dpkg.cfg.d/01-g1t-slim \
45 && rm -f /etc/apt/apt.conf.d/docker-clean \
46 && echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-downloads
47
48# The system's packages. The apt cache and package lists live in
49# BuildKit's cache, not the image (run `sudo apt-get update` before
50# installing more). dpkg skips its fsync after every file, which an image
51# build has no use for and which made this step several times slower.
Merge branch 'main' into actions-toolkit-oidc-artifacts52# musl-tools is musl-gcc, with which (and `rustup target add
53# x86_64-unknown-linux-musl`) the static g1t runner builds in a workflow
54# job (scripts/build-runner.mjs).
The workspace cost alert compares only days that carry their value, not the days before it was kept55RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
56 --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
57 apt-get update \
58 && apt-get install -y --no-install-recommends -o Dpkg::Options::=--force-unsafe-io \
59 git ca-certificates curl build-essential pkg-config libssl-dev \
60 python3 python3-pip python3-venv ripgrep jq zstd \
61 sudo unzip zip xz-utils wget file gnupg lsb-release openssh-client \
Merge branch 'main' into actions-toolkit-oidc-artifacts62 musl-tools \
The workspace cost alert compares only days that carry their value, not the days before it was kept63 && rm -rf /var/log/apt /var/log/dpkg.log /var/cache/debconf/*-old
64
Merge branch 'main' into actions-toolkit-oidc-artifacts65# Docker, for workflow jobs: the CLI with Buildx and Compose, and the
66# Engine (dockerd, containerd, runc), which the runner starts as a job's
67# own the first time the job uses it (crates/runner/src/docker). From
68# Docker's own repository, its signing key checked against the fingerprint
69# Docker publishes. Nothing here runs until a job asks for it.
70ARG DOCKER_KEY_FINGERPRINT=9DC858229FC7DD38854AE2D88D81803C0EBFCD88
71RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
72 --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
73 install -m 0755 -d /etc/apt/keyrings \
74 && curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc \
75 && GNUPGHOME="$(mktemp -d)" gpg --show-keys --with-colons /etc/apt/keyrings/docker.asc \
76 | grep -q "^fpr:::::::::${DOCKER_KEY_FINGERPRINT}:" \
77 && echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" \
78 > /etc/apt/sources.list.d/docker.list \
79 && apt-get update \
80 && apt-get install -y --no-install-recommends -o Dpkg::Options::=--force-unsafe-io \
81 docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \
82 && rm -rf /var/log/apt /var/log/dpkg.log /var/cache/debconf/*-old \
83 && docker --version && dockerd --version && docker buildx version && docker compose version
84
The workspace cost alert compares only days that carry their value, not the days before it was kept85# Workflows expect GitHub's runner layout under /home/runner, and sudo
86# without a password. The agent works in /work.
87RUN mkdir /work && chown node:node /work \
88 && mkdir -p /home/runner/work /home/runner/_temp /home/runner/_tool /home/runner/_actions \
89 && chown -R node:node /home/runner \
90 && echo 'node ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/node \
91 && chmod 0440 /etc/sudoers.d/node
92
93# Go, from go.dev (Debian's is years behind), without its own test suite
94# and API history, which only Go's developers use.
95ARG GO_VERSION=1.27.1
96ARG GO_SHA256=63d339f0da5ab53635a56f2490a7984dfe12dfcff22ad749f63edaf590168445
97RUN curl -fsSLo /tmp/go.tgz "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" \
98 && echo "${GO_SHA256} /tmp/go.tgz" | sha256sum -c - \
99 && tar -C /usr/local -xzf /tmp/go.tgz \
100 && rm -rf /tmp/go.tgz /usr/local/go/test /usr/local/go/api /usr/local/go/doc \
101 && ln -s /usr/local/go/bin/go /usr/local/go/bin/gofmt /usr/local/bin/
102
103# Claude Code refuses to skip permission prompts as root.
104USER node
105ENV HOME=/home/node
106ENV PATH=/home/node/.cargo/bin:/home/node/go/bin:$PATH
107
108# Rust stable, for the user the agent runs as, with the formatter and
109# linter that CI so often checks with (an agent that cannot run them only
110# finds out from a failed workflow), and the wasm32 target that Workers,
111# and g1t's own deploys, build for.
112RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
113 | sh -s -- -y --no-modify-path --profile minimal --default-toolchain stable \
114 --component rustfmt --component clippy --target wasm32-unknown-unknown \
115 && rm -rf /home/node/.rustup/downloads /home/node/.rustup/tmp \
116 /home/node/.rustup/toolchains/*/share/doc \
117 /home/node/.rustup/toolchains/*/share/man
118
Merge runner image: Java 21, .NET 8, Ruby 3.3119USER 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).
124RUN --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.
135ARG TEMURIN_RELEASE=jdk-21.0.12.1+1
136ARG TEMURIN_TOOLCACHE_VERSION=21.0.12-101.0.LTS
137ARG TEMURIN_SHA256=ce79869e1307ed8ee1e2baa86a412b1eb5b75d10a01006d788a6f968bcfaee94
138RUN 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
148ENV JAVA_HOME=/home/runner/_tool/Java_Temurin-Hotspot_jdk/${TEMURIN_TOOLCACHE_VERSION}/x64
149ENV 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.
155ARG DOTNET_SDK_VERSION=8.0.425
156ARG DOTNET_SDK_SHA512=934b8060a7190e5909ad1fd0785db542f487b3bbf6cdd14826b02095fdd0d0394298b1634085eff302928fccc33f7c1a7253e9b87df555fc36fce819bcd2e798
157RUN 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
167ENV 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.
179ARG RUBY_VERSION=3.3.12
180ARG RUBY_SHA256=b06d63beae271933033e27f0a389bc582a009e7845357d44365c39de525a051b
181RUN 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"
193ENV PATH=${JAVA_HOME}/bin:/home/runner/_tool/Ruby/${RUBY_VERSION}/x64/bin:$PATH
194
195USER node
196
The workspace cost alert compares only days that carry their value, not the days before it was kept197# Commits are the g1t agent's; the harness does not sign them as its own.
198RUN mkdir -p /home/node/.claude \
199 && echo '{"includeCoAuthoredBy": false}' > /home/node/.claude/settings.json
200
201# The Claude Code CLI, pinned, on top: it changes most often of anything
202# here, and is one layer to rebuild and push.
203ARG CLAUDE_CODE_VERSION=2.1.291
204USER root
205# npm's cache stays in BuildKit's cache, out of the image.
206RUN --mount=type=cache,target=/tmp/npm-cache \
207 npm install --global --no-audit --no-fund --cache /tmp/npm-cache "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
208 && claude --version
209USER node

This file's history is long; its oldest lines are credited to the oldest commit read.