Files
CIAgent 1a88ab3199
Release / release (push) Successful in 3m45s
CI / python (push) Successful in 1m52s
CI / node (push) Successful in 1m0s
merge: milestone/v0.8-ship-anywhere -> main (v0.8 complete — Ship Anywhere)
The v0.8 milestone: installable on any Debian-based server via docker
(Gitea registry image, honest sandbox degrade), deb (amd64+arm64,
advisory postinst), and arch-aware binary — with release consistency
enforced by pre-commit hooks + Gitea Actions (3-runner fleet on CT 113,
machine-made releases with the full 8-asset matrix + registry image), the
CLI carrying the full suite (guide, channel-aware doctor), and the admin
runbook (deploy/README.md, evidence-backed recipes).

---ci---
phase: 6
milestone: v0.8
status: complete
requirements:
  covered: [REQ-8-001, REQ-8-002, REQ-8-003, REQ-8-004, REQ-8-005]
  partial: []
---/ci---
2026-09-29 16:04:40 +00:00
..

CI runner fleet (CT 113) — bring-up runbook

The reproducible definition of the Gitea Actions runner fleet on CT 113 (gitea-runner LXC, 10.0.0.113). This directory is the source of truth (G-207): CT 113's /srv/nc-ci/ is a copy of these files — never hand-edited box memory. deploy/README.md (the admin runbook) references these files.

Contents

File Purpose
docker-compose.yaml Three sibling-mode runner services (nc-runner-node / nc-runner-python / nc-runner-release), image docker.io/gitea/runner:3, restart: always, host /var/run/docker.sock
config-runner-{node,python,release}.yaml Per-runner CONFIG_FILE: runner.capacity: 1, cache server (enabled, host 10.0.0.113, port 8088 published 8081/8082/8083 per runner), health_check.min_free_disk_space_mb: 4096
nc-ci-prune.cron Daily host cache-hygiene cron (docker system prune -af --filter until=168h + docker volume prune -f)

Label map (R-3 hybrid fleet)

Label Job image Carried by
node docker://node:24-bookworm nc-runner-node
ubuntu-latest docker://docker.gitea.com/runner-images:ubuntu-latest nc-runner-node
python docker://python:3.11-bookworm nc-runner-python
release docker://node:24-bookworm nc-runner-release

Fleet concurrency = 3 (capacity 1 per runner). Jobs whose runs-on matches no runner label still run (on the runner's default image) — always pin one of the four labels above in workflows.

Bring-up (fresh CT or reproduction)

  1. Copy this directory to the CT:

    scp -r deploy/ci/ root@10.0.0.113:/srv/nc-ci/
    
  2. Verify the runner config schema on the deployed 3.x minor (R-risk-9 — generate-config is the authority; adapt the three config-runner-*.yaml keys if renamed):

    docker run --rm docker.io/gitea/runner:3 gitea-runner generate-config > /tmp/gen-config.yaml
    
  3. Register and start — the registration token is passed via the shell environment ONLY (never committed, never in a file on disk; it is founder-provided from /org/coreci/settings/actions/runners):

    cd /srv/nc-ci
    export REGISTRATION_TOKEN='<founder-provided org registration token>'
    docker compose up -d
    unset REGISTRATION_TOKEN
    

    Each runner container registers itself on first start (.runner file lands in its own data-runner-*/ volume — never shared, two runners on one file refuse to start via advisory lock), then execs gitea-runner daemon.

  4. Install the prune cron (root crontab):

    crontab /srv/nc-ci/nc-ci-prune.cron
    

Bring-up variables

Variable Value Where
REGISTRATION_TOKEN founder-provided, session env only compose interpolation (${REGISTRATION_TOKEN})
CT IP (cache.host) 10.0.0.113 config-runner-*.yaml — if the CT IP changes, update all three

Verification (post bring-up)

  • docker compose ps — three containers Up.
  • .runner files exist: /srv/nc-ci/data-runner-{node,python,release}/.runner.
  • docker logs nc-runner-node 2>&1 | tail — daemon connected, labels declared.
  • Org runners page: /org/coreci/settings/actions/runners — all three visible + idle.
  • Cache ports reachable from a job container: 10.0.0.113:8081/8082/8083.

Secrets chain (G-205 outcome)

  • REGISTRY_TOKEN (Gitea Actions repo secret, consumed by P2's registry push): the self-serve PUT (PUT /api/v1/repos/coreci/nextcraft/actions/secrets/REGISTRY_TOKEN) SUCCEEDED on 2026-09-27 (HTTP 201). The value is the existing coreci-bot PAT — probe-verified it carries write:package + write:repository scope (the registry token-flow JWT lists both), so it satisfies the G-205 requirement (package-write). No founder escalation needed for P1. If the registry push in P2 hits 401/403 anyway, the G-205 escalation stands: founder mints a PAT with write:package + write:repository and either PUTs it or hands it to the orchestrator. The secret value is never committed, echoed, or logged.
  • GITEA_TOKEN (repo secret, release-asset ownership per D-064): not yet set — when set by the founder it also wins over the automatic job token in release.yaml (lowest-level-wins, R-13). The automatic job token's releases scope covers P1's asset-attach steps.

Notes

  • Reboot persistence: restart: always on all three services — the docker daemon (systemd) brings the compose stack back on CT reboot. Registration lives in the per-runner volumes; no re-registration needed.
  • Re-registration (new token) requires removing the old runner entries in the Gitea org UI and deleting data-runner-*/.runner before restart.
  • Accepted risk: sibling mode shares the host docker daemon with job containers (org-scoped private repos only — see PLAN R-21 note; the CT hosts nothing else).
  • Token lifecycle: after the fleet is green, the founder resets the registration token in the org UI. The token's job is done once the three .runner files exist.
  • Root access: first contact uses the founder-provided root password ONCE to install the repo's ed25519 deploy key into /root/.ssh/authorized_keys; the founder then rotates the root password (G-206 — Wave 1-1 completion condition). Note: on this CT template, sshd runs PermitRootLogin without-password (password auth policy-blocked for root) — the deploy key is the access path, and the initial key install must happen founder-side via the Proxmox console/pct exec (see Wave 1-1 report).