fuzzyworld-tools / files / runner / install-laptop.sh
install-laptop.sh
a4997156 2026-10-31 Pavel Safronov | 1 | #!/bin/bash |
| 2 | # Install or update the CI runner on this Mac, as a polite second runner. Run on the laptop: | |
| 3 | # | |
| 4 | # runner/install-laptop.sh copy the runner, make its ssh key, add the launchd job | |
| 5 | # runner/install-laptop.sh --authorize ...and allow that key on DreamHost (restricted to server/runner-api) | |
| 6 | # runner/install-laptop.sh --remove stop it and remove the launchd job (files stay) | |
| 7 | # | |
| 8 | # The runner wakes every 2 minutes and claims a job only when the Mac is on AC power, the CPU is mostly idle | |
| 9 | # and Docker is running (FW_RUNNER_POLITE=1). Containers get a low CPU share and FW_RUNNER_CPUS cores. | |
| 10 | # Settings (environment): FW_RUNNER_DIR (default ~/.fw-runner), FW_HOST (DreamHost ssh alias, default: dh). | |
| 11 | # Needs Docker (Docker Desktop, OrbStack, ...) and Python 3 with `venv`; PyYAML is installed into the runner's own venv. | |
| 12 | set -euo pipefail | |
| 13 | cd "$(dirname "$0")" | |
| 14 | DIR=${FW_RUNNER_DIR:-$HOME/.fw-runner} | |
| 15 | HOST=${FW_HOST:-dh} | |
| 16 | LABEL=net.fuzzyworld.fw-runner | |
| 17 | PLIST=$HOME/Library/LaunchAgents/$LABEL.plist | |
| 18 | ||
| 19 | if [ "${1:-}" = --remove ]; then | |
| 20 | launchctl bootout "gui/$(id -u)/$LABEL" 2>/dev/null || true | |
| 21 | rm -f "$PLIST" | |
| 22 | echo "launchd job removed; $DIR is untouched (delete it to forget the key, secrets and logs)" | |
| 23 | exit 0 | |
| 24 | fi | |
| 25 | ||
| 26 | mkdir -p "$DIR/.ssh" "$DIR/secrets" "$DIR/work" | |
| 27 | chmod 700 "$DIR/.ssh" "$DIR/secrets" | |
| 28 | cp fw-runner.py shoot.py "$DIR/" | |
| 29 | chmod +x "$DIR/fw-runner.py" | |
| 30 | [ -x "$DIR/venv/bin/python" ] || python3 -m venv "$DIR/venv" | |
| 31 | "$DIR/venv/bin/python" -c 'import yaml' 2>/dev/null || "$DIR/venv/bin/pip" -q install pyyaml | |
| 32 | [ -f "$DIR/.ssh/id_ed25519" ] || ssh-keygen -q -t ed25519 -N '' -C "fw-runner $(scutil --get LocalHostName) (CI only on DreamHost)" -f "$DIR/.ssh/id_ed25519" | |
| 33 | # The first install writes the settings; later installs leave your edits alone. | |
| 34 | [ -f "$DIR/runner.env" ] || cat > "$DIR/runner.env" <<ENV | |
| 35 | FW_RUNNER_NAME=$(scutil --get LocalHostName) | |
| 36 | FW_RUNNER_TAGS=docker,chrome | |
| 37 | FW_RUNNER_POLITE=1 | |
| 38 | FW_RUNNER_CPUS=2 | |
| 39 | FW_RUNNER_MEMORY=4g | |
| 40 | ENV | |
| 41 | ||
| 42 | cat > "$PLIST" <<XML | |
| 43 | <?xml version="1.0" encoding="UTF-8"?> | |
| 44 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| 45 | <plist version="1.0"><dict> | |
| 46 | <key>Label</key><string>$LABEL</string> | |
| 47 | <key>ProgramArguments</key><array><string>$DIR/venv/bin/python</string><string>$DIR/fw-runner.py</string></array> | |
| 48 | <key>StartInterval</key><integer>120</integer> | |
| 49 | <key>EnvironmentVariables</key><dict><key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$HOME/.orbstack/bin:/Applications/Docker.app/Contents/Resources/bin</string></dict> | |
| 50 | <key>StandardOutPath</key><string>$DIR/runner.log</string> | |
| 51 | <key>StandardErrorPath</key><string>$DIR/runner.log</string> | |
| 52 | <key>ProcessType</key><string>Background</string> | |
| 53 | <key>LowPriorityIO</key><true/> | |
| 54 | <key>Nice</key><integer>10</integer> | |
| 55 | </dict></plist> | |
| 56 | XML | |
| 57 | launchctl bootout "gui/$(id -u)/$LABEL" 2>/dev/null || true | |
| 58 | launchctl bootstrap "gui/$(id -u)" "$PLIST" | |
| 59 | echo "runner installed in $DIR (launchd: every 2 minutes, polite; settings in $DIR/runner.env, log in $DIR/runner.log)" | |
| 60 | ||
| 61 | pub=$(cat "$DIR/.ssh/id_ed25519.pub") | |
| 62 | line="restrict,command=\"/home/fuzzygamer/fw/tools/server/runner-api\" $pub" | |
| 63 | if [ "${1:-}" = --authorize ]; then | |
| 64 | ssh "$HOST" "grep -qF '$pub' ~/.ssh/authorized_keys || echo '$line' >> ~/.ssh/authorized_keys" | |
| 65 | echo "key allowed on $HOST (runner-api only)" | |
| 66 | else | |
| 67 | echo "To let it take jobs, add this line to ~/.ssh/authorized_keys on DreamHost (or rerun with --authorize):" | |
| 68 | echo " $line" | |
| 69 | fi | |
| 70 | echo "Repo secrets for jobs that deploy go in $DIR/secrets/<repo>.env" |