#!/usr/bin/env bash# Run make targets (or one command) in order, with the whole session captured to audit/logs/.## bash scripts/ops/capture-run.sh release upgrade-web status-web# bash scripts/ops/capture-run.sh redeploy-web# bash scripts/ops/capture-run.sh -- sudo journalctl -u euler-auth --since -15min --no-pager## Each argument is a make target, run from the repo root, one after another; the run stops at the# first failure. After `--` the rest of the line is a single command instead. The session runs under# script(1), so sudo and release.sh keep a real terminal for their prompts, and everything shown on# screen lands in the log. Typed input is not recorded (sudo does not echo passwords, so they never# reach the log).## Log: audit/logs/<timestamp>-<what>.log (0600); audit/logs/latest.log links to the newest. The# audit/ folder is ignored by git: logs name collaborators and the host's layout.## The run happens inside its own tmux session, as release.sh's does, so the steps outlive this# terminal: Ctrl-b d detaches, and a dropped connection or a closed web shell costs only the view.# That matters most for `logout-all` from an `ssh` in the web shell, which closes its own terminal.# The screen goes when the session ends; the log keeps it. Without tmux, or a terminal to attach# from, the run stays in this terminal; --no-tmux asks for that.# shellcheck disable=SC2016 # the echo '…' lines write literal shell text into the session scriptset-uopipefailROOT="$(cd"$(dirname"${BASH_SOURCE[0]}")/../.."&&pwd)"LOGS="${ROOT}/audit/logs"# --log is the outer run naming the file for the one it starts in tmux.NO_TMUX=0log=''while[$#-gt0];docase"$1"in--no-tmux)NO_TMUX=1;shift;;--log)log="${2:?--log needs a file}";shift2;;*)break;;esacdoneif[$#-eq0];thensed-n'2,21p'"${BASH_SOURCE[0]}"|sed's/^# \{0,1\}//'exit2fiORIG_ARGS=("$@")if["$1"='--'];thenshift[$#-gt0]||{echo"error: nothing after --">&2;exit2;}label="cmd-$(basename"$1")"steps=("$(printf'%q '"$@")")elselabel="$(IFS=+;echo"$*")"steps=()fortargetin"$@";dosteps+=("make $(printf'%q'"${target}")")donefiumask077mkdir-p"${LOGS}"chmod0700"${ROOT}/audit""${LOGS}"log="${log:-${LOGS}/$(date+%Y%m%d-%H%M%S)-${label//[^A-Za-z0-9+._-]/_}.log}"# Run the rest inside tmux: its server keeps the pane's terminal open when this one goes, so the# steps see no hangup and a sudo ticket, which is bound to that terminal, stays good. Each run# gets its own session, so a second capture can read the logs while a long one waits.if["${NO_TMUX}"-eq0]&&[-z"${TMUX:-}"]&&{:>/dev/tty;}2>/dev/null;thenifcommand-vtmux>/dev/null;then# Quoted by hand: tmux hands this to a shell that may not read printf %q's $'…' forms.sq(){printf"'%s'""${1//\'/\'\\\'\'}";}tmux_session="euler-capture-$$"rc_file="$(mktemp"${TMPDIR:-/tmp}/euler-capture-rc-XXXXXX")"inner="bash $(sq"${ROOT}/scripts/ops/capture-run.sh") --no-tmux --log $(sq"${log}")"forargin"${ORIG_ARGS[@]}";doinner+=" $(sq"${arg}")";done# tmux reports its own status, never the command's. If this outer run has gone, nobody# is left to read the file; the log carries the status.inner+="; rc=\$?; if kill -0 $$ 2> /dev/null; then printf '%s' \"\$rc\" > $(sq"${rc_file}")"inner+="; else rm -f $(sq"${rc_file}"); fi"echo"Running inside tmux session '${tmux_session}' (Ctrl-b d to detach)."# A tmux server already running would otherwise hand the pane its own, older PATH.tmuxnew-session-s"${tmux_session}"-c"${ROOT}"-e"PATH=${PATH}""${inner}"rc="$(cat"${rc_file}"2>/dev/null)"if[-n"${rc}"];thenrm-f"${rc_file}"echo"log: ${log} (rc=${rc})"exit"${rc}"fiiftmuxhas-session-t"${tmux_session}"2>/dev/null;thenecho"Detached: still running in '${tmux_session}'."echo" tmux attach -t ${tmux_session} tail -f ${log}"exit0firm-f"${rc_file}"echo"error: the tmux session ended without reporting a status; read ${log}">&2exit1fiecho"note: tmux is not installed, so this run ends with this terminal.">&2fi# The session script: a header (what, where, which commit), each step with its status, and a# footer. It stops at the first failing step, and its exit status is that step's.session="$(mktemp)"trap'rm -f "${session}"'EXIT{echo'set -uo pipefail'# The steps run with the ordinary umask, not this wrapper's 077: a venv rebuild run as# root would otherwise leave every package unreadable to the service users.echo'umask 022'echo"cd $(printf'%q'"${ROOT}")"echo'echo "═══ scripts/ops/capture-run.sh — $(date -Is)"'echo'echo " user: $(id -un) host: $(hostname)"'echo'echo " repo: $(git describe --tags --always --dirty) on $(git branch --show-current) ($(git rev-parse --short HEAD))"'echo'echo " status: $(git status --short | wc -l) changed path(s)"'forstepin"${steps[@]}";doecho"echo; echo \"═══ \$(date +%T) \$ $(printf'%s'"${step}"|sed's/["\\$`]/\\&/g')\""echo"${step}"echo'rc=$?'echo"echo \"═══ \$(date +%T) rc=\${rc}\""echo'[ "${rc}" -eq 0 ] || { echo "═══ stopped: step failed"; exit "${rc}"; }'doneecho'echo; echo "═══ all steps succeeded — $(git describe --tags --always --dirty)"'}>"${session}"script-q-e-c"bash ${session}""${log}"rc=$?chmod0600"${log}"ln-sfn"$(basename"${log}")""${LOGS}/latest.log"echoecho"log: ${log} (rc=${rc})"exit"${rc}"