Skip to content

feat(tool-images): build the runner image with other runner ids - #167

Draft
Mearman wants to merge 1 commit into
mainfrom
feat/runner-build-runner-id
Draft

Mearman wants to merge 1 commit into
mainfrom
feat/runner-build-runner-id

Conversation

@Mearman

@Mearman Mearman commented Nov 2, 2026

Copy link
Copy Markdown
Member

A runner pod that runs with hostUsers: false gets its hostPath volumes (the node cache and the capability store) mounted idmapped, and the mapping keeps the container's uid as the node's: on a node, every file in <node cache>/<namespace>/<release>/pnpm-store and in the capability store is owned by uid and gid 1001, the stock runner user's. On a fleet I run, 1001 is a person's login account on some nodes and the operator's own account on others, so that person owns the CI's shared pnpm store and the tool copies every job runs, and can change them. The role already takes github_runner_arc_runner_uid and github_runner_arc_runner_gid for "an image with a different runner user", but no reference image has one.

tool-images/build now takes RUNNER_UID and RUNNER_GID (1001 unless set). When one differs it runs groupmod/usermod and moves every file the old ids owned to the new ones. groupmod and usermod refuse an id the image already uses. With the default ids the step changes no file, and the published 2.338.0 tag is never rebuilt anyway. A build runner entry in publish-tool-images.yml with a runner_id builds the same Dockerfile with both set to that id and tags it <runner version>-uid<id>. I added one for 549, an id in the middle of the system range that the fleet reserves for the runner's own account. Digest artifacts are now named with the architecture before the tag (digests-<kind>-<name>-<arch>-<tag>), because ...-2.338.0-* would also have matched the 2.338.0-uid549 artifacts and joined four digests under one tag.

Tested: tests/runner_build/run.sh (new, run by the new runner-build.yml on amd64 and arm64) builds both and checks the runner's uid, gid, name and group, the docker group, no file left with the old ids, a writable home, Runner.Listener --version, sudo -n true and cc, and that RUNNER_UID=0 is refused. It passed on an arm64 Mac's Docker. Dropping the chgrp step makes it fail on /home/runner. I also built the 549 variant for amd64 under emulation and checked id, sudo, a writable home and no file owned by 1001. tests/unit/test_publish_tool_images.py runs each entry's image step to get its tag, then checks three things: build and merge list the same entries, a runner_id entry gets both build arguments and the suffix, and each tag's download pattern matches exactly its own two artifacts. It fails when the pattern is put back to -*. It passed in python:3.12-slim. On macOS it cannot pass, because BSD wc -l pads its output, and that affects the step's own check, not just the test. actionlint, shellcheck, hadolint and yamllint are clean on the changed files.

This overlaps #149, which also edits the matrix and artifact names. Whichever lands second rebases onto the other: both can keep the architecture-before-tag naming, and a version entry composes with runner_id.

Order once merged: gh workflow run publish-tool-images.yml, make the new tag's package public if needed (it is the existing github-runner-runner-build package, so it already is), and take the 2.338.0-uid549@sha256:... reference from the run summary.

tool-images/build takes RUNNER_UID and RUNNER_GID, the stock 1001 unless set,
and moves every file the stock ids owned to the new ones.
Under hostUsers: false a runner pod's hostPath volumes are mounted idmapped,
so the node cache and capability store files belong on the node to the runner's own uid,
which on some nodes is a person's account.

The publish workflow builds a build runner entry with a runner_id
under the tag <runner version>-uid<id>, and names each digest artifact
with the architecture before the tag so one tag's pattern never matches another's.
tests/runner_build/run.sh checks both sets of ids on amd64 and arm64.
@Mearman

Mearman commented Nov 2, 2026

Copy link
Copy Markdown
Member Author

Published ghcr.io/exadev/github-runner-runner-build:2.338.0-uid549@sha256:ece854b13b8a652fb907fc4a29a05dfaf9edeb1eb5335553f9c7a51e5dc69cae from this branch's head 56237d0 (publish run https://github.com/ExaDev/github-runner/actions/runs/41448319868, dispatched on the branch). Every other entry logged "already published; nothing to do", and 2.338.0 still resolves to sha256:1335f3d1195fe1c3b7f75fc519c64b1124752f1846a3d4431d6d23de630796b3. Both platform images carry org.opencontainers.image.revision 56237d0. Pulled by digest on arm64 and amd64: id gives uid=549(runner) gid=549(runner) groups=549(runner),27(sudo),100(users),123(docker), and no file is left owned by uid or gid 1001.

If this branch changes, the tag is republished only through this workflow (after the old tag is deleted, since the workflow skips an existing tag), and the pin moves with it.

Recovery in kind fails here with ctr: content digest sha256:71b0cbca...: not found, the same failure as main's workflow_dispatch run on 98ba39c. #149 fixes it. It is not caused by this change, which touches nothing that job uses.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant