Files
hnefatafl/deploy/hnefatafl-backup.sh
T
Eric WagonerandClaude Opus 5.5 57ae338cdb The rollup and the backup check in once, when they end, with the run's duration
The rollup finishes in two seconds on the shared host, and its "in progress"
and "ok" check-ins reached Sentry in either order; when "ok" landed first,
the late start was never closed and the monitor reported a timeout ten
minutes later. One check-in at the end cannot race, and a run that hangs
still shows: nothing arrives and the night is marked missed.

Fixes TAFL-1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwFKMuQnPAEHJ5yA1q4orh
2026-10-10 17:44:43 -04:00

52 lines
2.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Nightly ledger backup to DigitalOcean Spaces, one prefix per game in the shared bucket.
# current/ - exact mirror of /var/lib/hnefatafl
# snapshots/ - one dated copy per day, pruned after 90 days
# Ledgers are append-only JSONL; the game server never needs stopping.
# Sentry Crons check-in: /root/.hnefatafl-sentry-cron holds the check-in
# URL (absent file = no check-ins). SCHEDULE must match /etc/cron.d/hnefatafl.
set -u
SCHEDULE="23 7 * * *"
BUCKET="kestrel-wizwar-backups"
PREFIX="hnefatafl"
SRC="/var/lib/hnefatafl"
LOG="/var/log/hnefatafl/backup.log"
STAMP=$(date +%Y-%m-%d)
mkdir -p "$(dirname "$LOG")"
CRON_URL=$(cat /root/.hnefatafl-sentry-cron 2>/dev/null || true)
# One check-in, when the run ends, carrying its duration: a start and a
# finish sent seconds apart can reach Sentry in either order, and a start
# that lands second is never closed. A run that never ends sends nothing,
# and Sentry marks the night missed.
START=$SECONDS
checkin() {
[ -n "$CRON_URL" ] && curl -sf -o /dev/null -X POST -H "Content-Type: application/json" \
-d "{\"status\":\"$1\",\"duration\":$((SECONDS - START)),\"monitor_config\":{\"schedule\":{\"type\":\"crontab\",\"value\":\"$SCHEDULE\"},\"checkin_margin\":30,\"max_runtime\":10,\"timezone\":\"UTC\"}}" \
"$CRON_URL" || true
}
if [ ! -s /root/.config/rclone/rclone.conf ]; then
echo "$(date -Is) skipped: rclone is not configured" >> "$LOG"
exit 0
fi
{
echo "=== $(date -Is) backup start"
if rclone sync "$SRC" "spaces:$BUCKET/$PREFIX/current" --exclude 'rollup.jsonl' 2>&1 &&
rclone copy "$SRC" "spaces:$BUCKET/$PREFIX/snapshots/$STAMP" 2>&1; then
checkin ok
else
checkin error
fi
CUTOFF=$(date -d "90 days ago" +%Y-%m-%d)
rclone lsf "spaces:$BUCKET/$PREFIX/snapshots/" --dirs-only 2>/dev/null | \
while read -r d; do
day="${d%/}"
if [[ "$day" < "$CUTOFF" ]]; then
echo "pruning snapshot $day"
rclone purge "spaces:$BUCKET/$PREFIX/snapshots/$day" 2>&1
fi
done
echo "=== $(date -Is) backup done"
} >> "$LOG" 2>&1