diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f3eef0b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Shell scripts run on the Linux Hub host. With core.autocrlf=true on the +# Windows release machine, `git archive`/checkout would otherwise ship them +# with CRLF and `sh` rejects them ("set: Illegal option -"). +*.sh text eol=lf diff --git a/deploy/hub/backup.sh b/deploy/hub/backup.sh index 69037e7..a41d5b9 100644 --- a/deploy/hub/backup.sh +++ b/deploy/hub/backup.sh @@ -9,5 +9,7 @@ STAMP="$(date +%Y%m%d_%H%M%S)" FILE="gti_fiscal_${STAMP}.dump" $COMPOSE exec -T postgres pg_dump -U gti -d gti_fiscal -Fc -f "/backups/${FILE}" [ -s "$DIR/$FILE" ] || { echo "backup empty or missing: $DIR/$FILE" >&2; exit 1; } -ls -1t "$DIR"/gti_fiscal_*.dump | tail -n +15 | xargs -r rm -f +# Retention runs inside the container: Docker creates the bind-mounted +# 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" diff --git a/tests/test_deploy_scripts.py b/tests/test_deploy_scripts.py new file mode 100644 index 0000000..9516c36 --- /dev/null +++ b/tests/test_deploy_scripts.py @@ -0,0 +1,26 @@ +"""The Hub backup/restore scripts must actually run on the Linux host. + +Both failures below were found by running the scripts on the production VM, +not by reading them. +""" +from pathlib import Path + +ROOT = Path(__file__).parents[1] +HUB = ROOT / "deploy" / "hub" + + +def test_shell_scripts_are_forced_to_lf(): + # With core.autocrlf=true on the Windows release machine, `git archive` + # shipped CRLF and `sh` failed on the first line ("set: Illegal option -"). + rules = [line.split() for line in (ROOT / ".gitattributes").read_text(encoding="utf-8").splitlines() if line.strip() and not line.startswith("#")] + assert ["*.sh", "text", "eol=lf"] in rules + for script in HUB.glob("*.sh"): + assert b"\r" not in script.read_bytes(), script.name + + +def test_backup_retention_does_not_delete_on_the_host(): + # Docker creates the bind-mounted backup directory as root, so a host-side + # rm by the deploy user fails once retention kicks in. + lines = [line.strip() for line in (HUB / "backup.sh").read_text(encoding="utf-8").splitlines() if not line.strip().startswith("#")] + retention = [line for line in lines if "rm -f" in line] + assert retention and all(line.startswith("$COMPOSE exec -T postgres") for line in retention)