Files
Media-Server-Setup/deploy.sh
Brandyn / Techy 1ee8befdee install.sh, deploy.sh and the runbooks cover the bot bundle path
install.sh installs deployBotBundle.sh as vanguard-deploy-bot and creates /srv/bot/app; the pipeline
reinstalls it when it changes. FRESH-INSTALL gains section 7a and appendix L (the first release, the
python312 runner label, the Gitea secrets, the Authentik app), with the appendix relettered so it stays in
order. README's rebuild list says the bot's repo is outside this one and what to restore where.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-02 21:57:52 -06:00

116 lines
7.5 KiB
Bash
Executable File

#!/bin/bash
# Minimal-impact deployer for the server checkout. Run as root (sudo) — installed as /usr/local/bin/vanguard-deploy.
# vanguard-deploy pull fetch + fast-forward DEPLOY_BRANCH, then apply only what changed
# vanguard-deploy apply [OLDREV] apply changes between OLDREV (default: last deployed rev) and HEAD
# vanguard-deploy rollback check out the previously deployed rev and apply the reverse diff
# vanguard-deploy pending restart MediaMTX if a restart is pending and no stream is live (timer runs this)
# vanguard-deploy status
# Env (from /etc/vanguard/deploy.env): DEPLOY_REPO_DIR, DEPLOY_BRANCH, DEPLOY_NOTIFY_WEBHOOK_URL. VG_DRY=1 prints only.
set -uo pipefail
ENVF=/etc/vanguard/deploy.env; [[ -f $ENVF ]] && set -a && . "$ENVF" && set +a
REPO="${DEPLOY_REPO_DIR:-/home/debian/vanguard-stream}"; BRANCH="${DEPLOY_BRANCH:-main}"
STATE=/var/lib/vanguard; mkdir -p "$STATE" 2>/dev/null || true
LASTF="$STATE/deployed-rev"; PREVF="$STATE/previous-rev"; PENDING="$STATE/mediamtx-restart-pending"
DRY="${VG_DRY:-0}"; OWNER=$(stat -c %U "$REPO" 2>/dev/null || echo debian)
log() { printf 'ts=%s level=info component=deploy msg="%s"\n' "$(date -Is)" "${*//\"/\\\"}"; }
run() { log "\$ $*"; [[ $DRY == 1 ]] || "$@"; }
asowner() { log "\$ (as $OWNER) $*"; [[ $DRY == 1 ]] || sudo -u "$OWNER" -H "$@"; }
notify() {
[[ -n ${DEPLOY_NOTIFY_WEBHOOK_URL:-} && $DRY != 1 ]] || return 0
curl -s -m 10 -H 'Content-Type: application/json' -d "$(python3 -c 'import json,sys; print(json.dumps({"content": sys.argv[1][:1900]}))' "$1")" "$DEPLOY_NOTIFY_WEBHOOK_URL" >/dev/null || true
}
cd "$REPO" || { log "repo dir $REPO missing"; exit 1; }
# "pending" (the 5-min timer) needs no git; every other mode does
[[ ${1:-} == pending ]] || git rev-parse --is-inside-work-tree >/dev/null 2>&1 || { log "$REPO is not a git checkout (run switchToClone.sh once)"; exit 1; }
live_streams() { curl -s -m 3 'http://127.0.0.1:9997/v3/paths/list' 2>/dev/null | python3 -c 'import json,sys; d=json.load(sys.stdin); print(sum(1 for p in d.get("items",[]) if p.get("ready")))' 2>/dev/null || echo 0; }
restart_mediamtx_or_defer() {
local n; n=$(live_streams)
if [[ $n == 0 ]]; then
run docker compose restart mediamtx; rm -f "$PENDING"; log "mediamtx restarted"
else
[[ $DRY == 1 ]] || touch "$PENDING"; log "mediamtx restart DEFERRED: $n live stream(s); deploy-pending.timer retries every 5 min"
notify "⏳ deploy: MediaMTX restart deferred ($n live stream(s)); will restart when idle"
fi
}
apply_changes() {
local old="$1" new="$2"
local changed; changed=$(git diff --name-only "$old" "$new" 2>/dev/null || git ls-files)
[[ -n $changed ]] || { log "nothing changed between $old and $new"; return 0; }
log "changed files:"; echo "$changed" | sed 's/^/ /'
has() { echo "$changed" | grep -qE "$1"; }
# 0. static checks on the new tree
if [[ -x check.sh ]]; then asowner bash check.sh >/tmp/vg-check.log 2>&1 || { log "check.sh FAILED — aborting"; tail -20 /tmp/vg-check.log; notify "❌ deploy $new: check.sh failed"; return 1; }; fi
# 1. host scripts + units
if has '^bin/'; then run install -m 0755 bin/normalize-sweep.sh bin/trash-purge.sh bin/dumpd.py bin/vgstrip.py bin/vgdeband.sh /usr/local/bin/; fi
if has '^systemd/'; then run install -m 0644 systemd/*.service systemd/*.timer /etc/systemd/system/; run systemctl daemon-reload; fi
if has '^deploy\.sh$'; then run install -m 0755 deploy.sh /usr/local/bin/vanguard-deploy; fi
if has '^deployBotBundle\.sh$'; then run install -m 0755 deployBotBundle.sh /usr/local/bin/vanguard-deploy-bot; fi
if has '^firewall/nftables\.conf$'; then run install -m 0644 firewall/nftables.conf /etc/nftables.conf; run nft -f /etc/nftables.conf; fi
# 2. regenerate configs (Caddy reload, copyparty restart) — configure.py decides; MediaMTX is handled below
if has '^configure\.py$|^syncthing/stignore$|^dock/|^clips/|^about/'; then
local before_mtx; before_mtx=$(md5sum mediamtx.yml 2>/dev/null | cut -d' ' -f1)
if [[ $DRY == 1 ]]; then log "\$ (as $OWNER) VG_DRY=1 python3 configure.py --apply"; else sudo -u "$OWNER" -H python3 configure.py --apply >/tmp/vg-configure.log 2>&1 || { log "configure --apply FAILED"; tail -20 /tmp/vg-configure.log; return 1; }; fi
local after_mtx; after_mtx=$(md5sum mediamtx.yml 2>/dev/null | cut -d' ' -f1)
[[ $before_mtx == "$after_mtx" ]] || { log "mediamtx.yml changed"; restart_mediamtx_or_defer; }
fi
if has '^bin/dumpd\.py$|^systemd/dumpd\.service$'; then run systemctl restart dumpd; fi
# 3. compose: recreate changed services, but never MediaMTX while live
if has '^docker-compose\.yml$|^bot/Dockerfile$|^bot/entrypoint\.sh$'; then
local mtx_changed=0
git diff "$old" "$new" -- docker-compose.yml | grep -qE '^[+-]' && git diff -U0 "$old" "$new" -- docker-compose.yml | grep -qiE '^[+-].*(mediamtx|8189|8890|1935)' && mtx_changed=1
if [[ $mtx_changed == 1 && $(live_streams) != 0 ]]; then
log "compose touches mediamtx and streams are live: recreating everything except mediamtx"
run docker compose up -d --build --no-deps $(docker compose config --services | grep -v '^mediamtx$' | tr '\n' ' ')
[[ $DRY == 1 ]] || touch "$PENDING"; notify "⏳ deploy: MediaMTX recreate deferred (live streams)"
else
run docker compose up -d --build
fi
fi
# 4. Jellyfin plugin (committed zip)
if has '^jellyfin-plugin-vanguard/dist/.*\.zip$'; then run bash installJellyfinPlugin.sh; fi
# 5. live portal
if has '^live/'; then
if command -v npm >/dev/null; then
asowner bash -c 'cd live && npm install --no-audit --no-fund --loglevel=error && npm run build' && { run install -d -o "$OWNER" /srv/live; run rsync -a --delete live/dist/ /srv/live/; }
else log "npm not installed on the server: live/ changed but not built (apt-get install nodejs npm)"; fi
fi
log "applied $old..$new"
}
case "${1:-}" in
pull)
old=$(git rev-parse HEAD)
asowner git fetch --quiet origin "$BRANCH" || { log "fetch failed"; exit 1; }
asowner git merge --ff-only "origin/$BRANCH" || { log "fast-forward failed (local changes?)"; exit 1; }
new=$(git rev-parse HEAD)
[[ $old == "$new" ]] && { log "already at $new"; exit 0; }
if apply_changes "$old" "$new"; then
[[ $DRY == 1 ]] || { cp -f "$LASTF" "$PREVF" 2>/dev/null; echo "$new" > "$LASTF"; }
notify "✅ deploy $(git log -1 --format='%h %s' "$new")"
else notify "❌ deploy $new failed — see journalctl -u dumpd"; exit 1; fi ;;
apply)
old="${2:-$(cat "$LASTF" 2>/dev/null || git rev-parse HEAD~1)}"; new=$(git rev-parse HEAD)
apply_changes "$old" "$new" && { [[ $DRY == 1 ]] || { cp -f "$LASTF" "$PREVF" 2>/dev/null; echo "$new" > "$LASTF"; }; } ;;
rollback)
prev=$(cat "$PREVF" 2>/dev/null) || { log "no previous rev recorded"; exit 1; }
cur=$(git rev-parse HEAD); log "rolling back $cur -> $prev"
asowner git checkout --quiet "$prev" && apply_changes "$cur" "$prev" && { [[ $DRY == 1 ]] || { echo "$cur" > "$PREVF"; echo "$prev" > "$LASTF"; }; notify "↩️ rolled back to $prev"; } ;;
pending)
[[ -f $PENDING ]] || exit 0
restart_mediamtx_or_defer ;;
status)
echo "repo: $REPO ($(git rev-parse --abbrev-ref HEAD) @ $(git rev-parse --short HEAD))"
echo "deployed: $(cat "$LASTF" 2>/dev/null || echo '-') previous: $(cat "$PREVF" 2>/dev/null || echo '-')"
echo "auto: DEPLOY_AUTO=${DEPLOY_AUTO:-0} branch: $BRANCH pending mediamtx restart: $([[ -f $PENDING ]] && echo yes || echo no) live streams: $(live_streams)" ;;
*) sed -n '2,9p' "$0"; exit 2 ;;
esac