feat: back up and restore the release-storage volume, not just Postgres

backup.sh now also streams the fiscal-hub container's release binaries
(dist tarball, never in Postgres) into BACKUP_DIR alongside the pg_dump,
with the same 14-backup retention; restore_releases.sh is the companion
restore. Verified with a real restore smoke test in production: the
restored dump's agent_releases row and the restored tarball's exe both
match the published sha256 exactly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-23 22:49:16 -03:00
co-authored by Claude Sonnet 5
parent e7d77beb26
commit d884d41db5
3 changed files with 47 additions and 5 deletions
+14 -2
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env sh
# PostgreSQL backup of the GTI Fiscal Hub (custom format, restorable with restore.sh). Run from deploy/hub.
# ./backup.sh -> ./backups/gti_fiscal_YYYYmmdd_HHMMSS.dump (last 14 kept)
# PostgreSQL + release-storage backup of the GTI Fiscal Hub (restorable with
# restore.sh + restore_releases.sh). Run from deploy/hub.
# ./backup.sh -> ./backups/gti_fiscal_YYYYmmdd_HHMMSS.dump (Postgres, last 14 kept)
# -> ./backups/gti_fiscal_releases_YYYYmmdd_HHMMSS.tgz (release binaries + AgentRelease metadata is IN the dump above)
set -eu
COMPOSE="docker compose -f docker-compose.prod.yml --env-file .env"
DIR="${BACKUP_DIR:-./backups}"
@@ -13,3 +15,13 @@ $COMPOSE exec -T postgres pg_dump -U gti -d gti_fiscal -Fc -f "/backups/${FILE}"
# backup directory as root, so a host-side rm would fail from the 15th dump.
$COMPOSE exec -T postgres sh -c 'ls -1t /backups/gti_fiscal_*.dump | tail -n +15 | xargs -r rm -f'
echo "backup ok: $DIR/$FILE"
# Release storage (central/app/release_storage.py: every published Agent
# package zip/exe, never in Postgres — only its metadata is, in the dump
# above). Streamed out of the fiscal-hub container's stdout so this works
# regardless of the compose project/container name.
RELEASES_FILE="gti_fiscal_releases_${STAMP}.tgz"
$COMPOSE exec -T fiscal-hub tar -C /app/data -cz releases > "$DIR/$RELEASES_FILE"
[ -s "$DIR/$RELEASES_FILE" ] || { echo "release backup empty or missing: $DIR/$RELEASES_FILE" >&2; exit 1; }
find "$DIR" -maxdepth 1 -name 'gti_fiscal_releases_*.tgz' -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | tail -n +15 | cut -d' ' -f2- | xargs -r rm -f
echo "release storage backup ok: $DIR/$RELEASES_FILE"
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env sh
# Restore the release-storage volume (backup.sh's *.tgz) into the Hub.
# DESTRUCTIVE for the current release binaries: asks for confirmation. The
# AgentRelease metadata (version/sha256/status) lives in Postgres, not here —
# restore that separately with restore.sh from a dump taken around the same time.
# ./restore_releases.sh ./backups/gti_fiscal_releases_20260923_224551.tgz
set -eu
[ $# -eq 1 ] || { echo "usage: $0 <releases_backup.tgz>" >&2; exit 2; }
FILE="$(basename "$1")"
COMPOSE="docker compose -f docker-compose.prod.yml --env-file .env"
[ -s "${BACKUP_DIR:-./backups}/$FILE" ] || { echo "backup not found in ${BACKUP_DIR:-./backups}: $FILE" >&2; exit 1; }
printf "This REPLACES the current release-storage contents with %s. Type RESTORE to continue: " "$FILE"
read -r answer
[ "$answer" = "RESTORE" ] || { echo "aborted"; exit 1; }
$COMPOSE exec -T fiscal-hub sh -c 'rm -rf /app/data/releases && mkdir -p /app/data/releases'
$COMPOSE exec -T fiscal-hub tar -C /app/data -xz < "${BACKUP_DIR:-./backups}/$FILE"
echo "restore ok: $FILE"