Files
bullpen/deploy/install.sh
T
marfrit c2af7b6882 systemd: units carry no username — one file, correct in both install layouts (#100)
deploy/install.sh symlinks the SAME tracked unit into ~/.config/systemd/user OR
/etc/systemd/system, so any literal ExecStart path is wrong in one of them. That
is why the path flip-flopped: 6905f4e moved %h/.local/bin -> /usr/local/bin ("--
system needs it"), 933dd86 moved it back but as /home/mfritsche/.local/bin,
dropping the specifier. bullpen-triage crash-looped 27000+ times on 203/EXEC.

Use a pair that resolves correctly in both scopes instead:
  Environment=PATH=%h/.local/bin:/usr/local/bin:/usr/bin:/bin
  ExecStart=/usr/bin/env <prog>
Measured on hertz, not recalled: in a system unit %h expands to /root, so
/root/.local/bin misses and /usr/local/bin wins; in a user unit it expands to the
user's home and ~/.local/bin wins first. /usr/bin/env resolves the program
through the unit's own PATH in both scopes.

The two layouts stay as they are — DEPLOY.md justifies them with a trust
boundary (agent tier without fleet credentials vs. coordinator tier with sic
reach), so collapsing them would trade a security property for deploy comfort.

tests/test_units_portable.py fails on the pre-fix tree (exactly these 7 units)
and passes after. Verified live: daemon-reload + restart of every affected unit
on noether, all active/running, NRestarts=0.
2026-08-01 23:49:25 +02:00

96 lines
4.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# deploy/install.sh — point this host's live bullpen AT this working copy, so the repo is the
# source of truth and `git pull` is the whole deploy.
#
# The drift problem: the fleet used to run hand-edited loose copies (scripts in ~/.local/bin,
# unit files copied into /etc/systemd/system), which silently diverged from the repo. This
# replaces those copies with SYMLINKS into the checked-out repo, so:
# * the running fleet == the repo, always; editing the live file IS editing the tracked file;
# * to update a host: `git pull` here (+ a daemon-reload / service restart to pick it up).
#
# Two layouts:
# install.sh user layout — bin/ -> ~/.local/bin, units -> ~/.config/systemd/user
# install.sh --system system layout — bin/ -> /usr/local/bin, lib/ -> /usr/local/lib/bullpen,
# contrib/lmcp-tool on PATH, units -> /etc/systemd/system (run as root)
#
# It deploys: bin/bullpen-* + the lurker, lib/*.py (--system), contrib/lmcp-tool (--system),
# and every systemd/ + lurker/ unit file. It does NOT touch per-nick lurker config dirs or the
# lmcp room tool (fleet state / load-bearing — deploy those deliberately).
#
# The units carry NO usernames and need no per-host editing. Because the SAME tracked file is
# symlinked into either layout, a literal ExecStart path is necessarily wrong in one of them —
# which is why it flip-flopped twice (6905f4e -> 933dd86) and crash-looped bullpen-triage. The
# units instead say `Environment=PATH=%h/.local/bin:/usr/local/bin:…` + `ExecStart=/usr/bin/env
# <prog>`: in user scope %h is the user's home and ~/.local/bin wins; in system scope %h is
# /root, that entry misses, and /usr/local/bin wins. tests/test_units_portable.py holds the line.
#
# Idempotent and reversible: a pre-existing NON-symlink file is moved to <name>.predeploy-bak
# before the symlink is created. Re-running is safe.
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SYSTEM=0
case "${1:-}" in
--system) SYSTEM=1 ;;
""|--user) SYSTEM=0 ;;
-h|--help) sed -n '2,20p' "$0"; exit 0 ;;
*) echo "usage: install.sh [--system]" >&2; exit 2 ;;
esac
if [ "$SYSTEM" = 1 ]; then
BINDEST="/usr/local/bin"; LIBDEST="/usr/local/lib/bullpen"; UNITDEST="/etc/systemd/system"
[ -w /usr/local/bin ] || { echo "install.sh --system needs write to /usr/local — run as root/sudo." >&2; exit 1; }
else
BINDEST="${HOME}/.local/bin"; LIBDEST=""; UNITDEST="${HOME}/.config/systemd/user"
fi
mkdir -p "$BINDEST" "$UNITDEST"; [ -n "$LIBDEST" ] && mkdir -p "$LIBDEST"
link() { # link <path-relative-to-repo> <dest-dir> <name>
local src="$REPO/$1" dest="$2" name="$3" tgt="$2/$3"
[ -f "$src" ] || { echo " skip $name (no $1 in repo)"; return; }
if [ -L "$tgt" ] && [ "$(readlink -f "$tgt")" = "$(readlink -f "$src")" ]; then
echo " ok $name (already linked)"; return
fi
if [ -e "$tgt" ] && [ ! -L "$tgt" ]; then
mv -- "$tgt" "$tgt.predeploy-bak"
echo " bak $name -> $name.predeploy-bak"
fi
ln -sfn "$src" "$tgt"
echo " link $name -> $src"
}
echo "bullpen deploy ($([ "$SYSTEM" = 1 ] && echo system || echo user)): symlinking -> $REPO"
# every bin/bullpen-* entrypoint …
for f in "$REPO"/bin/bullpen-*; do
[ -e "$f" ] || continue
link "bin/$(basename "$f")" "$BINDEST" "$(basename "$f")"
done
# … plus the lurker (lives under lurker/, not bin/)
link "lurker/bullpen-lurker" "$BINDEST" "bullpen-lurker"
if [ "$SYSTEM" = 1 ]; then
# the config/worker/participant libs the entrypoints import from /usr/local/lib/bullpen
for f in "$REPO"/lib/*.py; do
[ -e "$f" ] || continue
link "lib/$(basename "$f")" "$LIBDEST" "$(basename "$f")"
done
# the lmcp-tool client the coordinators call by name (room host)
link "contrib/lmcp-tool" "$BINDEST" "lmcp-tool"
fi
# systemd units — symlink service/timer files so `git pull` + daemon-reload deploys unit edits
units=0
for f in "$REPO"/systemd/*.service "$REPO"/systemd/*.timer "$REPO"/lurker/*.service; do
[ -e "$f" ] || continue
case "$f" in "$REPO"/systemd/*) rel="systemd/$(basename "$f")";; *) rel="lurker/$(basename "$f")";; esac
link "$rel" "$UNITDEST" "$(basename "$f")"; units=1
done
if [ "$units" = 1 ]; then
if [ "$SYSTEM" = 1 ]; then systemctl daemon-reload; else systemctl --user daemon-reload 2>/dev/null || true; fi
echo " systemd daemon-reload ($UNITDEST)"
fi
echo "done. scripts are byte-identical to what already ran (no restart needed);"
echo "changed unit files take effect after the daemon-reload above + a service restart."