#!/usr/bin/env bash # Lab-host auto-update. Pulls origin/main and re-runs install-lab-host.sh # when there's a newer commit on the canonical remote. # # Run by cis490-autoupdate.timer. Idempotent; safe to re-invoke. # # Why this exists: when the receiver's commit-allow-list rolls forward, # any lab host running older code starts getting 412/400 on every PUT. # Without auto-update, that requires either the on-device AI agent or # the operator to notice and run `git pull && install-lab-host.sh` — # neither of which happens reliably (k-gamingcom + elliott-thinkpad # both stalled silently on the post-cutover 2026-05-01 incident). # With auto-update, hosts catch up within RandomizedDelaySec of the # next timer fire (≤ 40 min) on their own. # # Safety: # - git pull is `--ff-only` — never rewrites or merges; if local # diverged from origin (operator hand-edit, partial install) it # bails rather than guess. # - install-lab-host.sh is the SAME script the operator runs by hand. # No special "auto" path; we want exactly one path through bring-up. # - On any failure we exit non-zero so systemd records it; the timer # re-fires next interval. Failures don't disable the timer. # - The version gate provides quality control: even if auto-update # pulls a known-bad commit, the receiver's allow-list catches it # downstream. set -euo pipefail INSTALL_ROOT="${INSTALL_ROOT:-/opt/cis490}" SERVICE_USER="${SERVICE_USER:-cis490}" log() { printf '[auto-update] %s\n' "$*" >&2; } [[ -d "$INSTALL_ROOT/.git" ]] || { log "no .git in $INSTALL_ROOT — auto-update only supports git checkouts" exit 0 } cd "$INSTALL_ROOT" # All git ops run as the service user (the owner of $INSTALL_ROOT). # Running as root would trip git's "dubious ownership" guard. GIT() { sudo -u "$SERVICE_USER" git -C "$INSTALL_ROOT" "$@"; } if ! GIT fetch --quiet origin main; then log "git fetch failed — network blip or remote down; will retry next tick" exit 0 # don't fail the unit; this is expected on offline hosts fi LOCAL="$(GIT rev-parse HEAD)" REMOTE="$(GIT rev-parse origin/main)" if [[ "$LOCAL" == "$REMOTE" ]]; then log "up to date at ${LOCAL:0:12}" exit 0 fi # Branch divergence check — operator hand-edits or partial installs # could leave HEAD on a non-main commit. We don't want to silently # overwrite that. if ! GIT merge-base --is-ancestor HEAD origin/main; then log "WARN: local HEAD ${LOCAL:0:12} is not an ancestor of origin/main" log " ${REMOTE:0:12} — refusing to fast-forward. Investigate via" log " 'git -C $INSTALL_ROOT log --all --oneline -10' on the host." exit 1 fi log "updating ${LOCAL:0:12} -> ${REMOTE:0:12}" GIT pull --ff-only --quiet origin main # install-lab-host.sh handles VERSION re-stamp, queue drain, daemon-reload, # and systemctl restart of the lab-host services. Pass control to it # directly via exec so its exit code is ours. log "re-running install-lab-host.sh to apply new code" exec "$INSTALL_ROOT/scripts/install-lab-host.sh"