diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8fc6ae51..73659bb32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,6 +114,8 @@ jobs: run: python3 scripts/test-check-kickoff.py - name: scripts/agent-prepare, agent-finish run: bash scripts/test-agent-work.sh + - name: scripts/runner-setup.sh (private-repo check) + run: sh scripts/test-runner-setup.sh test: name: Tests (${{ matrix.name }}) diff --git a/docs/AGENT_WORK.md b/docs/AGENT_WORK.md index 493c9f926..d590ced97 100644 --- a/docs/AGENT_WORK.md +++ b/docs/AGENT_WORK.md @@ -100,3 +100,92 @@ PR creation. Fix the reported problem and retry finish. Neither helper merges or - Draft PR on the first push; take it to ready only when the gates pass and CI is green. - A different worker than the author reviews the PR. - No auto-merge. Merging is the maintainer's (`CONTRIBUTING.md`). + +## Self-hosted runner + +On a dedicated Ubuntu 24.04 or newer machine with systemd (x86_64 or aarch64), run the +bootstrap from a trusted checkout. It creates the non-sudo `fizzy-worker` account, installs +Zig at the version in `scripts/cloud-setup.sh`, jj, gh, git, zstd and Linux build/test libraries, +restores the published Zig dependencies, and registers an Actions runner with the `fizzy` +label. It installs the runner's systemd service as `fizzy-worker`, plus a daily cleanup timer. +macOS and Windows provisioning are separate follow-up work. + +```sh +# Preview on any machine: no root, token, downloads or system changes needed. +scripts/runner-setup.sh --dry-run + +# Read a short-lived registration token from GitHub's Settings > Actions > Runners. +# The token enters neither shell history nor command arguments. +read -rs -p 'Runner registration token: ' RUNNER_TOKEN; echo +export RUNNER_TOKEN +sudo --preserve-env=RUNNER_TOKEN bash scripts/runner-setup.sh +unset RUNNER_TOKEN +``` + +**Private repositories only. Never register this runner on a public repository, and never let it +run fork PRs:** a workflow there would execute a stranger's code as `fizzy-worker`. `RUNNER_URL` +defaults to the private `https://github.com/fizzyedit/work` and must be a +`https://github.com//` URL. The script confirms it is private with +`gh api repos// --jq .private` and refuses otherwise, including when `gh` is missing +or not logged in; after checking by hand, `FIZZY_RUNNER_ALLOW_UNCHECKED=1` skips the check. +`RUNNER_NAME` defaults to the hostname and +`RUNNER_LABELS` to `fizzy` (comma-separated additional labels). Preserve these variables through +sudo when overriding them. The registration token is passed to `config.sh` through +`ACTIONS_RUNNER_INPUT_TOKEN`; it is never written by the bootstrap. The runner stores its own +service credentials in `/home/fizzy-worker/actions-runner`, outside the checkout. + +The runner's `.env` sets `FIZZY_BUILD_SLOTS` (default 2) and `FIZZY_BUILD_MAX_LOAD` (default +the CPU count) for every job, so `scripts/zb` bounds concurrent builds on the box; override either in +the environment when running the bootstrap, through sudo like the others. + +Reruns preserve an existing registration and service identity; they refresh tools, dependencies +and cleanup units. Stop jobs and builds before rerunning. `ZIG_VERSION`, `JJ_VERSION` and +`RUNNER_VERSION` can pin versions (without a leading `v`); jj and an unconfigured runner default +to GitHub's latest releases. `DEPS_CACHE_URL` can select another trusted cache archive. The jj, runner and deps-cache +downloads are not checksummed (the box is trusted), and `latest` is resolved when the script runs, +so two runs can install different versions; pin the versions for a reproducible box. Keep +both scripts beside `cloud-setup.sh` when copying the bootstrap to the box. A live installation +requires a maintainer's machine and token; local dry-run and shellcheck checks do not prove +registration or service startup. + +Use this only for trusted workflows: jobs execute with the worker account's access. The account +has no sudo or container-admin group membership. Runner installation does not provide a model +API credential or a GitHub push token. Configure any worker harness separately, keeping its +push credentials outside the model's reach as described under Trust boundary above. + +The runner uses `/home/fizzy-worker/actions-runner/_work`. Put additional jj workspaces in +`/home/fizzy-worker/workspaces`; keep their shared repo outside any disposable workspace. +Cleanup runs as `fizzy-worker`, retains the global `~/.cache/zig/p` archive cache, and removes +untracked `.zig-cache`, `zig-out` and `zig-pkg` trees in these two roots, including runaway +`zig-pkg/.tmp-*` trees. A successful jj or git tracked-file query is required before deleting +a cache. Any live Zig, ZLS or Actions job process vetoes the whole sweep; inspection failures +also retain the data. Builds started manually should be stopped before manual cleanup. + +Deleting a whole workspace is opt-in. As `fizzy-worker`, enroll a disposable workspace with a +file named `.pr` containing only its PR URL: + +```sh +mkdir -p ~/.config/fizzy/workspaces +printf '%s\n' 'https://github.com/fizzyedit/fizzy/pull/123' > ~/.config/fizzy/workspaces/ci-example.pr +# This authorizes removing ~/workspaces/ci-example after that PR merges. +/usr/local/sbin/fizzy-runner-clean --dry-run +``` + +Enrollment authorizes deleting the clean merged checkout, including its tracked source and +ignored files. Keep valuable ignored files elsewhere. The cleaner checks GitHub reports +`MERGED`, and that a clean standalone git clone's HEAD, or an empty jj workspace's parent, +matches the PR head commit. Linked jj workspaces are forgotten before removal; the shared +repo is retained. Standalone jj repositories and git worktrees are retained. Dirty, divergent, +unregistered and unsupported workspaces are retained. Failed GitHub lookups retain workspaces; +authenticate gh separately as `fizzy-worker` if private PRs need checking. The runner registration +token is not a gh API token. + +```sh +systemctl status fizzy-runner-clean.timer +journalctl -u fizzy-runner-clean.service +sudo -u fizzy-worker -H /usr/local/sbin/fizzy-runner-clean --dry-run +``` + +Each removal logs its estimated freed KiB and the sweep logs a total. The timer skips active +jobs rather than stopping them; the next daily sweep retries. It never sweeps unrelated users' +projects or deletes unregistered workspace directories. diff --git a/scripts/runner-clean.sh b/scripts/runner-clean.sh new file mode 100755 index 000000000..b748455a9 --- /dev/null +++ b/scripts/runner-clean.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash +# Clean only this dedicated worker's disposable build data and enrolled PR workspaces. +set -euo pipefail + +dry_run=false +case "${1:-}" in + --dry-run) dry_run=true ;; + '') ;; + *) echo "usage: $0 [--dry-run]" >&2; exit 2 ;; +esac +[ "$#" -le 1 ] || exit 2 +worker_home=${FIZZY_WORKER_HOME:-/home/fizzy-worker} +if [ "$(uname -s)" != Linux ]; then + if "$dry_run"; then + echo "Would inspect Linux /proc for active Zig, ZLS and Actions jobs." + echo "Would clean untracked caches under $worker_home/{workspaces,actions-runner/_work}." + echo "Would check enrolled PRs in $worker_home/.config/fizzy/workspaces/*.pr." + exit 0 + fi + echo 'runner-clean requires Linux /proc' >&2; exit 1 +fi +[ -d "$worker_home" ] && [ ! -L "$worker_home" ] || exit 0 +worker_home=$(realpath -e "$worker_home") +exec 9>"$worker_home/.runner-clean.lock" +flock -n 9 || exit 0 + +# A whole-sweep veto also protects builds whose compiler's cwd is outside the project. +# Check all users: a root-started build can still be using a worker's packages. +busy() { + local proc comm + if [ ! -r /proc/self/comm ]; then + echo 'Cannot inspect /proc; retaining all data' + return 0 + fi + for proc in /proc/[0-9]*; do + if ! IFS= read -r comm 2>/dev/null < "$proc/comm"; then + [ ! -d "$proc" ] && continue + echo "Cannot inspect $proc; retaining all data" + return 0 + fi + case "$comm" in + zig|zls|Runner.Worker*) echo "Busy process ${proc##*/} ($comm); retaining all data"; return 0 ;; + esac + done + return 1 +} +busy && exit 0 +tracked_file=$(mktemp "$worker_home/.runner-clean.XXXXXX") +trap 'rm -f -- "$tracked_file"' EXIT +freed=0 +remove() { + local path=$1 kb + busy && return 0 + kb=$(du -sk -- "$path" | cut -f1) + if "$dry_run"; then + printf 'Would remove %q (%s KiB)\n' "$path" "$kb" + else + rm -rf --one-file-system -- "$path" + freed=$((freed + kb)) + printf 'Removed %q (%s KiB)\n' "$path" "$kb" + fi +} + +# A successful tracked-file query is required; an unrelated parent git repo is not evidence. +clean_caches() { + local repo=$1 cache relative file is_tracked parent nested + local -a tracked + if [ -d "$repo/.jj" ]; then + jj -R "$repo" --ignore-working-copy file list -T 'path ++ "\0"' > "$tracked_file" || return 0 + elif [ -e "$repo/.git" ]; then + git -C "$repo" ls-files -z > "$tracked_file" || return 0 + else + return 0 + fi + mapfile -d '' -t tracked < "$tracked_file" + while IFS= read -r -d '' cache; do + # Nested repositories must use their own tracked-file list. + parent=${cache%/*} + nested=false + while [ "$parent" != "$repo" ]; do + if [ -e "$parent/.git" ] || [ -d "$parent/.jj" ]; then nested=true; break; fi + parent=${parent%/*} + done + "$nested" && continue + nested=$(find "$cache" \( -name .git -o -name .jj \) -print -quit) + if [ -n "$nested" ]; then + printf 'Retaining cache containing repository metadata %q\n' "$cache" + continue + fi + relative=${cache#"$repo/"} + is_tracked=false + for file in "${tracked[@]}"; do + case "$file" in "$relative"|"$relative"/*) is_tracked=true; break ;; esac + done + if "$is_tracked"; then + printf 'Retaining tracked cache %q\n' "$cache" + continue + fi + remove "$cache" + done < <(find "$repo" -type d \( -name .git -o -name .jj \) -prune -o \ + -type d \( -name .zig-cache -o -name zig-out -o -name zig-pkg \) -prune -print0) +} + +for base in "$worker_home/workspaces" "$worker_home/actions-runner/_work"; do + [ -d "$base" ] && [ ! -L "$base" ] || continue + # Discover nested Actions checkouts as well as top-level jj workspaces, without entering caches. + while IFS= read -r -d '' metadata; do + clean_caches "${metadata%/*}" + done < <(find "$base" -type d \( -name .zig-cache -o -name zig-out -o -name zig-pkg \) -prune -o \ + \( -name .git -o -name .jj \) -print0 -prune) +done + +for enrollment in "$worker_home"/.config/fizzy/workspaces/*.pr; do + [ -f "$enrollment" ] && [ ! -L "$enrollment" ] || continue + name=${enrollment##*/}; name=${name%.pr} + [[ "$name" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]] || continue + workspace=$worker_home/workspaces/$name + [ -d "$workspace" ] && [ ! -L "$workspace" ] || continue + # Nested symlinks could otherwise make metadata checks inspect a different checkout. + [ "$(realpath -e "$workspace")" = "$workspace" ] || continue + pr=$(cat "$enrollment") + [[ "$pr" =~ ^https://github\.com/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/pull/[0-9]+$ ]] || continue + if ! result=$(gh pr view "$pr" --json state,headRefOid --jq '[.state,.headRefOid] | @tsv'); then + echo "Cannot look up $pr; retaining $workspace"; continue + fi + read -r state head <<< "$result" + [ "$state" = MERGED ] && [[ "$head" =~ ^[0-9a-f]{40}$ ]] || continue + # Nested repositories and submodules have their own changes; retain their container. + nested=$(find "$workspace" -mindepth 1 \( -path "$workspace/.git" -o -path "$workspace/.jj" \) -prune -o \ + \( -name .git -o -name .jj \) -print -quit) + [ -z "$nested" ] || continue + if [ -d "$workspace/.jj" ]; then + # Shared repo storage must live outside the directory being removed. + [ -f "$workspace/.jj/repo" ] && [ ! -L "$workspace/.jj" ] || continue + repo_pointer=$(cat "$workspace/.jj/repo") + repo_path=$(realpath -e "$workspace/.jj/$repo_pointer") || continue + case "$repo_path/" in "$workspace/"*) continue ;; esac + # Snapshot first, so edits made after the last jj command cannot be discarded. + if "$dry_run"; then + echo "Would snapshot $workspace, check for edits and match its parent to $head; forget and remove only if clean" + continue + fi + diff=$(jj -R "$workspace" diff --summary) || continue + [ -z "$diff" ] || continue + parent=$(jj -R "$workspace" log --no-graph -r @- -T 'commit_id') || continue + [ "$parent" = "$head" ] || continue + # Ignored files are disposable only because the maintainer explicitly enrolled this workspace. + busy && break + if ! "$dry_run"; then jj -R "$workspace" workspace forget || continue; fi + elif [ -d "$workspace/.git" ] && [ ! -L "$workspace/.git" ]; then + status=$(git -C "$workspace" status --porcelain --untracked-files=all) || continue + [ -z "$status" ] || continue + current=$(git -C "$workspace" rev-parse HEAD) || continue + [ "$current" = "$head" ] || continue + # A clone with other worktrees owns metadata they need; retain it. + worktrees=$(git -C "$workspace" worktree list --porcelain | awk '/^worktree / { n++ } END {print n}') + [ "$worktrees" = 1 ] || continue + else + echo "Retaining unsupported workspace $workspace"; continue + fi + remove "$workspace" + if ! "$dry_run" && [ ! -e "$workspace" ]; then rm -- "$enrollment"; fi +done +printf 'Cleanup freed %s KiB\n' "$freed" diff --git a/scripts/runner-setup.sh b/scripts/runner-setup.sh new file mode 100755 index 000000000..1b597f6a8 --- /dev/null +++ b/scripts/runner-setup.sh @@ -0,0 +1,200 @@ +#!/usr/bin/env bash +# Provision a dedicated Ubuntu Actions worker. Run as root; --dry-run needs no privileges. +set -euo pipefail +# Registration credentials must not enter traces, command arguments, or generated files. +set +x +registration_token=${RUNNER_TOKEN:-} +unset RUNNER_TOKEN + +dry_run=false +case "${1:-}" in + --dry-run) dry_run=true ;; + '') ;; + *) echo "usage: $0 [--dry-run]" >&2; exit 2 ;; +esac +[ "$#" -le 1 ] || exit 2 +worker=fizzy-worker +worker_home=/home/$worker +runner=$worker_home/actions-runner +source_dir=$(cd -- "$(dirname -- "$0")" && pwd) +runner_url=${RUNNER_URL:-https://github.com/fizzyedit/work} +build_slots=${FIZZY_BUILD_SLOTS:-2} +build_max_load=${FIZZY_BUILD_MAX_LOAD:-$(getconf _NPROCESSORS_ONLN)} +runner_name=${RUNNER_NAME:-$(hostname)} +labels=${RUNNER_LABELS:-fizzy} +# Read the cloud bootstrap's default so these two entry points cannot drift. +# shellcheck disable=SC2016 # Match the literal default expression in cloud-setup.sh. +zig_version=${ZIG_VERSION:-$(sed -n 's/^zig_version="${ZIG_VERSION:-\([^}]*\)}"/\1/p' "$source_dir/cloud-setup.sh")} +[ -n "$zig_version" ] || { echo 'Cannot read cloud-setup.sh Zig version' >&2; exit 1; } + +run() { + printf '+'; printf ' %q' "$@"; printf '\n' + if ! "$dry_run"; then "$@"; fi +} +write_file() { + echo "+ write $1" + if "$dry_run"; then cat; else cat > "$1"; fi +} +latest() { + curl -fsSL --retry 4 "https://api.github.com/repos/$1/releases/latest" | + python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"].removeprefix("v"))' +} + +# The runner executes whatever the repository's workflows say, so it may only serve a private repo. +if [ "${FIZZY_RUNNER_ALLOW_UNCHECKED:-}" = 1 ]; then + echo "+ skip the private-repo check for $runner_url (FIZZY_RUNNER_ALLOW_UNCHECKED=1)" +else + repo_path=${runner_url#https://github.com/} + repo_path=${repo_path%/} + case "$repo_path" in + */*/* | */ | /* | "$runner_url") repo_path= ;; + */*) ;; + *) repo_path= ;; + esac + [ -n "$repo_path" ] || { + echo "RUNNER_URL must be https://github.com//: $runner_url" >&2; exit 1; + } + if ! command -v gh >/dev/null 2>&1 || ! gh auth status >/dev/null 2>&1; then + echo 'Cannot verify the repository is private: gh is missing or not logged in.' >&2 + echo 'Log in with gh, or set FIZZY_RUNNER_ALLOW_UNCHECKED=1 after checking by hand.' >&2 + exit 1 + fi + [ "$(gh api "repos/$repo_path" --jq .private 2>/dev/null)" = true ] || { + echo "Refusing $runner_url: not confirmed private. Never register a runner on a public repo." >&2 + exit 1 + } + echo "+ confirmed $repo_path is private" +fi + +echo '+ verify Ubuntu >=24.04, root, systemd, architecture, and no active builds or Actions jobs' +if ! "$dry_run"; then + [ "$(uname -s)" = Linux ] && [ "$(id -u)" -eq 0 ] || { + echo 'Run as root on Ubuntu with systemd.' >&2; exit 1; + } + # shellcheck disable=SC1091 + . /etc/os-release + [ "$ID" = ubuntu ] && dpkg --compare-versions "$VERSION_ID" ge 24.04 && [ -d /run/systemd/system ] || { + echo 'Ubuntu 24.04 or newer with systemd is required.' >&2; exit 1; + } + if pgrep -x 'zig|zls|Runner.Worker' >/dev/null; then + echo 'Stop active builds and Actions jobs before provisioning.' >&2; exit 1 + fi + if [ ! -f "$runner/.runner" ] && [ -z "${registration_token:-}" ]; then + echo 'Set RUNNER_TOKEN to a fresh GitHub runner registration token.' >&2; exit 1 + fi +fi +case "$(uname -m)" in + x86_64) runner_arch=x64; jj_arch=x86_64 ;; + aarch64|arm64) runner_arch=arm64; jj_arch=aarch64 ;; + *) echo 'Only x86_64 and aarch64 are supported.' >&2; exit 1 ;; +esac + +# Pause the listener during a rerun so a queued job cannot start while tools are replaced. +if [ -f "$runner/.service" ]; then + run bash -c 'cd "$1" && ./svc.sh stop' _ "$runner" +fi +run apt-get update +run env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates curl git gh zstd xz-utils python3 python3-venv jq \ + build-essential pkg-config libwayland-dev libxkbcommon-dev libx11-dev \ + libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxfixes-dev libxss-dev \ + libasound2-dev libpulse-dev libegl-dev libgl-dev \ + mesa-vulkan-drivers libvulkan1 vulkan-tools xvfb xauth +if ! id "$worker" >/dev/null 2>&1; then + run useradd --create-home --user-group --shell /bin/bash "$worker" +fi +if ! "$dry_run"; then + [ "$(getent passwd "$worker" | cut -d: -f6)" = "$worker_home" ] || { + echo "Existing $worker has an unexpected home" >&2; exit 1; + } + # Refuse a privileged existing account rather than silently repurposing it. + [ "$(id -u "$worker")" -ne 0 ] || exit 1 + if id -nG "$worker" | tr ' ' '\n' | grep -Eq '^(sudo|admin|wheel|docker|lxd)$'; then + echo "$worker must be an unprivileged dedicated account" >&2; exit 1 + fi +fi +run install -d -o "$worker" -g "$worker" "$runner" "$worker_home/workspaces" "$worker_home/.cache/zig" +run python3 -m venv /opt/fizzy-zig +run /opt/fizzy-zig/bin/pip install "ziglang==$zig_version" +if "$dry_run"; then + echo '+ locate ziglang with /opt/fizzy-zig/bin/python and link its zig into /usr/local/bin' +else + zig_binary=$(/opt/fizzy-zig/bin/python -I -c 'import pathlib,ziglang; print(pathlib.Path(ziglang.__file__).parent / "zig")') + run ln -sfn "$zig_binary" /usr/local/bin/zig +fi + +scratch=/tmp/fizzy-runner-setup.DRY-RUN +if ! "$dry_run"; then scratch=$(mktemp -d); fi +trap 'if ! "$dry_run"; then rm -rf -- "$scratch"; fi' EXIT +jj_version=${JJ_VERSION:-latest} +if [ "$jj_version" = latest ]; then + echo '+ resolve latest jj-vcs/jj release via GitHub API' + if ! "$dry_run"; then jj_version=$(latest jj-vcs/jj); fi +fi +run curl -fL --retry 4 -o "$scratch/jj.tar.gz" \ + "https://github.com/jj-vcs/jj/releases/download/v$jj_version/jj-v$jj_version-$jj_arch-unknown-linux-musl.tar.gz" +run mkdir -p "$scratch/jj" +run tar -xzf "$scratch/jj.tar.gz" -C "$scratch/jj" +run install -m 0755 "$scratch/jj/jj" /usr/local/bin/jj +# Never unpack over a configured runner: its own updater manages that installation. +if [ ! -f "$runner/.runner" ]; then + runner_version=${RUNNER_VERSION:-latest} + if [ "$runner_version" = latest ]; then + echo '+ resolve latest actions/runner release via GitHub API' + if ! "$dry_run"; then runner_version=$(latest actions/runner); fi + fi + run curl -fL --retry 4 -o "$scratch/runner.tar.gz" \ + "https://github.com/actions/runner/releases/download/v$runner_version/actions-runner-linux-$runner_arch-$runner_version.tar.gz" + run tar -xzf "$scratch/runner.tar.gz" -C "$runner" + run "$runner/bin/installdependencies.sh" + run chown -R "$worker:$worker" "$runner" +fi +run curl -fL --retry 4 -o "$scratch/deps.tar.zst" \ + "${DEPS_CACHE_URL:-https://github.com/fizzyedit/fizzy/releases/download/deps-cache/zig-deps-linux.tar.zst}" +run chmod 0755 "$scratch" +run chmod 0644 "$scratch/deps.tar.zst" +run runuser -u "$worker" -- tar --zstd -xf "$scratch/deps.tar.zst" -C "$worker_home/.cache/zig" +if [ ! -f "$runner/.runner" ]; then + echo '+ configure runner as fizzy-worker using RUNNER_TOKEN from environment (redacted)' + if ! "$dry_run"; then + export ACTIONS_RUNNER_INPUT_TOKEN=$registration_token + unset registration_token + (cd "$runner" && runuser -u "$worker" -- ./config.sh --unattended \ + --url "$runner_url" --name "$runner_name" --labels "$labels" --work _work) + unset ACTIONS_RUNNER_INPUT_TOKEN + fi +fi +# The runner passes .env to every job, so scripts/zb inherits the slot count and load ceiling. +run bash -c 'touch "$1/.env" && sed -i "/^FIZZY_BUILD_\(SLOTS\|MAX_LOAD\)=/d" "$1/.env" && + printf "FIZZY_BUILD_SLOTS=%s\nFIZZY_BUILD_MAX_LOAD=%s\n" "$2" "$3" >> "$1/.env" && + chown "$4:$4" "$1/.env"' _ "$runner" "$build_slots" "$build_max_load" "$worker" +if [ ! -f "$runner/.service" ]; then + run bash -c 'cd "$1" && ./svc.sh install "$2"' _ "$runner" "$worker" +fi +run bash -c 'cd "$1" && ./svc.sh start' _ "$runner" +run install -m 0755 "$source_dir/runner-clean.sh" /usr/local/sbin/fizzy-runner-clean +write_file /etc/systemd/system/fizzy-runner-clean.service <<'UNIT' +[Unit] +Description=Clean idle Fizzy worker caches and merged workspaces +[Service] +Type=oneshot +User=fizzy-worker +Environment=HOME=/home/fizzy-worker +ExecStart=/usr/local/sbin/fizzy-runner-clean +Nice=19 +UNIT +write_file /etc/systemd/system/fizzy-runner-clean.timer <<'UNIT' +[Unit] +Description=Daily Fizzy worker disk hygiene +[Timer] +OnCalendar=daily +Persistent=true +RandomizedDelaySec=30m +[Install] +WantedBy=timers.target +UNIT +run systemctl daemon-reload +run systemctl enable --now fizzy-runner-clean.timer +if "$dry_run"; then echo 'Dry run complete; no changes made.'; else + echo 'Worker ready. Cleanup logs: journalctl -u fizzy-runner-clean.service' +fi diff --git a/scripts/test-runner-setup.sh b/scripts/test-runner-setup.sh new file mode 100755 index 000000000..fd0fa9b0e --- /dev/null +++ b/scripts/test-runner-setup.sh @@ -0,0 +1,36 @@ +#!/bin/sh +# Checks the pure parts of scripts/runner-setup.sh through --dry-run: RUNNER_URL defaults to the +# private work repo, a public or unverifiable repo is refused, and the unchecked override works. +# A fake gh on PATH answers `gh auth status` and `gh api`; its answer is in $T/private. +cd "$(dirname "$0")" || exit 1 +T=$(mktemp -d) +trap 'rm -rf "$T"' EXIT +mkdir "$T/bin" +cat >"$T/bin/gh" <"$T/asked" +cat "$T/private" +FAKE +chmod +x "$T/bin/gh" +fail() { echo "FAIL: $1"; exit 1; } +setup() { PATH="$T/bin:$PATH" bash ./runner-setup.sh --dry-run 2>&1; } + +echo true >"$T/private" +out=$(setup) || fail "private default refused: $out" +grep -qx 'repos/fizzyedit/work' "$T/asked" || fail "default is not fizzyedit/work: $(cat "$T/asked")" + +echo false >"$T/private" +out=$(setup) && fail "public repo accepted" +echo "$out" | grep -q 'Refusing' || fail "no refusal line: $out" + +echo true >"$T/private" +out=$(RUNNER_URL=https://github.com/fizzyedit sh -c 'PATH="$1/bin:$PATH" bash ./runner-setup.sh --dry-run 2>&1' _ "$T") && + fail "org URL accepted" + +# gh absent from PATH's front is not enough (the host may have one): hide it behind a failing stub. +printf '#!/bin/sh\nexit 1\n' >"$T/bin/gh" +out=$(setup) && fail "unverifiable repo accepted" +out=$(FIZZY_RUNNER_ALLOW_UNCHECKED=1 setup) || fail "override refused: $out" +echo "$out" | grep -q 'skip the private-repo check' || fail "override not reported" +echo ok