c2af7b6882
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:6905f4emoved %h/.local/bin -> /usr/local/bin ("-- system needs it"),933dd86moved 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.
96 lines
4.5 KiB
Bash
Executable File
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."
|