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..931d59d 100644 --- a/README.md +++ b/README.md @@ -6,21 +6,31 @@ 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_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 7418b0f..47d06e8 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,19 +56,106 @@ 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-*); + # 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` `golangci-lint` step installs its + # own pinned `@v2.13.2` on every run (ci.yml:92), 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 + - 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, so under the default + # POSIX locale a process's codeset is ASCII (`locale charmap` reports + # ANSI_X3.4-1968). GitHub-hosted runners run UTF-8; this is that parity, and + # C.UTF-8 is built into glibc so nothing needs generating. Measured caveats: + # /etc/locale.conf already carries LANG=C.UTF-8, but that only reaches + # login/PAM sessions — a charly `check` or a job process is neither, so the + # declaration here is what actually takes effect. CPython >= 3.7 coerces the + # C locale itself and reports utf-8 either way, so "Python breaks" is NOT the + # justification; the locale the tools see is. + 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=" + - 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=