"""scripts/backup_git_bundle.py — laptop-death insurance for git history. Runs ``git bundle create --all`` to pack every commit, branch, and tag in the repo into a single binary file, then uploads it to S3 under ``backups/``. To restore from a bundle on a different machine: aws s3 cp s3:///backups/.bundle ./backup.bundle git clone backup.bundle restored-repo You get back the full history of every branch. The bundle does NOT include ``.git/config`` (so embedded auth tokens stay behind), working-tree files (e.g. .env), or anything not in the object graph. Config (.env or env vars): S3_BUCKET, AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY S3_BACKUPS_PREFIX default 'backups/' Schedule this weekly via Task Scheduler (Windows) or cron (Linux): python scripts/backup_git_bundle.py """ from __future__ import annotations import os import subprocess import sys from datetime import date from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT)) try: from dotenv import load_dotenv load_dotenv(ROOT / ".env") except ImportError: pass BUCKET = os.environ.get("S3_BUCKET") REGION = os.environ.get("AWS_REGION") PREFIX = os.environ.get("S3_BACKUPS_PREFIX", "backups/") def main() -> int: if not BUCKET or not REGION: print("ERROR: S3_BUCKET and AWS_REGION required.", file=sys.stderr) return 1 bundle_name = f"backup-{date.today().isoformat()}.bundle" bundle_path = ROOT / bundle_name print(f"Creating bundle: {bundle_name}") res = subprocess.run( ["git", "bundle", "create", str(bundle_path), "--all"], cwd=ROOT, capture_output=True, text=True, ) if res.returncode != 0: print(f"ERROR creating bundle: {res.stderr.strip()}", file=sys.stderr) return 1 size_mb = bundle_path.stat().st_size / 1024 / 1024 print(f"Bundle size: {size_mb:.1f} MB") # Verify the bundle is well-formed before uploading. verify = subprocess.run( ["git", "bundle", "verify", str(bundle_path)], cwd=ROOT, capture_output=True, text=True, ) if verify.returncode != 0: print(f"ERROR: bundle verification failed: {verify.stderr.strip()}", file=sys.stderr) bundle_path.unlink(missing_ok=True) return 1 import boto3 from botocore.config import Config from botocore.exceptions import ClientError s3 = boto3.client( "s3", region_name=REGION, aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"], aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"], endpoint_url=f"https://s3.{REGION}.amazonaws.com", config=Config(signature_version="s3v4", s3={"addressing_style": "virtual"}), ) key = f"{PREFIX}{bundle_name}" try: s3.upload_file(str(bundle_path), BUCKET, key, ExtraArgs={"ContentType": "application/octet-stream"}) print(f"Uploaded -> s3://{BUCKET}/{key}") except ClientError as e: print(f"ERROR uploading: {e}", file=sys.stderr) return 1 finally: # Local bundle was a temp file; clean it up. bundle_path.unlink(missing_ok=True) print() print("Backup complete.") return 0 if __name__ == "__main__": raise SystemExit(main())