Files
6krrt/deploy/offsite-sync.sh
adlee-was-taken 4a32b0180f feat(deploy): 6-hourly off-site backup to a private git repo
Closes the last gap from docs/incidents.md #5. The hourly local snapshots
survive `git clean -fdx` because they live outside the repo, but they do not
survive the disk. This pushes the irreplaceable-and-small state to a private
Gitea repo: .omo plans and evidence ledger, tuned systemd units, opencode
plugins, this project's Claude memory, config/config.local.yaml, and a DAILY
gzipped router.db.

The database is committed daily rather than hourly on purpose: it is binary and
~5MB gzipped, so git cannot delta it. Hourly would grow the repo ~120MB/day
instead of ~5MB. SYNC_DB=0 turns it off entirely.

.env is deliberately NOT synced. There is no usable secret key on this host to
encrypt it to (public keys only), so it would sit in git history in plaintext,
and history is forever even in a private repo. An API key is replaceable by
regenerating it from the provider; 22,821 energy observations are not. It stays
in the local backups only, and the user confirmed that trade.

Verified by fresh clone: 35 plan artifacts, 127 evidence files, 9 systemd
units, 2 opencode plugins, 11 memory files, the tariff, and a 5.1MB db
snapshot. Checked the actual 67-character key VALUE appears in zero off-site
files -- an earlier check grepped for the variable NAME and for 'sk-', which
matches 'task-', and produced 50 false positives. Grep for the secret, not for
its label.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRQXz5SYZYVWscxS1QqF6U
2026-09-04 18:43:23 -04:00

86 lines
3.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Push the irreplaceable-and-small state to a private off-site git repo.
#
# Closes the last gap from docs/incidents.md #5: local snapshots survive
# `git clean -fdx` because they live outside the repo, but they do NOT survive
# the disk. This does.
#
# WHAT IS SYNCED
# .omo/ plan artifacts + evidence ledger (13MB, text, versions well)
# systemd units tuned: graceful-shutdown fix, timers
# opencode plugins session-registry.js etc. -- hand-written, not from a package
# claude memory cross-session project memory
# config.local.yaml operator tariff/overrides (personal, NOT a credential)
# router.db gzipped, at most once per day -- see SIZE below
#
# WHAT IS DELIBERATELY NOT SYNCED
# .env — the provider API key. There is no usable secret key on this host to
# encrypt it to (only public keys), so it would land in git history in
# plaintext, and git history is forever even in a private repo. An API
# key is REPLACEABLE: regenerate it from the provider. 22,000 energy
# observations are not. It stays in the local backups only.
# .venv, node_modules — rebuildable from pinned requirements.
# ollama models — ~30GB, re-pullable; recipes in docs/local-models.md.
#
# SIZE: router.db is ~5MB gzipped and binary, so git cannot delta it. It is
# committed at most DAILY (not hourly) to keep growth near 5MB/day rather than
# 120MB/day. Set SYNC_DB=0 to skip it entirely and rely on local snapshots.
set -euo pipefail
REPO="${REPO:-$HOME/Sources/6krrt}"
REMOTE="${OFFSITE_REMOTE:-ssh://git@git.adlee.work:2222/alee/6kbackups.git}"
WORK="${OFFSITE_WORKDIR:-$HOME/.local/share/6krrt-offsite}"
SYNC_DB="${SYNC_DB:-1}"
if [ ! -d "$WORK/.git" ]; then
git clone "$REMOTE" "$WORK" 2>/dev/null || { mkdir -p "$WORK"; git -C "$WORK" init -q; git -C "$WORK" remote add origin "$REMOTE"; }
fi
cd "$WORK"
git fetch -q origin 2>/dev/null || true
git checkout -q -B main 2>/dev/null || true
git reset -q --hard origin/main 2>/dev/null || true
rsync -a --delete "$REPO/.omo/" ./omo/ 2>/dev/null || true
mkdir -p home config
rsync -a --delete "$HOME/.config/systemd/user/" ./home/systemd-user/ 2>/dev/null || true
rsync -a --delete "$HOME/.config/opencode/" ./home/opencode/ --exclude 'cache/' --exclude 'log/' --exclude '*.log' 2>/dev/null || true
rsync -a --delete "$HOME/.claude/projects/-home-alee-Sources-6krrt/memory/" ./home/claude-memory/ 2>/dev/null || true
[ -f "$REPO/config/config.local.yaml" ] && cp -f "$REPO/config/config.local.yaml" ./config/
# Daily DB snapshot, same filename so the working tree stays flat.
if [ "$SYNC_DB" = "1" ]; then
today=$(date +%Y-%m-%d)
if [ ! -f .db-stamp ] || [ "$(cat .db-stamp)" != "$today" ]; then
tmp=$(mktemp)
sqlite3 "$REPO/router.db" ".backup '$tmp'"
[ "$(sqlite3 "$tmp" 'SELECT integrity_check FROM pragma_integrity_check LIMIT 1;')" = "ok" ] \
&& { gzip -c "$tmp" > router.db.gz; echo "$today" > .db-stamp; } \
|| echo "db snapshot failed integrity_check; not synced" >&2
rm -f "$tmp"
fi
fi
cat > README.md <<'INNER'
# 6krrt workstation backup
Off-site copy of state the main repo does not carry. See
`docs/incidents.md` #5 in the main repo for why this exists.
**Not here on purpose:** `.env` (provider API key — regenerate it instead;
there is no usable secret key on the source host to encrypt it to), `.venv`,
`node_modules`, and Ollama models.
Restore: clone the main repo, rebuild the venv from pinned requirements, then
copy `omo/` back to `.omo/`, `config/config.local.yaml` into place, and
`home/*` to their `~/.config` locations. `router.db.gz` is a daily snapshot.
INNER
git add -A
if git diff --cached --quiet; then
echo "offsite: no changes"
else
git -c user.name="6krrt-backup" -c user.email="backup@localhost" \
commit -q -m "backup $(date -Iseconds)"
git push -q -u origin main && echo "offsite: pushed $(git rev-parse --short HEAD)"
fi