From 0bc117c8687a39b6b42e644cf3bae03278f4c73a Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 26 Aug 2026 22:30:47 -0700 Subject: [PATCH] Add: git-guard.sh - pre-deploy sync check to prevent local/Gitea divergence --- deploy/git-guard.sh | 142 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 deploy/git-guard.sh diff --git a/deploy/git-guard.sh b/deploy/git-guard.sh new file mode 100644 index 0000000..ab0f622 --- /dev/null +++ b/deploy/git-guard.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# git-guard.sh — Ensures the compose-files working tree is in sync with Gitea +# before any deploy proceeds. Called automatically by stack-deploy.sh. +# +# Behavior: +# - Clean + up to date -> pass silently +# - Clean + behind (ff-only) -> auto `git pull --ff-only`, then pass +# - Ahead only (unpushed) -> interactive: offer to push; non-interactive: BLOCK +# - Dirty tracked changes -> offer to commit + push right now +# (auto in non-interactive/CI runs, after a +# secret-pattern scan of the staged diff) +# - Diverged (local AND -> REFUSE. Never auto-resolves. Prints the +# remote both moved) backup/stash/reset recovery steps and exits. +# +# Exit codes: 0 = safe to deploy, 1 = blocked, needs human intervention + +set -euo pipefail + +DIR="/volume1/docker/compose-files" +cd "$DIR" + +# Non-interactive detection (Woodpecker/cron have no TTY on stdin) +INTERACTIVE=0 +[ -t 0 ] && INTERACTIVE=1 + +echo "==> git-guard: checking repo sync state" + +git fetch origin --quiet + +LOCAL="$(git rev-parse main)" +REMOTE="$(git rev-parse origin/main)" +BASE="$(git merge-base main origin/main)" + +DIRTY=0 +git status --porcelain | grep -q . && DIRTY=1 + +SECRET_PATTERN='(-----BEGIN [A-Z]+ PRIVATE KEY-----|AKIA[0-9A-Z]{16}|xox[baprs]-[0-9a-zA-Z-]+|password[[:space:]]*[:=][[:space:]]*[^$ ]|api[_-]?key[[:space:]]*[:=][[:space:]]*[^$ ])' + +# ---- Case: dirty tracked changes ---- +if [ "$DIRTY" -eq 1 ]; then + echo "!! WORKING TREE DIRTY — uncommitted changes detected:" + git status --short + echo + + if [ "$INTERACTIVE" -eq 1 ]; then + read -rp "Commit and push these changes to origin/main now? [y/N] " ans + else + ans="y" + echo "(non-interactive session — auto-committing and pushing)" + fi + + if [[ "$ans" =~ ^[Yy]$ ]]; then + git add -A + + if git diff --cached | grep -Eiq "$SECRET_PATTERN"; then + echo "ERROR: possible secret detected in staged changes. Refusing to auto-commit." + echo "Review manually: git diff --cached" + git reset + exit 1 + fi + + git commit -m "chore(auto): git-guard autofix - commit local changes before deploy $(date -u +%Y-%m-%dT%H:%M:%SZ)" + + if git push origin main; then + echo "==> Pushed. Re-checking sync state..." + exec "$0" "$@" + else + echo "ERROR: push failed (likely diverged from origin). Aborting deploy." + echo "Run: cd $DIR && git status" + exit 1 + fi + else + echo "Aborting deploy - commit or stash changes manually, then retry." + exit 1 + fi +fi + +# ---- Case: fully in sync ---- +if [ "$LOCAL" = "$REMOTE" ]; then + echo "==> In sync with origin/main ($LOCAL). OK to deploy." + exit 0 +fi + +# ---- Case: behind only (fast-forwardable) ---- +if [ "$LOCAL" = "$BASE" ]; then + echo "!! Local main is behind origin/main." + if [ "$INTERACTIVE" -eq 1 ]; then + read -rp "Fast-forward pull now? [y/N] " ans + else + ans="y" + echo "(non-interactive session — auto fast-forwarding)" + fi + if [[ "$ans" =~ ^[Yy]$ ]]; then + git pull --ff-only origin main + echo "==> Fast-forwarded to $(git rev-parse --short main). OK to deploy." + exit 0 + else + echo "Aborting deploy - pull manually, then retry." + exit 1 + fi +fi + +# ---- Case: ahead only (local commits not yet pushed) ---- +if [ "$REMOTE" = "$BASE" ]; then + echo "!! Local main is AHEAD of origin/main (unpushed commits):" + git log --oneline "origin/main..main" + echo + if [ "$INTERACTIVE" -eq 1 ]; then + read -rp "Push local commits to origin/main now? [y/N] " ans + else + ans="n" + echo "(non-interactive session — will NOT auto-push ahead commits; needs human review)" + fi + if [[ "$ans" =~ ^[Yy]$ ]]; then + git push origin main + echo "==> Pushed. OK to deploy." + exit 0 + else + echo "Aborting deploy. Review with: git log origin/main..main" + exit 1 + fi +fi + +# ---- Case: true divergence (both ahead and behind) — NEVER auto-fix ---- +echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" +echo "!! DIVERGED: local main and origin/main have both moved independently." +echo "!!" +echo "!! Local-only commits:" +git log --oneline "$BASE..main" | sed 's/^/!! /' +echo "!!" +echo "!! Remote-only commits:" +git log --oneline "$BASE..origin/main" | sed 's/^/!! /' +echo "!!" +echo "!! This requires a human decision - git-guard will NOT auto-resolve this." +echo "!! Recommended recovery:" +echo "!! 1. tar backup: tar czf /volume1/docker/compose-files-backup-\$(date +%Y%m%d-%H%M%S).tar.gz -C /volume1/docker compose-files" +echo "!! 2. name the branch: git branch backup/pre-reset-\$(date +%Y%m%d)" +echo "!! 3. stash all state: git stash push -u -m 'pre-reset-snapshot'" +echo "!! 4. reset to origin: git reset --hard origin/main" +echo "!! 5. selectively restore needed files from the stash/backup branch" +echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" +exit 1