diff --git a/docs/impl/reports/CI-RUNNER.md b/docs/impl/reports/CI-RUNNER.md new file mode 100644 index 0000000..e240f47 --- /dev/null +++ b/docs/impl/reports/CI-RUNNER.md @@ -0,0 +1,49 @@ +--- +type: report +date: 2026-10-13 +--- +# CI runner: the development Orin builds the Rust services + +The development unit (`mikkeli-orin-nano-2`) runs the yukart org's Gitea Actions runner +for Rust: native builds on the car Orin's L4T userland, so a passing glibc check means +the binary loads on the car (project-yukart `docs/REPOS.md`, "The runner must build on +the host"). halogen keeps the org's other runner (crosscheck, the Flutter app). + +## What runs + +| | | +|---|---| +| Registration | `mikkeli-orin-nano-2-yukart`, org-scoped to yukart | +| Labels | `orin-nano-2` (the machine), `l4t-rust` (what workflows ask for); both `:host` | +| Service | `gitea-runner-yukart.service`, system service, `User=gitea-runner`, capacity 1 | +| Binary | `/usr/local/bin/gitea-runner` v3.3.0 linux-arm64, sha256-checked at install | +| Account | `gitea-runner`, system user, no password, HOME `/var/lib/gitea-runner` (0750), no extra groups | +| Toolchain | its own rustup (stable, minimal profile, clippy + rustfmt) under that HOME | +| Config | `/var/lib/gitea-runner/yukart/config.yaml`, registration `.runner` beside it | +| Build cache | `~gitea-runner/target/`, kept between runs | + +Host mode, because the sysroot is the point; a dedicated account, because in host mode +the account is the only sandbox a workflow has (not mikkeli: home, keys, sudo). + +## Install + +```bash +sudo ./scripts/setup-ci-runner.sh --check # pre-flight only +sudo RUNNER_TOKEN= ./scripts/setup-ci-runner.sh # idempotent +``` + +The token: `tea api -X POST /orgs/yukart/actions/runners/registration-token`. The +script needs `node` (JavaScript actions in host mode), `objdump` (check-glibc), `cc`, +`pkg-config`, and Gitea reachable at `http://pi5-16.local:3005`. + +## Workflows that use it + +`.gitea/workflows/rust.yml` in svc-core, svc-gw and svc-nav (identical): clone the +pushed commit with contracts beside it, then clippy with `-D warnings`, tests, a +release build, and `scripts/check-glibc.sh` on every binary. Plain git, no JavaScript +actions. It runs on changes to `src/`, `Cargo.*`, the glibc script or itself, not on +contracts changes. + +⚠ **Upgrade both Orins or neither.** This runner proves glibc parity only while its +L4T/Ubuntu release matches the car's (R36.4.7 here, R36.4.4 on the car, both glibc 2.35 +on 2026-10-13). diff --git a/scripts/setup-ci-runner.sh b/scripts/setup-ci-runner.sh new file mode 100755 index 0000000..563f382 --- /dev/null +++ b/scripts/setup-ci-runner.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash +# Provision the development Orin as the yukart org's Gitea Actions runner for Rust +# services: native L4T builds whose binaries load on the car Orin. +# +# sudo ./scripts/setup-ci-runner.sh --check # pre-flight only, changes nothing +# sudo RUNNER_TOKEN=xxxx ./scripts/setup-ci-runner.sh +# +# The token is ORG-scoped (yukart), from /org/yukart/settings/actions/runners +# or `POST /api/v1/orgs/yukart/actions/runners/registration-token`. +# Re-running is safe: every step checks before it changes anything, and a registered +# runner's config is left alone. +# +# Modelled on halogen's runners (android-stuff iperf3-gui tool/setup_ci_runner.sh): +# +# ── WHY HOST MODE ─────────────────────────────────────────────────────────── +# The point of building here is the sysroot: this unit runs the same L4T and +# Ubuntu release as the car Orin, so a binary cannot reference a glibc symbol the +# car lacks (project-yukart docs/REPOS.md). A container would build against +# whatever image the workflow names and throw that away. +# +# ── WHY A DEDICATED USER, WITH ITS OWN TOOLCHAIN ─────────────────────────── +# In host mode the service account is the only sandbox: a workflow is code and runs +# as this account. Not mikkeli (home, SSH keys, passwordless sudo), not root. Its +# own rustup under its own HOME, so CI never shares or drifts with an interactive +# install, and `rustup` can add components without shared-write tricks. + +set -euo pipefail + +VERSION="${RUNNER_VERSION:-3.3.0}" +SHA256_ARM64="${RUNNER_SHA256:-afdad29640ce9c2a6979f292e18313e14a22d39ca5f66bdd0add6480590b38b6}" +INSTANCE="${GITEA_INSTANCE:-http://pi5-16.local:3005}" +RUNNER_USER="${RUNNER_USER:-gitea-runner}" +RUNNER_HOME="${RUNNER_HOME:-/var/lib/gitea-runner}" +CONFIG_DIR="$RUNNER_HOME/yukart" +BIN="${RUNNER_BIN:-/usr/local/bin/gitea-runner}" +UNIT_NAME="gitea-runner-yukart.service" +UNIT="/etc/systemd/system/$UNIT_NAME" +# Machine-named registration, as halogen-yukart is. +RUNNER_NAME="${RUNNER_NAME:-$(hostname)-yukart}" +# One label for the machine, one for what it can do: workflows ask for the capability. +LABELS="${RUNNER_LABELS:-orin-nano-2:host,l4t-rust:host}" + +CHECK_ONLY=0 +[[ "${1:-}" == "--check" ]] && CHECK_ONLY=1 + +[[ $EUID -eq 0 ]] || { echo "Run with sudo: sudo RUNNER_TOKEN=... $0" >&2; exit 1; } +if [[ $CHECK_ONLY -eq 0 ]]; then + : "${RUNNER_TOKEN:?set RUNNER_TOKEN to a yukart org registration token (see header)}" +fi + +say() { printf '\n==> %s\n' "$1"; } + +# ── Pre-flight ────────────────────────────────────────────────────────────── +say "Pre-flight" +[[ "$(uname -m)" == aarch64 ]] || { echo "not aarch64: this is the wrong machine" >&2; exit 1; } +head -1 /etc/nv_tegra_release 2>/dev/null | sed 's/^/ /' || echo " (no /etc/nv_tegra_release)" +ldd --version | sed -n "1s/^/ /p" +missing=0 +# node runs the JavaScript actions (actions/checkout) in host mode; objdump is what +# scripts/check-glibc.sh reads; cc links, and builds C dependencies (zstd-sys). +for cmd in node git curl cc objdump pkg-config sha256sum; do + if PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin command -v "$cmd" >/dev/null; then + printf ' ok %s\n' "$cmd" + else + printf ' MISSING %s (not on systemd default PATH)\n' "$cmd" + missing=1 + fi +done +if ! curl -fsS -o /dev/null "$INSTANCE/api/v1/version"; then + printf ' MISSING %s unreachable\n' "$INSTANCE" + missing=1 +fi +if [[ $missing -ne 0 ]]; then + echo "Refusing to register: a runner advertising these labels must actually build." >&2 + exit 1 +fi +if [[ $CHECK_ONLY -eq 1 ]]; then + say "Pre-flight passed; nothing was created. Re-run with RUNNER_TOKEN set to provision." + exit 0 +fi + +# ── Service account ───────────────────────────────────────────────────────── +say "Service account: $RUNNER_USER" +if id "$RUNNER_USER" &>/dev/null; then + echo " exists" +else + # A shell, not nologin: host-mode steps run as shell scripts. No password, so + # the account is only reachable through root. + useradd --system --create-home --home-dir "$RUNNER_HOME" --shell /bin/bash \ + --comment "Gitea Actions runner (yukart, l4t-rust)" "$RUNNER_USER" + echo " created" +fi +install -d -o "$RUNNER_USER" -g "$RUNNER_USER" -m 0750 "$RUNNER_HOME" "$CONFIG_DIR" "$CONFIG_DIR/workspace" +id "$RUNNER_USER" + +# ── Rust toolchain, the account's own ────────────────────────────────────── +say "Rust toolchain for $RUNNER_USER" +as_runner() { sudo -u "$RUNNER_USER" env HOME="$RUNNER_HOME" CARGO_HOME="$RUNNER_HOME/.cargo" \ + RUSTUP_HOME="$RUNNER_HOME/.rustup" PATH="$RUNNER_HOME/.cargo/bin:/usr/bin:/bin" "$@"; } +if as_runner sh -c 'command -v rustup' >/dev/null 2>&1; then + as_runner rustup update stable --no-self-update | tail -1 +else + as_runner sh -c 'curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal --no-modify-path' +fi +as_runner rustup component add clippy rustfmt +as_runner rustc --version +as_runner cargo --version + +# ── Binary ────────────────────────────────────────────────────────────────── +say "gitea-runner $VERSION" +if [[ -x "$BIN" ]] && "$BIN" --version 2>/dev/null | grep -qF "$VERSION"; then + echo " already at $VERSION" +else + url="https://dl.gitea.com/gitea-runner/${VERSION}/gitea-runner-${VERSION}-linux-arm64" + echo " $url" + curl -fSL --retry 3 -o "$BIN.tmp" "$url" + echo "$SHA256_ARM64 $BIN.tmp" | sha256sum -c --quiet || { rm -f "$BIN.tmp"; echo "checksum mismatch" >&2; exit 1; } + chmod 0755 "$BIN.tmp" + mv "$BIN.tmp" "$BIN" +fi +"$BIN" --version + +# ── Config and registration ───────────────────────────────────────────────── +say "Config: $CONFIG_DIR/config.yaml" +CONFIG="$CONFIG_DIR/config.yaml" +if [[ -f "$CONFIG_DIR/.runner" ]]; then + echo " registered already — leaving config and registration alone" +else + yaml_labels=$(tr ',' '\n' <<<"$LABELS" | sed 's/.*/ - "&"/') + cat > "$CONFIG" < "$UNIT" <