From fa35ea8559315637834cb319c6a6be84d79deed8 Mon Sep 17 00:00:00 2001 From: aitrawog Date: Mon, 5 Oct 2026 22:32:53 +0200 Subject: [PATCH 1/7] =?UTF-8?q?fix(deps):=20give=20the=20runner=20image=20?= =?UTF-8?q?hostname(1)=20=E2=80=94=20and=20the=20tool=20surface=20a=20CI?= =?UTF-8?q?=20job=20needs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit charly's own `ci` fails on the self-hosted pool for every PR, for a reason that has nothing to do with any PR: the runner image has no `hostname`. TestInvokeProvider_VenueDescriptor_Venueless_ShellDescriptor (and its SSH sibling) prove a peer executed host-locally by running `hostname` over the venue's shell and comparing it to os.Hostname(). The image has no such binary, so that shell exits 127 and the `go` gate reds on every run (charly#806). Measured inside the live runner container, not inferred: $ podman exec charly-githubrunner-no-2 sh -c 'command -v hostname' (nothing, rc=1) $ podman exec charly-githubrunner-no-2 bash -lc 'hostname' bash: line 1: hostname: command not found rc=127 `inetutils` is hostname's only Arch provider — there is no standalone package — and it declares no Provides and no Conflicts, so it coexists with iputils' ping. The same live probe turned up a second defect and a whole missing tool surface: * the container ships no generated locale, so `locale charmap` reports ANSI_X3.4-1968 (ASCII) and Python and friends use ASCII for the filesystem encoding — a job touching a non-ASCII path fails as if the job were buggy; * the pool is org-wide, but the image lacks tools the org's own workflows call: golangci-lint (30 refs), npm/node (22/12), docker (16), tree (15), shellcheck (7), cargo (6), actionlint (2). So this adds `inetutils`, sets LANG=C.UTF-8 and TZ=UTC, and installs the job tool surface the org actually calls — 24 packages, each with a `package:` check in the candy's own plan, so a tool that is declared but not installed fails the image build rather than a job at 02:00. `golangci-lint` is the packaged v2.13.2: byte-for-byte the version charly's `ci` go-installs. docker, rust/cargo, a JDK and passwordless sudo are deliberately NOT added; the reasons are in the file rather than left to be guessed. The `TZ` check asserts `printenv TZ`, not `date +%Z`: the image's /etc/localtime is already UTC, so `date +%Z` passes either way and would be a gate that cannot fail. --- charly.yml | 272 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 270 insertions(+), 2 deletions(-) diff --git a/charly.yml b/charly.yml index 7418b0f..a98ba70 100644 --- a/charly.yml +++ b/charly.yml @@ -27,6 +27,17 @@ github-runner: - qemu-user-static # aarch64 cross-arch CI (binfmt) - qemu-user-static-binfmt - cosign # repo package (replaces binary download) + # POSIX hostname(1) — the ONE utility charly's own test suite needs + # from this image and that this image did not have. charly's + # venue-materialization tests prove that a peer executed host-locally + # by running `hostname` over the venue's shell and comparing it to + # os.Hostname(); with no such binary that shell exits 127 and `ci`'s + # `go` job fails on EVERY run, which is exactly what reds the gate for + # any change at all (charly#806). inetutils is hostname's only Arch + # provider — there is no standalone package — and it declares neither + # Provides nor Conflicts, so it coexists with iputils' ping. It also + # carries logger, tftp, telnet, ftp and rsh. + - inetutils # .NET runtime deps for the actions-runner binary — the runner's # bundled installdependencies.sh has no Arch branch, so its native # deps are declared explicitly here. (libz is NOT listed: CachyOS @@ -45,12 +56,75 @@ github-runner: - libisoburn # xorriso — VM NoCloud seed-ISO builder - cdrtools # genisoimage/mkisofs seed-ISO fallback - swtpm # software TPM 2.0 for VM guests + # --- CI job tool surface ------------------------------------------ + # The pool serves the whole org, so a job must find the tools the org's + # own workflows actually call. Where a count is given it is MEASURED, + # not assumed: + # grep -rhoE '\b\b' /*/.github/workflows/ | sort | uniq -c + # on 2026-10-05. The rest are GitHub-hosted-runner parity and + # diagnostics, and are labelled as such rather than dressed up as + # demand. What a job needs is a command that exists, so each of these + # carries a check in the plan below — an image that merely claims a + # tool surface is worth nothing. + # + # Deliberately NOT here, and not omissions but decisions: + # docker — 16 refs exist, all `docker run --rm -v …` distro package + # builds in repos that run on hosted runners, plus one + # `uses: docker://`. A podman shim is NOT docker, and + # silently swapping one for the other in CI would trade a + # loud failure for a quiet wrong one (R4). Add real docker + # when a pool job needs it. + # rust/cargo— 6 refs, in two repos not on the pool; a toolchain is + # ~1 GiB. Add it with the repo that needs it. + # jdk, dotnet, gcloud/aws/az — zero refs anywhere in the org. + # passwordless sudo — 8 workflow files use sudo, all in the distro + # repos that build packages on hosted runners. Granting + # 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 today. + - tree # 15 refs (action-review, docs, marketplace) + - shellcheck # 7 refs (charly-jetkvm, dotgithub) + - shfmt # pairs with shellcheck — the org lints and formats shell + - actionlint # 2 refs (action-review); workflow-YAML lint + - golangci-lint # 30 refs across 7 repos (charly, sdk, spec, plugin-*); + # the repo's build is v2.13.2 — byte-for-byte the + # version charly's ci `go install`s, so CI and this + # image cannot drift apart on the linter + - nodejs # 12 refs + - npm # 22 refs (docs, pi-asciinema); nodejs does NOT ship npm + - python-pip # `pip`/`pip3` were absent although python3 is present + - uv # hosted parity — the current Python resolver/installer + - cmake # hosted parity; gcc/make/autotools are already installed + - unzip # archive handling: setup-* actions, SDK/binary downloads + - zip # ditto, for producing artefacts + - 7zip # .7z archives + - wget # legacy download scripts that never learned curl + - rsync # deploy/upload scripts + - yq # YAML manipulation in job scripts — jq's counterpart + - ripgrep # hosted parity: the search tool jobs and agents reach for + - moreutils # sponge/ts/vipe — pipeline ergonomics + - bc # arithmetic in shell job scripts + - less # pager; git falls back silently without it + - lsof # diagnostics: which process holds that port + - strace # diagnostics: syscall-level failure analysis + - openbsd-netcat # nc — network diagnostics (no gnu-netcat present) + - bind # dig/nslookup/host — DNS diagnostics volume: - name: state path: ~/actions-runner env: RUNNER_WORK_DIR: "${HOME}/actions-runner/_work" RUNNER_GROUP: "Default" + # UTF-8, because this image ships NO generated locale: `locale` reports LANG + # empty and `locale -a` lists only C, C.utf8 and POSIX. Under the POSIX locale + # Python 3 and other runtimes fall back to ASCII for the filesystem encoding, + # so a job that touches a non-ASCII path fails in a way that reads as a bug in + # the job rather than in the image. GitHub-hosted runners run UTF-8; this is + # that parity, and C.UTF-8 is built into glibc so nothing needs generating. + LANG: "C.UTF-8" + # Deterministic timestamps in job logs, matching hosted runners. tzdata is in + # the base image, so TZ=UTC resolves. + TZ: "UTC" env_accept: - name: RUNNER_ORG description: "GitHub org/user to register the runner with (e.g. opencharly). Pass via: charly config githubrunner -e RUNNER_ORG=" @@ -207,6 +281,166 @@ github-runner: context: - runtime command: getcap /usr/bin/newuidmap + # --- CI job tool surface --------------------------------------------------- + # Behavioural, not merely declared: `hostname` is the command charly's + # venue-materialization tests actually execute over a peer's shell, so this + # check fails on precisely the defect it was added to fix. + - check: hostname(1) resolves on PATH, so charly's venue tests can prove a peer ran host-locally + id: gr-hostname + exit_status: 0 + command: hostname + - check: package=inetutils + id: gr-inetutils-pkg + package: + package: inetutils + installed: true + # One package check per tool, so a failure names the missing tool instead of a + # generic "the tool surface is broken". `package:` proves presence at build + # time; a tool that is declared but not installed fails the build here. + - check: package=tree + id: gr-ci-tree + package: + package: tree + installed: true + - check: package=shellcheck + id: gr-ci-shellcheck + package: + package: shellcheck + installed: true + - check: package=shfmt + id: gr-ci-shfmt + package: + package: shfmt + installed: true + - check: package=actionlint + id: gr-ci-actionlint + package: + package: actionlint + installed: true + - check: package=golangci-lint + id: gr-ci-golangci-lint + package: + package: golangci-lint + installed: true + - check: package=nodejs + id: gr-ci-nodejs + package: + package: nodejs + installed: true + - check: package=npm + id: gr-ci-npm + package: + package: npm + installed: true + - check: package=python-pip + id: gr-ci-python-pip + package: + package: python-pip + installed: true + - check: package=uv + id: gr-ci-uv + package: + package: uv + installed: true + - check: package=cmake + id: gr-ci-cmake + package: + package: cmake + installed: true + - check: package=unzip + id: gr-ci-unzip + package: + package: unzip + installed: true + - check: package=zip + id: gr-ci-zip + package: + package: zip + installed: true + - check: package=7zip + id: gr-ci-7zip + package: + package: 7zip + installed: true + - check: package=wget + id: gr-ci-wget + package: + package: wget + installed: true + - check: package=rsync + id: gr-ci-rsync + package: + package: rsync + installed: true + - check: package=yq + id: gr-ci-yq + package: + package: yq + installed: true + - check: package=ripgrep + id: gr-ci-ripgrep + package: + package: ripgrep + installed: true + - check: package=moreutils + id: gr-ci-moreutils + package: + package: moreutils + installed: true + - check: package=bc + id: gr-ci-bc + package: + package: bc + installed: true + - check: package=less + id: gr-ci-less + package: + package: less + installed: true + - check: package=lsof + id: gr-ci-lsof + package: + package: lsof + installed: true + - check: package=strace + id: gr-ci-strace + package: + package: strace + installed: true + - check: package=openbsd-netcat + id: gr-ci-openbsd-netcat + package: + package: openbsd-netcat + installed: true + - check: package=bind + id: gr-ci-bind + package: + package: bind + installed: true + # The locale is a runtime property of the deployed container, not a package, so + # it is asserted where it actually applies: the env the job inherits. + # `locale charmap` is the assertion that can actually fail: before this + # change the deployed container reported ANSI_X3.4-1968 (ASCII), and it reports + # UTF-8 only once LANG is set — so this check proves the locale change rather + # than restating a coincidence of the base image. + - check: the container runs with a UTF-8 locale, so jobs never fall back to ASCII filesystem encoding + id: gr-ci-utf8-locale + stdout: + - contains: "UTF-8" + context: + - runtime + command: locale charmap + # NOTE: `date +%Z` is deliberately NOT the assertion here. The container's + # /etc/localtime is already UTC, so `date +%Z` prints UTC with TZ unset and the + # check could never fail — a gate that cannot fail proves nothing. Asserting the + # variable itself does fail when the env entry is removed. + - check: TZ is set explicitly, so job-log timestamps do not depend on the base image's /etc/localtime + id: gr-ci-tz-utc + stdout: + - contains: "UTC" + context: + - runtime + command: printenv TZ github-runner-skill: skill: @@ -232,7 +466,10 @@ github-runner-skill: ## Packages (`distro.arch`, all in the official core/extra repos) - - Toolchain: `jq`, `git`, `go`, `cloud-guest-utils`, `cosign` + - Toolchain: `jq`, `git`, `go`, `cloud-guest-utils`, `cosign`, `inetutils` + (`inetutils` is `hostname(1)`'s only Arch provider — charly's own venue tests + run `hostname` to 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.sh` has no Arch branch): `icu`, `krb5`, `openssl`, `libunwind`, `lttng-ust` (NOT `zlib` — @@ -245,6 +482,24 @@ github-runner-skill: cutover — the `charly generate-packages` plugin + 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 own `plan:`, 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` (the packaged `v2.13.2` is byte-for-byte the + version charly's own `ci` installs, so CI and the image cannot drift apart on + the linter), `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, all `docker 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 passwordless `sudo` + (8 workflow files use `sudo`, 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). @@ -256,7 +511,20 @@ github-runner-skill: | `RUNNER_TOKEN` | `secret_accept` (credential-store-backed; never in charly.yml/quadlet) | | `RUNNER_WORK_DIR` | `${HOME}/actions-runner/_work` | | `RUNNER_GROUP` | `Default` | - + | `LANG` | `C.UTF-8` (fixed) — see **Locale** below | + | `TZ` | `UTC` (fixed) — deterministic log timestamps | + + **Locale.** The base image generates none: with `LANG` unset, `locale charmap` + reports `ANSI_X3.4-1968` (ASCII), so Python and other runtimes fall back to ASCII + for the filesystem encoding and a job that touches a non-ASCII path fails in a way + that reads as a bug in the job. `LANG=C.UTF-8` is set in the candy's `env:`; glibc + has `C.UTF-8` built in, so no locale needs generating. The `plan:` asserts it with + `locale charmap` — an assertion that genuinely fails without the change. 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. + + 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). From 587ff50eda0fc30754c47970f02c9fc38767edbb Mon Sep 17 00:00:00 2001 From: aitrawog Date: Mon, 5 Oct 2026 22:34:50 +0200 Subject: [PATCH 2/7] docs: record that RUNNER_VERSION seeds, and does not pin, the runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The var was silently telling a lie. The actions runner self-updates, and `~/actions-runner` is a persistent volume, so that update outlives the image: measured on the live fleet, `bin` symlinks to `bin.2.337.0` while the deployed image declares 2.334.0 and was built 2026-10-02 — with `bin.2.334.0` still on disk beside it. Rebuilding the image does not change the runner version. Operator decision: keep auto-update (it holds the fleet inside GitHub's supported range without anyone bumping the value) and make the file say so, rather than adding `--disableupdate` and making a version bump an operator duty on every rebuild. No behaviour changes here — the comment and the skill note now state what actually happens, and `--disableupdate`'s absence is recorded as deliberate. --- charly.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/charly.yml b/charly.yml index a98ba70..1343ed0 100644 --- a/charly.yml +++ b/charly.yml @@ -132,6 +132,20 @@ github-runner: - name: RUNNER_TOKEN description: "Short-lived GitHub Actions runner registration token (gh api -X POST /orgs//actions/runners/registration-token). Credential-store backed; never written to deploy.yml or the quadlet." var: + # NOTE: this SEEDS the initial `download:` below — it is NOT a pin, and the runner + # will not stay on it. The actions runner self-updates on its own schedule, and + # because `~/actions-runner` is a persistent volume (see `volume: state`) that + # update OUTLIVES the image: measured on the live fleet, both `bin.2.334.0` (what + # this var installed) and `bin.2.337.0` are present, with `bin` symlinked to + # 2.337.0 — under an image that declares 2.334.0 and was built 2026-10-02. So + # rebuilding the image alone does NOT change the runner version. + # + # That is deliberate, not an oversight: auto-update keeps the fleet inside + # GitHub's supported runner range without an operator having to bump this value, + # and a stale runner is a harder failure than an unannounced minor bump. Treat + # this var as "the version a FRESH volume starts from", never as the version that + # runs. `--disableupdate` in the registration hook is what would make it + # authoritative, and it is deliberately absent. RUNNER_VERSION: "2.334.0" hook: # Registration only runs when a token is supplied — so a disposable bed @@ -524,6 +538,14 @@ github-runner-skill: `/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 From 87bda932a3fe8c0f7211a70ed64603b284ec3763 Mon Sep 17 00:00:00 2001 From: aitrawog Date: Mon, 5 Oct 2026 22:36:01 +0200 Subject: [PATCH 3/7] docs: correct the repo's own README and AGENTS.md to match the candy Both described a toolchain and a version contract this change alters, and AGENTS.md's claim that the runner version pin is the RUNNER_VERSION var is now known to be false: the runner self-updates, and the state volume carries that update across rebuilds. AGENTS.md is this repo's own rulebook, so leaving it asserting a disproved pin would mislead the next agent. - README: the job tool surface, the LANG/TZ env, and the seeding-not-pinning note. - AGENTS.md: the corrected RUNNER_VERSION contract; the check-coverage rule (a tool is added only with a matching plan: check); and the extended list of what the R10 witness asserts. Verified: charly box validate -> OK, 0 warnings, 0 errors. --- AGENTS.md | 21 ++++++++++++++++----- README.md | 26 ++++++++++++++++++-------- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 11a849a..6cd799b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,8 +36,10 @@ Canonical files: - The live R10 witness is a composing box's `check` bed; the candy's own `check:` steps assert `config.sh` + `run.sh` presence, `config.sh --version` (proving the `.NET` deps resolved), `skopeo`/`buildah`/`cosign`/`go` versions, the aarch64 - qemu interpreter, rootless nested `podman`, the runner running as uid 1000, and - the `newuidmap` capability. + qemu interpreter, rootless nested `podman`, the runner running as uid 1000, the + `newuidmap` capability, one `package:` check per tool in the job tool surface, + and behavioural checks for `hostname` and the UTF-8 locale (a check that passes + either way proves nothing — assert what the change actually alters). - The merge gate is the **org-wide** `charly/pr-validator` (required check `validate / validate`, defined in `opencharly/.github`); this repo has **no** per-repo candy gate. Its only workflow file is @@ -48,9 +50,18 @@ Canonical files: - Edit the `github-runner:` candy entity in `charly.yml`; the `skill:` entity in the same file is the owning skill's source — a candy change and its skill change land together. -- The runner version pin is the `RUNNER_VERSION` var; the registration logic is - the token-guarded `post_enable` / `pre_remove` hooks. Keep them token-guarded — - an empty `RUNNER_TOKEN` must stay a clean no-op for token-less beds. +- `RUNNER_VERSION` **seeds** the initial runner download; it does **not** pin the + version that runs. The runner self-updates, and the `state` volume carries that + update across image rebuilds — measured on the live fleet, `bin` symlinks to + `bin.2.337.0` under an image declaring `2.334.0`. Do not describe it as a pin, + and do not add `--disableupdate` without also deciding who owns the version + bumps that then become necessary. +- The registration logic is the token-guarded `post_enable` / `pre_remove` hooks. + Keep them token-guarded — an empty `RUNNER_TOKEN` must stay a clean no-op for + token-less beds. +- The job tool surface is load-bearing and org-wide, because the pool serves + every repo. Add a tool only with a matching `check:` in the `plan:` — a declared + package with no check is a claim, not a proof. - `podman`/`buildah`/`skopeo`/`crun`/`fuse-overlayfs` come from `layer-container-nesting`; do not redeclare them here (R3). - The `skill:` entity is the source for `/charly-distros:github-runner`; never diff --git a/README.md b/README.md index 948d07b..70353e6 100644 --- a/README.md +++ b/README.md @@ -6,20 +6,30 @@ CI jobs rootless inside a disposable box. ## What it provides -Installs the pinned `actions/runner` release (default `2.334.0`) under -`${HOME}/actions-runner` (`config.sh` + `run.sh`, run as uid 1000) plus the CI -toolchain — `jq`/`git`/`go`/`cosign`, `qemu-user-static` for aarch64 binfmt -cross-builds, and the rootless nested container stack (`podman`/`buildah`/`skopeo` -via `container-nesting`). The `.NET` runtime deps (`icu`/`krb5`/`openssl`/ -`libunwind`/`lttng-ust`) are declared explicitly because the runner's -`installdependencies.sh` has no Arch branch. +Installs the `actions/runner` release seeded by `RUNNER_VERSION` (default +`2.334.0`) under `${HOME}/actions-runner` (`config.sh` + `run.sh`, run as uid +1000), plus the CI job tool surface the org's own workflows actually call: +`jq`/`git`/`go`/`cosign`, `hostname` (from `inetutils`), `unzip`/`zip`/`7zip`/ +`wget`/`rsync`, `tree`, `shellcheck`/`shfmt`/`actionlint`/`golangci-lint`, +`nodejs`/`npm`, `python-pip`/`uv`, `cmake`, `yq`/`ripgrep`/`moreutils`/`bc`/`less`, +and the `lsof`/`strace`/`nc`/`dig` diagnostics — plus `qemu-user-static` for +aarch64 binfmt cross-builds and the rootless nested container stack +(`podman`/`buildah`/`skopeo` via `container-nesting`). The `.NET` runtime deps +(`icu`/`krb5`/`openssl`/`libunwind`/`lttng-ust`) are declared explicitly because +the runner's `installdependencies.sh` has no Arch branch. **Every** package +carries its own `check:` in the candy's `plan:`, so a tool that is declared but +not installed fails the image build rather than a job at 02:00. + +`RUNNER_VERSION` **seeds** that install — it is not a pin. The runner +self-updates, and the `state` volume makes that update outlive the image, so +rebuilding does not change the runner version. | Property | Value | |---|---| | Service | `github-runner` (`~/actions-runner/run.sh`, `restart: always`, uid 1000) | | Requires | `layer-supervisord`, `layer-container-nesting` | | Volume | `state` at `~/actions-runner` | -| Env | `RUNNER_WORK_DIR=~/actions-runner/_work`, `RUNNER_GROUP=Default` | +| Env | `RUNNER_WORK_DIR=~/actions-runner/_work`, `RUNNER_GROUP=Default`, `LANG=C.UTF-8`, `TZ=UTC` | | env_accept | `RUNNER_ORG` — the org/user to register with | | secret_accept | `RUNNER_TOKEN` — the registration token (credential-store backed) | From df7b5f7da2a46530186e7883403c9bdde557b98d Mon Sep 17 00:00:00 2001 From: aitrawog Date: Tue, 6 Oct 2026 01:36:03 +0200 Subject: [PATCH 4/7] =?UTF-8?q?docs:=20correct=20the=20golangci-lint=20par?= =?UTF-8?q?ity=20claim=20=E2=80=94=20it=20was=20measurably=20false?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit charly.yml twice asserted that the packaged `golangci-lint` "is v2.13.2 — byte-for-byte the version charly's ci `go install`s, so CI and this image cannot drift apart on the linter". Both halves are wrong, and the measurement inverts the state: - the packaged Arch package is 2.14.0, not 2.13.2 (measured in the built image: `podman run --entrypoint golangci-lint ghcr.io/opencharly/githubrunner:check-githubrunner-pod-2026.278.2322 version`); - charly's `ci` never uses the packaged copy — its lint step runs `go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.2` and lints with that (ci.yml:97). So the two DO differ, and the packaged copy cannot decide a CI lint verdict. The honest reason to ship it is what it actually buys: a job can lint without paying a `go install` of its own. Corrected in both places — the package list and the skill's tool-surface paragraph. Verified: charly box validate -> OK, 0 warnings, 0 errors. --- charly.yml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/charly.yml b/charly.yml index 1343ed0..f933a14 100644 --- a/charly.yml +++ b/charly.yml @@ -87,9 +87,13 @@ github-runner: - shfmt # pairs with shellcheck — the org lints and formats shell - actionlint # 2 refs (action-review); workflow-YAML lint - golangci-lint # 30 refs across 7 repos (charly, sdk, spec, plugin-*); - # the repo's build is v2.13.2 — byte-for-byte the - # version charly's ci `go install`s, so CI and this - # image cannot drift apart on the linter + # so a job can lint without paying a `go install` of + # its own. Measured in the built image: 2.14.0 (Arch's + # current package). It is NOT the version CI's gate + # uses — charly's `ci` installs its own pinned + # `@v2.13.2` on every run (ci.yml:97), so the packaged + # copy never decides a CI lint result and cannot drift + # CI's verdict; it only spares jobs the install. - nodejs # 12 refs - npm # 22 refs (docs, pi-asciinema); nodejs does NOT ship npm - python-pip # `pip`/`pip3` were absent although python3 is present @@ -500,9 +504,10 @@ github-runner-skill: org's workflows actually call. Every entry below carries a `package:` check in the candy's own `plan:`, 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` (the packaged `v2.13.2` is byte-for-byte the - version charly's own `ci` installs, so CI and the image cannot drift apart on - the linter), `nodejs`, `npm`, `python-pip`, `uv`, `cmake`, `unzip`, `zip`, + `actionlint`, `golangci-lint` (so a job can lint without its own `go install`; + the packaged build measured 2.14.0, and it is not what CI's gate uses — that + installs its own pinned `@v2.13.2` each 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 From 543ca5c333a95ca8270a0b11ad83bc033005e1bc Mon Sep 17 00:00:00 2001 From: aitrawog Date: Tue, 6 Oct 2026 01:38:40 +0200 Subject: [PATCH 5/7] fix(config): drop the dead RUNNER_GROUP knob; declare the RUNNER_LABELS one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The skill entity's environment table — the table this branch extends with the `LANG`/`TZ` rows — documented two variables that did not match the image: * `RUNNER_GROUP | Default` was documented as part of the deploy contract, but the candy declares it in `env:` and NOTHING reads it — the registration hook passes `--name`/`--labels`/`--work`, never `--runnergroup`, and no other repo in the org sets or reads it either (`git grep RUNNER_GROUP` over the umbrella returns only the declaration and its copies of this doc). Wiring it would be an unprovable runtime change: `config.sh --runnergroup` is org-only, so it would break the user/repo-level registration the `RUNNER_ORG` description advertises, and the token-guarded hook cannot be exercised by a bed. So the declaration is removed rather than a doc written around it (R4a/R5). * `RUNNER_LABELS` was consumed (`--labels "${RUNNER_LABELS}"`) but declared nowhere, so an operator had no supported way to set the one value every self-hosted job selects on. It is now an `env_accept` entry next to `RUNNER_ORG`, and the table describes it instead of the dead knob. Behaviour is unchanged: the hook's `: "${RUNNER_LABELS:=opencharly}"` fallback still yields `opencharly` when nothing sets it, so the registered label set is identical to before. What changes is that the image now states truthfully which variables exist and what they do. Swept in this same commit (R5): `git grep RUNNER_GROUP` over the repo is empty (exit 1) — env declaration, skill table row and README row all gone. Verified: charly box validate -> OK, 0 warnings, 0 errors. --- README.md | 4 ++-- charly.yml | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 70353e6..931d59d 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ rebuilding does not change the runner version. | Service | `github-runner` (`~/actions-runner/run.sh`, `restart: always`, uid 1000) | | Requires | `layer-supervisord`, `layer-container-nesting` | | Volume | `state` at `~/actions-runner` | -| Env | `RUNNER_WORK_DIR=~/actions-runner/_work`, `RUNNER_GROUP=Default`, `LANG=C.UTF-8`, `TZ=UTC` | -| env_accept | `RUNNER_ORG` — the org/user to register with | +| Env | `RUNNER_WORK_DIR=~/actions-runner/_work`, `LANG=C.UTF-8`, `TZ=UTC` | +| env_accept | `RUNNER_ORG` — the org/user to register with; `RUNNER_LABELS` — the labels a job's `runs-on:` selects on (defaults to `opencharly`) | | secret_accept | `RUNNER_TOKEN` — the registration token (credential-store backed) | ## Registration diff --git a/charly.yml b/charly.yml index f933a14..ad083ae 100644 --- a/charly.yml +++ b/charly.yml @@ -118,7 +118,6 @@ github-runner: path: ~/actions-runner env: RUNNER_WORK_DIR: "${HOME}/actions-runner/_work" - RUNNER_GROUP: "Default" # UTF-8, because this image ships NO generated locale: `locale` reports LANG # empty and `locale -a` lists only C, C.utf8 and POSIX. Under the POSIX locale # Python 3 and other runtimes fall back to ASCII for the filesystem encoding, @@ -132,6 +131,8 @@ github-runner: env_accept: - name: RUNNER_ORG description: "GitHub org/user to register the runner with (e.g. opencharly). Pass via: charly config githubrunner -e RUNNER_ORG=" + - name: RUNNER_LABELS + description: "Labels the runner registers under (`config.sh --labels`), i.e. what a job's `runs-on:` selects on. The post_enable hook falls back to `opencharly` when unset. Pass via: charly config githubrunner -e RUNNER_LABELS=