github-runner
Recipe card from the charly-distros plugin (Images — the deployable catalog).
github-runner – GitHub Actions self-hosted runner
Section titled “github-runner – GitHub Actions self-hosted runner”Candy Properties
Section titled “Candy Properties”| Property | Value |
|---|---|
| Dependencies | supervisord, container-nesting |
| Volume | state -> ${HOME}/actions-runner |
| Service | github-runner (supervisord; runs run.sh as uid 1000) |
| Security | none of its own — rootless nested podman comes from container-nesting (no privileged) |
| Install files | task: — declarative pinned download: of the runner tarball + write: of the ghcr mirror config; the runner tree is root-extracted (the shared download cache is root-owned) then chown -Red to uid 1000 (the one ownership cmd:) |
Packages (distro.arch, all in the official core/extra repos)
Section titled “Packages (distro.arch, all in the official core/extra repos)”-
Toolchain:
jq,git,go,cloud-guest-utils,cosign,inetutils(inetutilsishostname(1)’s only Arch provider — charly’s own venue tests runhostnameto prove a peer executed host-locally, and without the binary they fail with exit 127; see CI job tool surface below) -
Cross-arch CI:
qemu-user-static,qemu-user-static-binfmt(aarch64 binfmt) -
.NET runtime deps for the runner binary (its
installdependencies.shhas no Arch branch):icu,krb5,openssl,libunwind,lttng-ust(NOTzlib— CachyOS shipszlib-ng-compat, which Provides it; an explicitzlibconflicts) -
charly-host
depends=completion:slirp4netns,libisoburn,cdrtools,swtpm— the part of the charly package’sdepends=set (packaging.formats.archlinux.dependsin the charly candy) the charly/virtualization candies do not already install, so the charly CLI (and the CI jobs that drive it) runs fully on the runner. (The legacycharly box pkgrelease-packages build that ran here is removed with the nFPM cutover — thecharly generate-packagesplugin + the per-distro repos build the packages now.) -
CI job tool surface — the pool is org-wide, so a job must find whatever the org’s workflows actually call. Every entry below carries a
package:check in the candy’s ownplan:, so a tool that is declared but not installed fails the image build rather than a job at 02:00:tree,shellcheck,shfmt,actionlint,golangci-lint(so a job can lint without its owngo install; the packaged build measured 2.14.0, and it is not what CI’s gate uses — that installs its own pinned@v2.13.2each run, so the image cannot decide a CI lint verdict),nodejs,npm,python-pip,uv,cmake,unzip,zip,7zip,wget,rsync,yq,ripgrep,moreutils,bc,less,lsof,strace,openbsd-netcat,bind. Deliberately absent, as decisions rather than omissions:docker(16 refs exist, alldocker run --rm -v …distro package builds on hosted runners — a podman shim is not docker, and quietly swapping the two in CI trades a loud failure for a silent wrong one, R4);rust/cargo(6 refs, in repos not on the pool); a JDK and the cloud CLIs (zero refs org-wide); and passwordlesssudo(8 workflow files usesudo, all in distro repos building packages on hosted runners — NOPASSWD root inside a long-lived container that runs org PRs would widen an escape path for a convenience nothing on the pool asks for).
podman/buildah/skopeo/crun/fuse-overlayfs are provided by the
container-nesting dependency (not redeclared here — R3).
Environment / token contract
Section titled “Environment / token contract”| Variable | Mechanism |
|---|---|
RUNNER_ORG |
env_accept (plaintext identifier; supplied at deploy) |
RUNNER_TOKEN |
secret_accept (credential-store-backed; never in charly.yml/quadlet) |
RUNNER_WORK_DIR |
${HOME}/actions-runner/_work |
RUNNER_LABELS |
env_accept — the labels a job’s runs-on: selects on; the hook defaults to opencharly |
LANG |
C.UTF-8 (fixed) — see Locale below |
TZ |
UTC (fixed) — deterministic log timestamps |
Locale. The base image generates none: locale -a lists only C, C.utf8
and POSIX, and with LANG unset a non-login process is in the POSIX locale, so
locale charmap reports ANSI_X3.4-1968 (ASCII). LANG=C.UTF-8 is set in the
candy’s env:; glibc has C.UTF-8 built in, so no locale needs generating. The
base image’s /etc/locale.conf also names C.UTF-8, but that reaches only
login/PAM sessions, and both a charly check and a job process are neither — the
env: declaration is the one that applies, which is why the plan: assertion
(locale charmap under context: runtime) genuinely fails without it. It is a
claim about the locale the tools see, not about Python: CPython >= 3.7 coerces the
C locale itself and reports utf-8 either way. Note that
the TZ check asserts the variable (printenv TZ), not date +%Z: the image’s
/etc/localtime is already UTC, so date +%Z would pass either way and prove
nothing.
Runner version. RUNNER_VERSION seeds the initial download — it is not a
pin. The actions runner self-updates on its own schedule, and because
~/actions-runner is a persistent volume that update survives an image rebuild:
measured on the live fleet, bin symlinks to bin.2.337.0 while the deployed image
declares 2.334.0 (and bin.2.334.0 is still on disk beside it). --disableupdate
is deliberately not set, so the fleet stays inside GitHub’s supported runner
range without anyone bumping the value; the cost is that the declared number
describes a fresh volume, not a running one.
The runner installs under ${HOME}/actions-runner and runs as uid 1000. The
ghcr.io pull-through mirror (127.0.0.1:5000) config is written to the user
location ${HOME}/.config/containers/registries.conf.d/ (rootless podman).
Lifecycle Hooks (token-guarded)
Section titled “Lifecycle Hooks (token-guarded)”- post_enable — registers the runner with
RUNNER_ORG+RUNNER_TOKEN. Skips (no-op) whenRUNNER_TOKENis empty — so a token-less deploy (an check bed) brings the image up without registering. - pre_remove — deregisters; needs a remove-token (distinct from the registration token). Also skips when the token is empty.
# charly.yml — rootless, CachyOSgithubrunner: base: cachyos.cachyos build: [pac] candy: [agent-forwarding, github-runner, charly, dbus, container-nesting] network: host # no uid/privileged override → rootlessTOKEN=$(gh api -X POST /orgs/myorg/actions/runners/registration-token --jq .token)charly config githubrunner -e RUNNER_ORG=myorg -e RUNNER_TOKEN="$TOKEN"charly remove githubrunner -e RUNNER_TOKEN=$(gh api -X POST /orgs/myorg/actions/runners/remove-token --jq .token)Org-runner routing: a queued job is almost never an agent problem
Section titled “Org-runner routing: a queued job is almost never an agent problem”A job that shows labels: ["<your-label>"], status: queued, runner_name: ""
while the runner reads online + busy: false is a ROUTING failure, not a
runner failure — the runner has nothing to execute. Work the causes in order:
-
The runner group does not allow public repos (the silent one). An org runner in a group whose
allows_public_repositoriesisfalseis INVISIBLE to every public repo in the org, so their jobs queue forever with no error anywhere. Check and fix with a REAL boolean (the-fflag sends the string"true", which the API rejects as"true" is not a boolean— use--input):Terminal window gh api /orgs/<org>/actions/runner-groups --jq '.runner_groups[]|{id,name,allows_public_repositories,visibility}'echo '{"allows_public_repositories": true}' > rg.jsongh api -X PATCH /orgs/<org>/actions/runner-groups/<id> --input rg.json -
labelsmismatch. The runner must carry every labelruns-onnames. Comparegh api /orgs/<org>/actions/runners --jq '.runners[0].labels'against the job’slabels(a reusable workflow may resolve avars.label you forgot to set, e.g.vars.REVIEW_RUNNER_LABEL). -
A stale broker session. After a container restart the listener can log
A session for this runner already exists/TaskAgentSessionConflictExceptionand retry on a 30s loop without taking jobs, while still reportingonline. A full clean cycle clears it —charly remove(deregisters) thencharly config+charly start— not just a restart.
Confirm the fix by watching the job, not the runner: a picked-up job flips to
in_progress with runner_name set, and the workflow’s steps then appear.
gh must be present for a review/gate runner
Section titled “gh must be present for a review/gate runner”A runner whose job is to execute gh-driven workflows (the org pr-validator
runs gh api / gh pr comment / gh pr merge) needs the gh CLI in its
image — compose the layer-gh candy (it is not part of the base toolchain). A
runner without it fails the workflow at its first gh call.
Used In Boxes
Section titled “Used In Boxes”Related Candies
Section titled “Related Candies”/charly-infrastructure:supervisord— process manager dependency/charly-distros:container-nesting— rootless nested podman/buildah/skopeo + subuid layout + caps
When to Use This Skill
Section titled “When to Use This Skill”Use when the user asks about:
- GitHub Actions self-hosted runners
- Runner registration or deregistration
- CI/CD container infrastructure
RUNNER_TOKENorRUNNER_ORGconfiguration
Author + Test References
Section titled “Author + Test References”/charly-image:layer— candy authoring reference (tasks, vars, secret_accept, check block syntax)/charly-check:check— declarative testing framework for thecheck:block + thecheck-githubrunner-podbed/charly-build:secrets— the credential store backingRUNNER_TOKEN

