From b4e21d42f42646c3151b7c367a3034150c605164 Mon Sep 17 00:00:00 2001 From: swibrow <15628653+swibrow@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:05:48 +0200 Subject: [PATCH] feat(actions-runner): add a mise-managed CLI toolchain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gh and yq are standard workflow tooling that GitHub's hosted images ship and Ubuntu does not usefully package: there is no gh in the archive at all, and its `yq` is the unrelated Python jq-wrapper with different syntax. Without them, every workflow that opens a PR or patches a manifest curls its own copy into RUNNER_TEMP — exactly the per-repo drift this image exists to absorb. hadolint and shellcheck join them so lint jobs stop reaching for third-party setup actions. The list lives in mise.toml, one line per tool, so growing it costs no Dockerfile change. Renovate's native mise manager matches that filename by default, which is why the entries carry no `# renovate:` annotation; mise itself is a pinned release binary rather than the unpinned mise.run installer. Only static, version-insensitive binaries belong there — anything a workflow pins against stays with the setup-* actions and their RUNNER_TOOL_CACHE. The data dir stays root-owned and read-only to the runner user, with only the cache pointed somewhere writable. metadata.yaml's test is generic over mise.toml: it asserts the installed count matches the manifest and that every tool resolves through the shims and runs as the runner user, so adding a tool needs no edit there either. Signed-off-by: swibrow <15628653+swibrow@users.noreply.github.com> --- README.md | 2 +- images/actions-runner/Dockerfile | 29 ++++++++++++++++++++++++++++- images/actions-runner/metadata.yaml | 9 ++++++--- images/actions-runner/mise.toml | 12 ++++++++++++ 4 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 images/actions-runner/mise.toml diff --git a/README.md b/README.md index 2af6413..16f8957 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Public container images for cloud work, built multi-arch and rootless with secur | Image | Description | |---|---| -| `ghcr.io/cloudsnacks/actions-runner` | Rootless GitHub Actions runner for [Actions Runner Controller](https://github.com/actions/actions-runner-controller), with docker CLI and buildx | +| `ghcr.io/cloudsnacks/actions-runner` | Rootless GitHub Actions runner for [Actions Runner Controller](https://github.com/actions/actions-runner-controller), with docker CLI, buildx and a mise-managed CLI toolchain ([`mise.toml`](images/actions-runner/mise.toml)) | | `ghcr.io/cloudsnacks/claude-code` | Headless Claude Code agent with git-repo and agent-profile bootstrap | | `ghcr.io/cloudsnacks/dev-desktop` | Arch Linux dev workstation with Hyprland streamed over [Sunshine](https://github.com/LizardByte/Sunshine)/Moonlight, cloud tooling, and AI coding agents (`linux/amd64` only, needs `/dev/dri`) | | `ghcr.io/cloudsnacks/infisical-mcp` | Infisical MCP server (secrets management over MCP) | diff --git a/images/actions-runner/Dockerfile b/images/actions-runner/Dockerfile index 50ef229..defcdca 100644 --- a/images/actions-runner/Dockerfile +++ b/images/actions-runner/Dockerfile @@ -107,6 +107,33 @@ RUN DOCKER_ARCH="$(case "${TARGETARCH}" in amd64) echo x86_64 ;; arm64) echo aar && docker --version \ && docker buildx version +# Workflow tooling that GitHub's own hosted images ship and Ubuntu does not +# usefully package — there is no gh in the archive at all, and its `yq` is the +# unrelated Python jq-wrapper with different syntax. Without them here every +# workflow that opens a PR or patches a manifest curls its own copy into +# RUNNER_TEMP, which is the per-repo drift this image exists to absorb. +# +# The list lives in mise.toml, one line per tool, so growing it costs no +# Dockerfile. Renovate's native mise manager reads that file, which is why +# those entries carry no `# renovate:` annotation. mise itself is fetched as a +# pinned release binary rather than through the unpinned mise.run installer. +# renovate: datasource=github-releases depName=jdx/mise +ARG MISE_VERSION=2026.7.17 +# Data dir stays root-owned and read-only to the runner user; the cache is the +# only path mise writes to at runtime, so it points somewhere world-writable. +ENV MISE_DATA_DIR=/opt/mise \ + MISE_CONFIG_DIR=/opt/mise \ + MISE_CACHE_DIR=/tmp/mise-cache \ + PATH=/opt/mise/shims:$PATH +COPY mise.toml /opt/mise/config.toml +RUN MISE_ARCH="$(case "${TARGETARCH}" in amd64) echo x64 ;; *) echo "${TARGETARCH}" ;; esac)" \ + && curl -fsSL -o /tmp/mise \ + "https://github.com/jdx/mise/releases/download/v${MISE_VERSION}/mise-v${MISE_VERSION}-linux-${MISE_ARCH}" \ + && install -o root -g root -m 0755 /tmp/mise /usr/bin/mise \ + && rm -f /tmp/mise \ + && mise install --yes \ + && mise ls --installed + USER runner -LABEL org.opencontainers.image.description="GitHub Actions runner for Actions Runner Controller, with docker CLI and buildx" +LABEL org.opencontainers.image.description="GitHub Actions runner for Actions Runner Controller, with docker CLI, buildx and a mise-managed CLI toolchain" diff --git a/images/actions-runner/metadata.yaml b/images/actions-runner/metadata.yaml index 6205d06..2e2b514 100644 --- a/images/actions-runner/metadata.yaml +++ b/images/actions-runner/metadata.yaml @@ -1,14 +1,17 @@ --- name: actions-runner -description: GitHub Actions runner for Actions Runner Controller, with docker CLI and buildx +description: GitHub Actions runner for Actions Runner Controller, with docker CLI, buildx and a mise-managed CLI toolchain # -. Bump the revision whenever this image's # contents change without RUNNER_VERSION moving, so scale sets pulling with the # default IfNotPresent policy get a tag they have not already cached. Renovate # tracks upstream on RUNNER_VERSION in the Dockerfile — deliberately not # annotated here, since a -N suffix is a semver pre-release and would be # "upgraded" straight back to the bare release. -version: 2.336.0-2 +version: 2.336.0-3 platforms: - linux/amd64 - linux/arm64 -test: /home/runner/bin/Runner.Listener --version && tmux -V && docker --version && docker buildx version && dpkg -s libatomic1 libnss3 libgbm1 fonts-noto-color-emoji >/dev/null && test "$LANG" = C.UTF-8 && id -nG | grep -qw docker +# The mise loop is deliberately generic over mise.toml: every tool listed there +# must resolve through the shims on PATH and run as the runner user, so adding +# a tool needs no edit here. +test: /home/runner/bin/Runner.Listener --version && tmux -V && docker --version && docker buildx version && jq --version && test "$(mise ls --installed --json | jq 'keys|length')" -eq "$(yq -p toml -oy '.tools|length' /opt/mise/config.toml)" && for t in $(mise ls --installed --json | jq -r 'keys[]'); do command -v "$t" | grep -q '^/opt/mise/shims/' && "$t" --version >/dev/null || exit 1; done && dpkg -s libatomic1 libnss3 libgbm1 fonts-noto-color-emoji >/dev/null && test "$LANG" = C.UTF-8 && id -nG | grep -qw docker diff --git a/images/actions-runner/mise.toml b/images/actions-runner/mise.toml new file mode 100644 index 0000000..ef9d340 --- /dev/null +++ b/images/actions-runner/mise.toml @@ -0,0 +1,12 @@ +# Long-tail CLI tooling for workflows: one line per tool. Renovate's native +# mise manager matches this filename by default, so entries here need no +# `# renovate:` annotation and no addition to the repo's custom manager. +# +# Only static, version-insensitive binaries belong here. Anything a workflow +# pins against (language runtimes, kubectl, helm) stays out and is left to the +# setup-* actions, which cache into RUNNER_TOOL_CACHE. +[tools] +gh = "2.96.0" +hadolint = "2.14.0" +shellcheck = "0.11.0" +yq = "4.53.3"