Skip to content

fix(image): give the runner image hostname(1), and the tool surface the org's CI jobs actually call - #8

Merged
github-actions[bot] merged 7 commits into
mainfrom
feat/hostname-inetutils
Oct 6, 2026
Merged

github-actions[bot] merged 7 commits into
mainfrom
feat/hostname-inetutils

Conversation

@aitrawog

@aitrawog aitrawog commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #7. check-githubrunner-pod builds ghcr.io/opencharly/githubrunner — the image the
org's self-hosted runners execute jobs in. Three defects in it: two break the pool, one makes
the image lie about itself.

1. The image had no hostname(1), so charly's own test-suite cannot pass on the pool

Verbatim from the go check-run of charly#807
(job 111961186574,
started 2026-10-05T20:19:41Z, completed 2026-10-05T20:27:01Z — 440 s, inside the required
check's 600 s budget, so this is a real test failure and not a duration stall):

--- FAIL: TestInvokeProvider_VenueDescriptor_Venueless_ShellDescriptor (0.97s)
    plugin_dispatch_reverse_venue_test.go:155: venue-less + shell descriptor peer result: {Status:pass Message:peer-executed Stdout: Stderr:bash: line 1: hostname: command not found
         Exit:127}
    plugin_dispatch_reverse_venue_test.go:161: peer executed on the WRONG host: got "" want "charly-githubrunner-no-2" (host-local materialization failed)

charly's venue descriptor shells out to hostname to prove a peer ran host-locally. The
runner image carried coreutils, but Arch's coreutils ships no hostname — it lives in
inetutils. The test therefore fails for the image, not for the code under test, and every
repo whose CI runs go test ./... on the pool inherits the red.

The fix is the package plus the check that would fail without it:

- check: hostname(1) resolves on PATH, so charly's venue tests can prove a peer ran host-locally
  id: gr-hostname

2. The image was missing most of the tool surface a CI job reaches for

24 further packages, each with its own package=<name> check in the plan, so a
declared-but-missing tool fails the image build rather than a job at 02:00: tree shellcheck
shfmt actionlint golangci-lint nodejs npm python-pip uv cmake unzip zip
7zip wget rsync yq ripgrep moreutils bc less lsof strace openbsd-netcat
bind, plus LANG=C.UTF-8 / TZ=UTC with the two checks that assert them.

None of these names existed in the candy before — measured against origin/main:

$ for p in inetutils tree shellcheck actionlint nodejs ripgrep; do
    printf '%-12s origin/main=%s  HEAD=%s\n' "$p" \
      "$(git show origin/main:charly.yml | grep -c -E "^\s+- $p\b")" \
      "$(grep -c -E "^\s+- $p\b" charly.yml)"; done
inetutils    origin/main=0  HEAD=1
tree         origin/main=0  HEAD=1
shellcheck   origin/main=0  HEAD=1
actionlint   origin/main=0  HEAD=1
nodejs       origin/main=0  HEAD=1
ripgrep      origin/main=0  HEAD=1

My first draft of this PR justified LANG=C.UTF-8 by claiming "Python 3 falls back to ASCII
as the filesystem encoding, so a job that touches a non-ASCII path fails". Probing the built
image disproved it — CPython ≥ 3.7 coerces the C locale itself — so the claim is gone from
the candy, its skill entity and this body rather than shipped (R1). What the probes did show,
with LANG unset:

$ podman run --rm --entrypoint env ghcr.io/opencharly/githubrunner:check-githubrunner-pod-2026.279.0009 -u LANG -u LC_ALL bash -c \
    'locale -a; locale charmap; python3 -c "import locale,sys;print(locale.getpreferredencoding(False), sys.getfilesystemencoding())"'
C
C.utf8
POSIX
ANSI_X3.4-1968
utf-8 utf-8

So: the image generates no locale, a non-login process lands in POSIX and its codeset is
ASCII, and Python is emphatically not the victim. The image already has /etc/locale.conf
naming C.UTF-8, but that reaches only login/PAM sessions, and a charly check and a job
process are neither — the env: declaration is what applies, which is precisely why the
locale charmap check genuinely fails when it is removed. C.UTF-8 is built into glibc, so
nothing needs generating. The TZ check asserts the variable (printenv TZ), deliberately
not date +%Z: the image's
/etc/localtime is already UTC, so a date-based check would pass either way and prove
nothing.

3. The skill entity documented a knob that does nothing, and omitted the one jobs select on

The | Variable | Mechanism | table in this candy's own skill entity carried
| RUNNER_GROUP | Default |. The candy declares RUNNER_GROUP in env:, and nothing reads
it
: the registration hook passes --name/--labels/--work and 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 copies of this table. Its mirror image is RUNNER_LABELS,
which the hook does consume (--labels "${RUNNER_LABELS}") but which was declared nowhere,
so the one value every self-hosted job selects on had no supported way of being set.

Because this PR extends that very table with the LANG/TZ rows, the dead row is this PR's to
cure rather than something to build on top of (R2, R5, and the reviewer's B14(c)).
RUNNER_GROUP is deleted — env declaration, skill-table row and README row, in this commit.
Wiring it instead would have been an unprovable runtime change: config.sh --runnergroup is
org-only, so consuming it would break the user/repo-level registration the RUNNER_ORG
description advertises, and the token-guarded hook can never be exercised by a bed.
RUNNER_LABELS becomes an env_accept entry beside RUNNER_ORG.

This deviates from the plan I filed on #7, on purpose. #7 predicted the fix would need its
own PR after this one, on the grounds that a registration-hook change is not bed-observable.
Deleting a dead env: entry and declaring a live one is not a hook change — the hook is
untouched, byte-identical — and it is bed-observable, because both halves show up in the
built image's labels, which the R8 read-back below reads from the real artefact. Folding it in
is also what R2 requires: this PR edits the very table that carried the dead row, so a separate
PR would be parking a defect this one is standing on. #7 is updated with a STATUS saying so,
and it closes with this PR.

Behaviour is unchanged: the hook's : "${RUNNER_LABELS:=opencharly}" fallback still yields
opencharly when nothing sets it, so the registered label set is byte-identical to before.

$ git grep -n RUNNER_GROUP -- . ; echo "grep_exit=$?"
grep_exit=1
$ git grep -n RUNNER_LABELS -- .
README.md:33:| env_accept | `RUNNER_ORG` — the org/user to register with; `RUNNER_LABELS` — the labels a job's `runs-on:` selects on (defaults to `opencharly`) |
charly.yml:139:            - name: RUNNER_LABELS
charly.yml:140:              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=<label>[,<label>]"
charly.yml:167:                : "${RUNNER_LABELS:=opencharly}"
charly.yml:172:                  --labels "${RUNNER_LABELS}" \
charly.yml:538:            | `RUNNER_LABELS` | `env_accept` — the labels a job's `runs-on:` selects on; the hook defaults to `opencharly` |

Files changed

$ git diff --numstat origin/main...HEAD
16	5	AGENTS.md
19	9	README.md
309	4	charly.yml
$ git diff --shortstat origin/main...HEAD
 3 files changed, 344 insertions(+), 18 deletions(-)

charly.yml is the fix (packages, env, 28 plan checks, and the skill entity's text).
README.md and AGENTS.md are the same repo's own descriptions of the image, corrected to
match it: the README called RUNNER_VERSION a pin and listed RUNNER_GROUP in its Env row,
and AGENTS.md repeated the pin framing — both now say the runner self-updates and that the
state volume carries the update across rebuilds.

A third correction was to this branch itself. A sentence I first wrote into charly.yml
claimed the packaged golangci-lint "is v2.13.2, byte-for-byte the version charly's ci go installs, so CI and this image cannot drift apart on the linter". Probing the built image
disproved both halves — the packaged build is 2.14.0, and charly's lint step installs its
own pinned @v2.13.2 and lints with that — so the sentence was rewritten inside this branch
before this PR opened. What the diff asserts is a measurement, not a parity claim:

$ podman run --rm --entrypoint golangci-lint ghcr.io/opencharly/githubrunner:check-githubrunner-pod-2026.279.0009 version
golangci-lint has version 2.14.0 built with go1.27.1-X:nodwarf5 from 114493f9b3e7257d29e4130f2b4a4aadefbb6845 on 2026-09-30T18:46:45Z
$ grep -n 'golangci-lint@\|name: golangci' charly/.github/workflows/ci.yml
90:      - name: golangci-lint
92:          go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.2

The honest reason to ship the package is what it buys: a job can lint without paying a go install of its own.

The env_candy line below is the "before" for the R8 read-back in the next section — this
branch's first head, so it still declares RUNNER_GROUP while LANG is already applied:

$ charly box labels ghcr.io/opencharly/githubrunner:check-githubrunner-pod-2026.278.2322 | grep -m1 env_candy
ai.opencharly.env_candy={"BUILDAH_ISOLATION":"chroot","CHARLY_BUILD_ENGINE":"podman","CHARLY_RUN_ENGINE":"podman","DBUS_SESSION_BUS_ADDRESS":"unix:path=/tmp/dbus-session","EXAMPLEDEPLOY_STEP_ENV":"present","LANG":"C.UTF-8","RUNNER_GROUP":"Default","RUNNER_WORK_DIR":"${HOME}/actions-runner/_work","TZ":"UTC","XDG_RUNTIME_DIR":"/tmp/xdg-runtime","_CONTAINERS_USERNS_CONFIGURED":""}

How tested

Change class gate — candy config. The owning repo's rulebook names two gates: charly box validate for the config itself, and a composing box's check bed for the live behaviour. Both
were run on the final tree.

charly box validate (repo root, final tree 49822b8):

$ charly box validate
charly box validate: OK — checked 5 candies, 7 distros, 7 builders; 0 warnings, 0 errors

The bed — charly check run check-githubrunner-pod, a disposable: true pod bed declared
in distro-cachyos/charly.yml, on the final tree. It composes this candy into the runner image
via CHARLY_REPO_OVERRIDE, so the run builds the branch's bytes, not main's:

$ CHARLY_PLUGIN_ONLY=1 \
  CHARLY_REPO_OVERRIDE="github.com/opencharly/pod-github-runner=$PWD" \
    charly check run check-githubrunner-pod
charly check run check-githubrunner-pod: [image-build] PASS after 2m3.046s [cache=176/223] (log: .check/check-githubrunner-pod/2026.279.0009/image-build.log)
charly check run check-githubrunner-pod: [check-image] PASS after 1m33.18s (log: .check/check-githubrunner-pod/2026.279.0009/check-image.log)
charly check run check-githubrunner-pod: [deploy-add] PASS after 11.186s (log: .check/check-githubrunner-pod/2026.279.0009/deploy-add.log)
charly check run check-githubrunner-pod: [config] PASS after 19.91s (log: .check/check-githubrunner-pod/2026.279.0009/config.log)
charly check run check-githubrunner-pod: [start] PASS after 5.97s (log: .check/check-githubrunner-pod/2026.279.0009/start.log)
charly check run check-githubrunner-pod: [bring-up-members] PASS after 124ms
charly check run check-githubrunner-pod: [check-live] PASS after 1m21.536s (log: .check/check-githubrunner-pod/2026.279.0009/check-live.log)
charly check run check-githubrunner-pod: [feature-run] PASS after 1m22.506s (log: .check/check-githubrunner-pod/2026.279.0009/feature-run.log)
charly check run check-githubrunner-pod: [update] PASS after 47.027s (log: .check/check-githubrunner-pod/2026.279.0009/update.log)
charly check run check-githubrunner-pod: [rebuild-ready] PASS after 5.486s
charly check run check-githubrunner-pod: [check-live-rebuild] PASS after 1m19.001s (log: .check/check-githubrunner-pod/2026.279.0009/check-live-rebuild.log)
charly check run check-githubrunner-pod: [feature-run-rebuild] PASS after 1m16.992s (log: .check/check-githubrunner-pod/2026.279.0009/feature-run-rebuild.log)
charly check run check-githubrunner-pod: [cleanup] PASS after 24.329s (log: .check/check-githubrunner-pod/2026.279.0009/cleanup.log)
charly check run check-githubrunner-pod: [cleanup-members] PASS after 0s

The override really was honoured — proven by check ids, not by a log line. The LOCAL OVERRIDE line is printed for a plugin: load but not for a box-candy load, so the proof is
that 28 check ids exist only on this branch:

$ git show origin/main:charly.yml | grep -c -E 'gr-(hostname|inetutils-pkg|ci-)'
0
$ grep -c -E 'gr-(hostname|inetutils-pkg|ci-)' charly.yml
28
$ git log --oneline -S'gr-hostname' origin/main..HEAD
fa35ea8 fix(deps): give the runner image hostname(1) — and the tool surface a CI job needs

All 28 pass at every bed step that can run them — check-image 26 PASS / 2 SKIP (the two
context: runtime checks are correctly skipped in box mode rather than silently passed),
check-live 28 PASS, feature-run 28 PASS, and the same 28 PASS again after charly update
in check-live-rebuild and feature-run-rebuild. Zero FAIL in any step. As check-live
printed them on the deployed pod, where the runtime context is active:

  PASS  check hostname(1) resolves on PATH, so charly's venue tests can prove a peer ran host-locally  [gr-hostname]  exit=0
  PASS  check package=inetutils  [gr-inetutils-pkg]  installed
  PASS  check package=tree  [gr-ci-tree]  installed
  PASS  check package=shellcheck  [gr-ci-shellcheck]  installed
  PASS  check package=shfmt  [gr-ci-shfmt]  installed
  PASS  check package=actionlint  [gr-ci-actionlint]  installed
  PASS  check package=golangci-lint  [gr-ci-golangci-lint]  installed
  PASS  check package=nodejs  [gr-ci-nodejs]  installed
  PASS  check package=npm  [gr-ci-npm]  installed
  PASS  check package=python-pip  [gr-ci-python-pip]  installed
  PASS  check package=uv  [gr-ci-uv]  installed
  PASS  check package=cmake  [gr-ci-cmake]  installed
  PASS  check package=unzip  [gr-ci-unzip]  installed
  PASS  check package=zip  [gr-ci-zip]  installed
  PASS  check package=7zip  [gr-ci-7zip]  installed
  PASS  check package=wget  [gr-ci-wget]  installed
  PASS  check package=rsync  [gr-ci-rsync]  installed
  PASS  check package=yq  [gr-ci-yq]  installed
  PASS  check package=ripgrep  [gr-ci-ripgrep]  installed
  PASS  check package=moreutils  [gr-ci-moreutils]  installed
  PASS  check package=bc  [gr-ci-bc]  installed
  PASS  check package=less  [gr-ci-less]  installed
  PASS  check package=lsof  [gr-ci-lsof]  installed
  PASS  check package=strace  [gr-ci-strace]  installed
  PASS  check package=openbsd-netcat  [gr-ci-openbsd-netcat]  installed
  PASS  check package=bind  [gr-ci-bind]  installed
  PASS  check the container's job environment runs with a UTF-8 locale, not the base image's ASCII POSIX default  [gr-ci-utf8-locale]  exit=0
  PASS  check TZ is set explicitly, so job-log timestamps do not depend on the base image's /etc/localtime  [gr-ci-tz-utc]  exit=0

Working tree at the head being pushed — the git status half of R6, run after the last
commit, and the tree the gates above ran against:

$ git status --porcelain; echo "porcelain_exit=$? (no output above = clean tree)"; git rev-parse HEAD
porcelain_exit=0
49822b815d221061a774454c8a9da84fc00229a4

R8 — generated artefact labels verified after build

The change alters a candy's packages:/env:, which changes emitted Containerfile sections
and the ai.opencharly.* labels. Both were read back from the built image with
charly box labels, and both halves move exactly as the diff says. RUNNER_GROUP disappears
from env_candy; RUNNER_LABELS appears in env_accept with the description from the diff;
no ai.opencharly.* label is empty or missing.

Before: this branch's image from before the knob fix
(ghcr.io/opencharly/githubrunner:check-githubrunner-pod-2026.278.2322, head 87bda93) —
env_accept carried only RUNNER_ORG. After: ghcr.io/opencharly/githubrunner:check-githubrunner-pod-2026.279.0009, built by the run above:

$ charly box labels ghcr.io/opencharly/githubrunner:check-githubrunner-pod-2026.279.0009 | grep -E 'env_accept|env_candy'
ai.opencharly.env_accept=[{"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=\u003clabel\u003e[,\u003clabel\u003e]"},{"name":"RUNNER_ORG","description":"GitHub org/user to register the runner with (e.g. opencharly). Pass via: charly config githubrunner -e RUNNER_ORG=\u003corg\u003e"}]
ai.opencharly.env_candy={"BUILDAH_ISOLATION":"chroot","CHARLY_BUILD_ENGINE":"podman","CHARLY_RUN_ENGINE":"podman","DBUS_SESSION_BUS_ADDRESS":"unix:path=/tmp/dbus-session","EXAMPLEDEPLOY_STEP_ENV":"present","LANG":"C.UTF-8","RUNNER_WORK_DIR":"${HOME}/actions-runner/_work","TZ":"UTC","XDG_RUNTIME_DIR":"/tmp/xdg-runtime","_CONTAINERS_USERNS_CONFIGURED":""}

R9 — target exercise

The artefact exercised here is the box image, not the charly binary; the bed rebuilds it from
source on every step that needs it (image-build, then charly update + check-live-rebuild),
and the branch-only check ids pasted in ## How tested above are that rebuild's own output.
Every new runtime OS dependency this PR adds is declared in the candy that installs it — this
candy's packages: list, the file the diff edits; none is a charly runtime dependency, so the
charly candy's packaging: section is untouched by design.

Review round 1 — BLOCK, all three findings addressed

Run 37392501882 (check-run 112040633514) blocked with three findings. All three were real,
and two of them were defects in the diff itself rather than in the prose around it. Each is
fixed at the head this body describes; the second one moved the head, which is why the bed
below was re-run against the new tree rather than reused.

  1. The "Files changed" numbers were stale. The body pasted 300 4 charly.yml and
    335 insertions(+), 18 deletions(-), both measured against the previous head. Re-run at
    the head this body describes:

    $ git diff --numstat origin/main...HEAD
    16	5	AGENTS.md
    19	9	README.md
    309	4	charly.yml
    $ git diff --shortstat origin/main...HEAD
     3 files changed, 344 insertions(+), 18 deletions(-)
    

    The same staleness had moved the git grep RUNNER_LABELS line numbers, corrected in place
    in ## Summary above. They are pasted from a frozen tree now, not from the tree as it was
    mid-edit.

  2. charly.yml cited a ci.yml line that does not exist. The golangci-lint package
    comment pointed at ci.yml:97; the step and its go install …@v2.13.2 are at :90 and
    :92. A comment diverging from the file it cites is an incident, not a typo (R1), so it is
    fixed in this head — and the citation now names the step as well, so it stays checkable when
    the line number moves:

    $ grep -n 'ci.yml:97' charly.yml ; echo "grep_exit=$?"
    grep_exit=1
    
  3. "no version number is asserted anywhere in the diff" was false. The diff does assert
    2.14.0 and @v2.13.2. The sentence now says what it meant — no version-number parity is
    asserted, and none ever was after the correction above.

Rulebook compliance

  • R0 — loaded before editing: /charly-image:image (the charly box family, candy/box
    authoring, the OCI-label contract R8 checks), /charly-internals:git-workflow (branch/PR
    loop, body-before-push, no-force-push), /charly-check:check (bed semantics, R10), plus the
    owning repo's own AGENTS.md, which is the rulebook in force inside pod-github-runner.
    The change follows the image skill's authoring rule (a candy: node carrying base:; no
    box: kind) and its validate gate.

  • R1 — the failure is root-caused, not framed: hostname missing from the image →
    exit 127 → the venue descriptor's host-local proof fails → the pool's go job is red for
    every repo. The 440 s duration rules out a timeout as the cause. Three claims of mine died
    the same way — probed against the built image, found false, rewritten rather than shipped:
    the golangci-lint "v2.13.2 parity" sentence, the RUNNER_GROUP knob that nothing reads,
    and the LANG rationale's Python story. None is left as an exception or a note; the text
    now says what the probes showed. charly box validate reports 0 warnings, 0 errors on
    the final tree, pasted above.

  • R2 — nothing surfaced is parked. The RUNNER_GROUP/RUNNER_LABELS divergence is fixed in
    this commit, not routed to a follow-up, precisely because this PR extends the table that
    carried it. No "pre-existing"/"out of scope"/"follow-up PR" framing appears anywhere above.

  • R3 — no duplication: the new plan checks are one per package, and the CI tool surface is
    declared once in the candy rather than restated in each consuming workflow. The container
    runtime stack stays where it was declared — layer-container-nesting owns it, and this candy
    declares none of it:

    $ comm -12 <(awk '/^ {20}- /{print $2}' charly.yml | sort -u) \
        <(awk '/^ *package:/{f=1;next} f&&/^ *[a-z-]+:/{f=0} f&&/^ *- /{sub(/^ *- */,"");print}' \
            ~/.cache/charly/repos/github.com/opencharly/layer-container-nesting@v2026.271.2309/charly.yml | sort -u)
    

    (empty — 41 packages declared here, 13 there, none in both)

  • R4 — no workaround anywhere in the diff: the missing tool is installed at the image layer
    that owns it, not shimmed into job scripts, and no check is retried or slept through. The
    documentation describes the image that now exists rather than the image that was wanted
    (R4a).

  • R5 — hard cutover: RUNNER_GROUP is deleted from all three places that named it (env,
    skill table, README) in the same commit; no transitional or dual-mode path is left behind;
    the grep evidence that the sweep is complete is pasted above.

  • R6 — git status was run before every commit and again after the last one; the clean
    tree at the head being pushed is pasted above. No force-push and no hook bypass was used at
    any point on this branch.

  • R7 — the gates this diff is in were run on the final tree and their real output is pasted
    above: charly box validate (the candy gate) and the composing box's disposable: true
    check bed. No gate outside this change class is claimed.

  • R8 — generated-artefact labels verified after build; see the ## R8 section above.

  • R9 — target exercise asserted; see the ## R9 section above.

  • R10 — the proof is a disposable: true bed on the final committed tree, with a fresh
    rebuild and zero warnings; the change ships the check coverage that fails without it (28 ids,
    all absent from origin/main). Attribution is fully tested and validated and no higher.

  • group / naming rules — untouched; the diff adds no new entity and renames nothing.

  • Ledger — the session ledger carries this PR, the bed run and its verdict.

Change classification

  • Change class: candy config + the same repo's docs. charly.yml (packages, env, plan,
    skill text), README.md, AGENTS.md — no Go, no schema, no CI workflow, no pin.
  • Verification gate: the owning repo's own gates — charly box validate plus a composing
    box's disposable: true check bed (check-githubrunner-pod), both on the final tree.
  • Attribution tier: fully tested and validated — the changed paths (image packages, env,
    the 28 new plan checks) executed against a fresh rebuild in the bed, and the label read-back
    confirms the built artefact carries the change. The env_accept/RUNNER_GROUP edit is inert
    config, covered by validate plus the label read-back rather than by a runtime path, because
    the registration hook it feeds is token-guarded and cannot be exercised by a disposable bed.
  • Generated copies, not deferred work: marketplace/distros/skills/github-runner/SKILL.md
    and the docs site carry copies of this candy's skill entity, dead RUNNER_GROUP row
    included. They are generated artefacts re-emitted by charly marketplace generate and the
    docs regen and are not hand-editable, so they cannot be fixed here — this candy is their
    source, and they follow from it. Nothing found was parked.

Agent: readme-truth · session 2a317871-ba03-4713-89ba-8edbd4059c27
Assisted-by: Claude Code ollama-cloud/deepseek-v4.1-flash (fully tested and validated)

… CI job needs

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.
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.
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.
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.
…LS one

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.
…nt disproves

The `LANG` entry this branch adds carried a justification that does not survive
contact with the image it describes. It said: "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."

Measured in the built image, with LANG unset:

  $ podman run --rm --entrypoint env <img> -u LANG -u LC_ALL bash -c \
      'locale charmap; python3 -c "import locale,sys;print(locale.getpreferredencoding(False), sys.getfilesystemencoding())"'
  ANSI_X3.4-1968
  UTF-8 utf-8

CPython >= 3.7 coerces the C locale itself (PEP 538/540), so Python never falls
back to ASCII here and the failure story it was used to justify cannot be
reproduced. An unprovable consequence dressed as a fact is exactly what R1
forbids, so it is replaced with what the probes actually showed:

  * `locale -a` lists only C, C.utf8, POSIX — true, and kept;
  * under the default POSIX locale a non-login process's codeset is ASCII,
    `locale charmap` -> ANSI_X3.4-1968 — true, and kept;
  * /etc/locale.conf already names C.UTF-8, but that reaches only login/PAM
    sessions, while a charly `check` and a job process are neither — so the
    `env:` declaration is what applies, which is stated now;
  * CPython coerces, so "Python breaks" is explicitly NOT the justification.

The check description loses the same clause ("so jobs never fall back to ASCII
filesystem encoding") and states the locale the tools see instead. The check
itself is unchanged and still fails without the env entry.

Verified: charly box validate -> OK, 0 warnings, 0 errors.
…ly exists

The `golangci-lint` package comment added earlier on this branch pointed at
`ci.yml:97` for charly's own pinned install. There is nothing at :97. The step
and its install are at :90 and :92:

  $ grep -n 'golangci-lint@\|name: golangci' charly/.github/workflows/ci.yml
  90:      - name: golangci-lint
  92:          go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.2

A comment that diverges from the file it cites is an incident, not a typo (R1),
so it is corrected here — and the citation now also names the step, so the claim
stays checkable when the line number moves:

  charly's `ci` `golangci-lint` step installs its own pinned `@v2.13.2` on every
  run (ci.yml:92)

Verified: charly box validate -> OK, 0 warnings, 0 errors.
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

Review — BLOCK

Head SHA: b78f3651f714dc336d2c9310c75a1e7dba866e8e (branch: feat/hostname-inetutils)

Change class: candy config (charly.yml) + the same repo's docs (README.md, AGENTS.md); no Go/schema/CI. Placement is correct (candy layer — the capability belongs here, not in core/sdk). The security screen is clean; the blocks below are body/evidence-truthfulness defects that reproduce against the current state.

Blocks

  1. A1 / rule 4 — the body's "Files changed" evidence does not match the current diff. The body pastes git diff --numstat origin/main...HEAD as 300 4 charly.yml and git diff --shortstat as 3 files changed, 335 insertions(+), 18 deletions(-). The current diff is charly.yml +308/-4, and the total is 16+19+308 = 343 insertions / 18 deletions (AGENTS.md and README.md counts do match, so the counting method is the same). The pasted counts predate the final commit b78f365 (which rewrote the LANG rationale in charly.yml), i.e. they are stale relative to the HEAD the body claims. Re-run both commands at b78f365 and paste the real numbers.

  2. R1 / B5 — the diff cites a ci.yml line that the body's own pasted grep contradicts. In charly.yml (the golangci-lint package entry) the comment reads … charly's \ci` installs its own pinned `@v2.13.2` on every run (ci.yml:97)`. The body's own evidence shows:

    $ grep -n 'golangci-lint@\|name: golangci' charly/.github/workflows/ci.yml
    90:      - name: golangci-lint
    92:          go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.2
    

    The go install …@v2.13.2 is at line 92, not 97. One of the two is wrong; fix the comment to cite the real line (a comment diverging from reality is an incident under R1).

  3. A1 / rule 4 — the body contradicts the diff on asserted version numbers. The body states "…and no version number is asserted anywhere in the diff", but the diff itself asserts version numbers: Measured in the built image: 2.14.0 (Arch's current package) and charly's \ci` installs its own pinned `@v2.13.2` … (ci.yml:97)`. Reword the claim (e.g. "no version-number parity is asserted") or drop it, so the body does not assert something the diff visibly falsifies.

Verdict: BLOCK


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

Review — PASS

Head SHA: 49822b815d221061a774454c8a9da84fc00229a4 (branch: feat/hostname-inetutils)

Security screen: PASS — no secrets/egress, no guardrail weakening, no scope mismatch; the diff is candy config + the same repo's docs, and the skill/var/env text is data, not instructions to me.
Checklist: All applicable rules PASS or NA. Change class is candy config + docs; the owning-repo gates (charly box validate + the disposable: true check-githubrunner-pod bed) are named and pasted on the final tree; the 28 new check ids are shown absent from origin/main (a check that fails without the change); R8 label read-back moves RUNNER_GROUP→gone and RUNNER_LABELS→env_accept exactly as the diff says; attribution fully tested and validated is justified by the rebuilt-and-exercised paths; body ends with the required Agent: + Assisted-by: lines in order.

Comment intake (1 comment, BLOCK by @github-actions[bot] at head b78f365):

  • Finding 1 (stale numstat) — stale-superseded: current body pastes 309 4 charly.yml / 344 insertions(+), 18 deletions(-), and the file headers are +16/-5, +19/-9, +309/-4 → 344/18. Matches.
  • Finding 2 (ci.yml:97 does not exist) — stale-superseded: the diff now cites ci.yml:92, matching the body's own grep (92: go install …@v2.13.2).
  • Finding 3 ("no version number is asserted" was false) — stale-superseded: the body now says no version-number parity is asserted, which the diff supports (it asserts a measurement 2.14.0 and charly's pin @v2.13.2, not parity).

All three reproduce against the prior head only; none survives against 49822b8.

Verdict: PASS


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

@github-actions
github-actions Bot enabled auto-merge (squash) October 6, 2026 00:27
@github-actions
github-actions Bot merged commit 297c9ea into main Oct 6, 2026
1 check passed
charly-auto-merge Bot added a commit that referenced this pull request Oct 6, 2026
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.

RUNNER_GROUP is declared but never consumed; RUNNER_LABELS is consumed but undeclared

1 participant