Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
This commit is contained in:
@@ -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/<repo>`, 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=<org 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).
|
||||
Executable
+185
@@ -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 <gitea>/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" <<YAML
|
||||
log:
|
||||
level: info
|
||||
|
||||
runner:
|
||||
# Absolute: register resolves a relative path against the caller's directory.
|
||||
file: ${CONFIG_DIR}/.runner
|
||||
# One job at a time: 6 cores and 8 GB, and a release build of a service uses them all.
|
||||
capacity: 1
|
||||
timeout: 45m
|
||||
# ':host' runs steps on this machine as ${RUNNER_USER}: the L4T sysroot is the point.
|
||||
labels:
|
||||
${yaml_labels}
|
||||
|
||||
container:
|
||||
network: ""
|
||||
privileged: false
|
||||
|
||||
host:
|
||||
workdir_parent: ${CONFIG_DIR}/workspace
|
||||
YAML
|
||||
chown "$RUNNER_USER:$RUNNER_USER" "$CONFIG"
|
||||
sudo -u "$RUNNER_USER" env HOME="$RUNNER_HOME" "$BIN" register --no-interactive \
|
||||
--config "$CONFIG" --instance "$INSTANCE" --token "$RUNNER_TOKEN" \
|
||||
--name "$RUNNER_NAME" --labels "$LABELS"
|
||||
fi
|
||||
|
||||
# ── Service ─────────────────────────────────────────────────────────────────
|
||||
say "Service: $UNIT_NAME"
|
||||
cat > "$UNIT" <<UNIT
|
||||
[Unit]
|
||||
Description=Gitea Actions runner (l4t-rust for the yukart org, host mode)
|
||||
Documentation=https://gitea.com/gitea/runner
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=${RUNNER_USER}
|
||||
Group=${RUNNER_USER}
|
||||
WorkingDirectory=${CONFIG_DIR}
|
||||
ExecStart=${BIN} daemon --config ${CONFIG}
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=HOME=${RUNNER_HOME}
|
||||
Environment=CARGO_HOME=${RUNNER_HOME}/.cargo
|
||||
Environment=RUSTUP_HOME=${RUNNER_HOME}/.rustup
|
||||
Environment=PATH=${RUNNER_HOME}/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now "$UNIT_NAME"
|
||||
sleep 2
|
||||
systemctl --no-pager --lines=5 status "$UNIT_NAME"
|
||||
Reference in New Issue
Block a user