mirror of
https://github.com/hanzoai/ci.git
synced 2026-10-20 15:28:05 +00:00
A forge runner lives one job and began each with an empty act cache, and act clones an action with go-git, full history, before the first step: forge run 16634 spent 156 of each job's 161 s of setup cloning setup-node (72 s), setup-go (42 s), setup-bun (20 s) and checkout (17 s), the three it then skipped included. The image now clones each into ~/.cache/act/<sha256 of the uses string> with every ref the remote advertises, so the runner opens it and fetches what moved since the build. Timed with act's own clone executor: 0.2 to 1.1 s each, against 2 to 90 s cold; with the branches and tags alone it still fetched every pull request's objects, 10 to 40 s each. It adds some 340 MB to the image. hanzo.yml's runner-preseeds fails when build.yml names an action the image does not carry.
226 lines
12 KiB
Docker
226 lines
12 KiB
Docker
# The runner image — our ubuntu-latest.
|
|
#
|
|
# `runs-on: ubuntu-latest` has to mean OUR machine, or it means GitHub's. There
|
|
# is no third option: the label is resolved by whoever owns the pool, so the way
|
|
# to stop renting builders is to serve the label ourselves.
|
|
#
|
|
# It runs under the `host` executor inside the visor sandbox, which is why the
|
|
# toolchain lives HERE rather than in a per-job container: the sandbox is the
|
|
# boundary, and a nested runtime would only be a second, weaker one. So this
|
|
# image is what a job sees, and it carries what a job actually reaches for --
|
|
# git, build essentials, node, python, docker CLI, and the `hanzo` CLI so a
|
|
# workflow can talk to the cloud it is being built for.
|
|
#
|
|
# Nothing can be installed at job time. visor ignores the setuid bit unless the
|
|
# node runs it with --allow-suid, which it does not, so `sudo` starts as the
|
|
# runner and answers "effective uid is not 0, is /usr/bin/sudo on a file system
|
|
# with the 'nosuid' option set" -- the message guesses the cause, and the guess
|
|
# is wrong. Whatever a job needs has to be in this file.
|
|
FROM ubuntu:24.04
|
|
|
|
# The build door pushes with its own credential, so ghcr links the package to
|
|
# no repository unless the image names one. Linked to this repo, the package is
|
|
# readable by this repo's workflow token, which is how bin/imgver reads the
|
|
# published floor and never writes one version over two digests.
|
|
LABEL org.opencontainers.image.source=https://github.com/hanzoai/ci
|
|
|
|
ENV DEBIAN_FRONTEND=noninteractive \
|
|
LANG=C.UTF-8 \
|
|
RUNNER_TOOL_CACHE=/opt/hostedtoolcache \
|
|
COREPACK_HOME=/opt/corepack
|
|
|
|
# The toolchain the reusable build asks for at job time. A job cannot install
|
|
# it: `sudo apt-get` answers "effective uid is not 0" (see the top of this file)
|
|
# and the step exits 1 — which is how every repo with a `client:` lane went red. build.yml already guards each of those
|
|
# steps with `command -v javac`, `pkg-config --exists openssl` and a libclang
|
|
# lookup, so carrying the packages here makes all of them no-ops.
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
ca-certificates curl wget git jq unzip zip xz-utils gnupg \
|
|
build-essential pkg-config libssl-dev libclang-dev \
|
|
openjdk-21-jdk-headless \
|
|
python3 python3-pip python3-venv \
|
|
openssh-client rsync sudo tini procps zstd \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# The OTHER architecture's C toolchain. gVisor builds the VDSO for both
|
|
# architectures in one bazel invocation, so building it on amd64 needs
|
|
# aarch64-linux-gnu-gcc and on arm64 needs x86_64-linux-gnu-gcc — and the
|
|
# visor publish lane failed on exactly that, with no way to add it at job time.
|
|
RUN set -eux; \
|
|
case "$(dpkg --print-architecture)" in \
|
|
amd64) pkgs="gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libc6-dev-arm64-cross" ;; \
|
|
arm64) pkgs="gcc-x86-64-linux-gnu g++-x86-64-linux-gnu libc6-dev-amd64-cross" ;; \
|
|
*) echo "unsupported $(dpkg --print-architecture)" >&2; exit 1 ;; \
|
|
esac; \
|
|
apt-get update && apt-get install -y --no-install-recommends $pkgs \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Node and Go live in the tool cache, laid out exactly as actions/setup-node and
|
|
# actions/setup-go lay out what they download: <tool>/<version>/<arch> beside an
|
|
# <arch>.complete marker. So `setup-node: 22` and a go.mod naming 1.27.1 find
|
|
# them there and fetch nothing; before, every job downloaded both, 110 runner
|
|
# minutes in 48 hours. A job that asks for another version still downloads it.
|
|
# The act runner reads its tool cache from ~/.cache/act/tool_cache, which is
|
|
# linked here further down, so both runners see the same one.
|
|
ARG NODE_VERSION=22.23.3
|
|
ARG NODE_SHA256_AMD64=df450af89261115ef9f9e3830c3eeb2cc9213b63c720b1af623cb5dcbe2e02de
|
|
ARG NODE_SHA256_ARM64=a44aeb94849a299b22df10b9e622ec2f605c2183501bc40590705131de7c740f
|
|
RUN set -eux; \
|
|
case "$(dpkg --print-architecture)" in \
|
|
amd64) a=x64; sum="$NODE_SHA256_AMD64" ;; \
|
|
arm64) a=arm64; sum="$NODE_SHA256_ARM64" ;; \
|
|
*) echo "unsupported $(dpkg --print-architecture)" >&2; exit 1 ;; \
|
|
esac; \
|
|
d="$RUNNER_TOOL_CACHE/node/$NODE_VERSION/$a"; mkdir -p "$d"; \
|
|
curl -fsSL -o /tmp/node.tar.xz "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-$a.tar.xz"; \
|
|
echo "$sum /tmp/node.tar.xz" | sha256sum -c -; \
|
|
tar -xJf /tmp/node.tar.xz -C "$d" --strip-components=1; rm /tmp/node.tar.xz; \
|
|
touch "$d.complete"; ln -s "$d" /opt/node
|
|
ENV PATH=/opt/node/bin:$PATH
|
|
|
|
# pnpm and yarn through corepack, which reads the version a repo pins in
|
|
# packageManager. The pnpm versions the fleet pins are fetched once here, so a
|
|
# job's `corepack enable` finds them; 10.x is the default for a repo that pins
|
|
# none, as the global `pnpm@10` it replaces was.
|
|
RUN set -eux; \
|
|
corepack enable; \
|
|
COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack install -g [email protected]; \
|
|
COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack install -g [email protected]; \
|
|
COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack install -g [email protected]; \
|
|
pnpm --version; yarn --version
|
|
|
|
ARG GO_VERSION=1.27.1
|
|
ARG GO_SHA256_AMD64=63d339f0da5ab53635a56f2490a7984dfe12dfcff22ad749f63edaf590168445
|
|
ARG GO_SHA256_ARM64=3450b45a3f9ee8568792736a5c5e70a1f2e9b36c35a8f74958c03e51d7d92bec
|
|
RUN set -eux; \
|
|
case "$(dpkg --print-architecture)" in \
|
|
amd64) a=x64; g=amd64; sum="$GO_SHA256_AMD64" ;; \
|
|
arm64) a=arm64; g=arm64; sum="$GO_SHA256_ARM64" ;; \
|
|
*) echo "unsupported $(dpkg --print-architecture)" >&2; exit 1 ;; \
|
|
esac; \
|
|
d="$RUNNER_TOOL_CACHE/go/$GO_VERSION/$a"; mkdir -p "$d"; \
|
|
curl -fsSL -o /tmp/go.tgz "https://go.dev/dl/go$GO_VERSION.linux-$g.tar.gz"; \
|
|
echo "$sum /tmp/go.tgz" | sha256sum -c -; \
|
|
tar -xzf /tmp/go.tgz -C "$d" --strip-components=1; rm /tmp/go.tgz; \
|
|
touch "$d.complete"; ln -s "$d" /usr/local/go
|
|
ENV PATH=/usr/local/go/bin:$PATH
|
|
|
|
# helm, which the universe deploy lanes call. Their install script copies the
|
|
# binary into /usr/local/bin with sudo, which cannot work here; with helm already
|
|
# at the version it would install, it has nothing to do.
|
|
ARG HELM_VERSION=v3.22.0
|
|
ARG HELM_SHA256_AMD64=1e4ab49e429626cf6c6958d914248b78c9730803c2751b87627e171dc800e7bb
|
|
ARG HELM_SHA256_ARM64=f14e804dfee240f55525b667488fe9adca349e63e00c9af634c0beb1421ac310
|
|
RUN set -eux; \
|
|
g="$(dpkg --print-architecture)"; \
|
|
case "$g" in amd64) sum="$HELM_SHA256_AMD64" ;; arm64) sum="$HELM_SHA256_ARM64" ;; *) exit 1 ;; esac; \
|
|
curl -fsSL -o /tmp/helm.tgz "https://get.helm.sh/helm-$HELM_VERSION-linux-$g.tar.gz"; \
|
|
echo "$sum /tmp/helm.tgz" | sha256sum -c -; \
|
|
tar -xzf /tmp/helm.tgz -C /tmp "linux-$g/helm"; \
|
|
install -m0755 "/tmp/linux-$g/helm" /usr/local/bin/helm; rm -rf /tmp/helm.tgz "/tmp/linux-$g"; \
|
|
helm version --short
|
|
|
|
# crane, which the delegate lane uses to read back and tag what the build door
|
|
# pushed, and which it used to download on every run.
|
|
ARG CRANE_VERSION=v0.20.2
|
|
RUN set -eux; \
|
|
case "$(dpkg --print-architecture)" in amd64) a=x86_64 ;; arm64) a=arm64 ;; *) exit 1 ;; esac; \
|
|
curl -fsSL "https://github.com/google/go-containerregistry/releases/download/$CRANE_VERSION/go-containerregistry_Linux_$a.tar.gz" \
|
|
| tar -xz -C /usr/local/bin crane; \
|
|
crane version
|
|
|
|
# bun, system-wide: the JS repos install with it, and a per-job setup action
|
|
# cannot put a binary on the PATH of a step that runs in the same guest.
|
|
ARG BUN_VERSION=1.3.9
|
|
RUN curl -fsSL "https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/bun-linux-$(dpkg --print-architecture | sed 's/amd64/x64/;s/arm64/aarch64/').zip" -o /tmp/bun.zip \
|
|
&& unzip -q /tmp/bun.zip -d /tmp && install -m0755 /tmp/bun-linux-*/bun /usr/local/bin/bun && ln -sf bun /usr/local/bin/bunx \
|
|
&& rm -rf /tmp/bun.zip /tmp/bun-linux-* && bun --version
|
|
|
|
# The docker CLI only. There is no daemon in here on purpose -- a job that wants
|
|
# to build an image asks the platform builder, which is the one build path.
|
|
RUN install -m0755 -d /etc/apt/keyrings \
|
|
&& curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc \
|
|
&& chmod a+r /etc/apt/keyrings/docker.asc \
|
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable" \
|
|
> /etc/apt/sources.list.d/docker.list \
|
|
&& apt-get update && apt-get install -y --no-install-recommends docker-ce-cli docker-buildx-plugin \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# git-runner: the agent that claims jobs from Hanzo Git. The binary is Gitea's
|
|
# act_runner release, installed under the name this estate calls it.
|
|
ARG RUNNER_VERSION=0.2.13
|
|
RUN set -eux; \
|
|
arch="$(dpkg --print-architecture)"; \
|
|
case "$arch" in amd64) a=amd64 ;; arm64) a=arm64 ;; *) echo "unsupported $arch" >&2; exit 1 ;; esac; \
|
|
curl -fsSL -o /usr/local/bin/git-runner \
|
|
"https://gitea.com/gitea/act_runner/releases/download/v${RUNNER_VERSION}/act_runner-${RUNNER_VERSION}-linux-${a}"; \
|
|
chmod +x /usr/local/bin/git-runner; \
|
|
git-runner --version
|
|
|
|
# The hanzo CLI, so a job can reach the cloud it builds for rather than
|
|
# re-implementing a piece of it in shell.
|
|
RUN set -eux; \
|
|
arch="$(dpkg --print-architecture)"; \
|
|
case "$arch" in amd64) a=x86_64 ;; arm64) a=aarch64 ;; *) a="$arch" ;; esac; \
|
|
curl -fsSL "https://hanzo.ai/install.sh" -o /tmp/hanzo-install.sh 2>/dev/null \
|
|
&& sh /tmp/hanzo-install.sh || echo "hanzo CLI: install script unavailable at build time"; \
|
|
rm -f /tmp/hanzo-install.sh
|
|
|
|
# GitHub's own runner, for a job GitHub queues: it registers with the one-job
|
|
# configuration the cloud minted (--jitconfig) and exits when that job is done.
|
|
ARG ACTIONS_RUNNER_VERSION=2.337.0
|
|
RUN set -eux; \
|
|
arch="$(dpkg --print-architecture)"; \
|
|
case "$arch" in amd64) a=x64 ;; arm64) a=arm64 ;; *) echo "unsupported $arch" >&2; exit 1 ;; esac; \
|
|
mkdir -p /opt/actions-runner && cd /opt/actions-runner; \
|
|
curl -fsSL -o runner.tgz "https://github.com/actions/runner/releases/download/v${ACTIONS_RUNNER_VERSION}/actions-runner-linux-${a}-${ACTIONS_RUNNER_VERSION}.tar.gz"; \
|
|
tar xzf runner.tgz && rm runner.tgz; \
|
|
./bin/installdependencies.sh && rm -rf /var/lib/apt/lists/*
|
|
|
|
# Playwright's chromium and the libraries it needs, at the version the site
|
|
# repos pin, live in the image: `playwright install chromium` at job time is a
|
|
# no-op, and `--with-deps` (which runs apt against whatever source a job added
|
|
# to the guest) is unnecessary.
|
|
ARG PLAYWRIGHT_VERSION=1.58.0
|
|
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright
|
|
RUN npx -y playwright@${PLAYWRIGHT_VERSION} install --with-deps chromium \
|
|
&& chmod -R a+rX /opt/playwright \
|
|
&& rm -rf /var/lib/apt/lists/* /root/.npm /root/.cache
|
|
|
|
# One entrypoint, two registrations: the environment says which system queued
|
|
# the job, and the runner that claims it lives exactly one job.
|
|
COPY --chmod=0755 runner /usr/local/bin/runner
|
|
|
|
RUN useradd -m -u 1001 -s /bin/bash runner \
|
|
&& echo 'runner ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/runner \
|
|
&& mkdir -p /data "$RUNNER_TOOL_CACHE" /home/runner/.cache/act \
|
|
&& ln -s "$RUNNER_TOOL_CACHE" /home/runner/.cache/act/tool_cache \
|
|
&& chown -R runner:runner /data "$RUNNER_TOOL_CACHE" "$COREPACK_HOME" /home/runner/.cache
|
|
|
|
USER runner
|
|
WORKDIR /data
|
|
RUN chown -R runner:runner /opt/actions-runner
|
|
|
|
# The actions build.yml declares, already cloned where the act runner looks for
|
|
# them: ~/.cache/act/<sha256 of the `uses:` string>, every ref the remote
|
|
# advertises included. A runner lives one job, so it began each one with an
|
|
# empty cache, and its clone is go-git's, full history and pure Go: forge run
|
|
# 16634 spent 156 s of each job's 161 s before the first step cloning these
|
|
# five (setup-node 72 s, setup-go 42 s, setup-bun 20 s, checkout 17 s), the
|
|
# three it would skip included, because a job prepares every action it names.
|
|
# With the clone here the runner opens it and fetches what moved since the
|
|
# image was built. Measured with act's own clone executor: 0.2 to 1.1 s each,
|
|
# against 2 to 90 s cold. A clone of the branches and tags alone is not
|
|
# enough: act fetches refs/*, so it then downloads every pull request's
|
|
# objects, 10 to 40 s each. hanzo.yml's `runner-preseeds` holds this list to
|
|
# build.yml's steps.
|
|
ARG ACTIONS="actions/checkout@v4 actions/setup-go@v5 actions/setup-node@v4 oven-sh/setup-bun@v2 dtolnay/rust-toolchain@stable"
|
|
RUN set -eux; \
|
|
for a in $ACTIONS; do \
|
|
d="/home/runner/.cache/act/$(printf %s "$a" | sha256sum | cut -c1-64)"; \
|
|
git clone -q "https://github.com/${a%@*}" "$d"; \
|
|
git -C "$d" fetch -q --update-head-ok origin '+refs/*:refs/*' '+HEAD:refs/heads/HEAD'; \
|
|
git -C "$d" rev-parse -q --verify "${a#*@}^{commit}" >/dev/null; \
|
|
done
|
|
ENTRYPOINT ["/usr/bin/tini","--","/usr/local/bin/runner"]
|