Files
leocrm/scripts/check_migration_hashes.py
T

102 lines
3.0 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Check that migration files up to and including 0092 have not been modified.
On first run (or with --generate), creates a hash file.
On subsequent runs, verifies that hashes match.
Usage:
python scripts/check_migration_hashes.py # Verify
python scripts/check_migration_hashes.py --generate # Generate/refresh hashes
"""
from __future__ import annotations
import hashlib
import sys
from pathlib import Path
BASE = Path(__file__).resolve().parent.parent
MIGRATIONS_DIR = BASE / "alembic" / "versions"
HASH_FILE = BASE / "alembic" / "migration_hashes.txt"
MAX_REVISION = 92 # Migrations 0001-0092 must not change
def get_migration_files() -> list[Path]:
"""Get all migration files with revision number <= MAX_REVISION."""
files = []
for f in sorted(MIGRATIONS_DIR.glob("*.py")):
# Extract revision number from filename like 0093_fix_...
name = f.stem
if not name[:4].isdigit():
continue
rev = int(name[:4])
if rev <= MAX_REVISION:
files.append(f)
return files
def compute_hash(path: Path) -> str:
"""Compute SHA256 hash of a file."""
h = hashlib.sha256()
with open(path, "rb") as f:
h.update(f.read())
return h.hexdigest()
def generate_hashes() -> None:
"""Generate hash file from current migration files."""
files = get_migration_files()
lines = []
for f in files:
h = compute_hash(f)
lines.append(f"{h} {f.name}")
HASH_FILE.write_text("\n".join(lines) + "\n")
print(f"Generated {len(lines)} hashes in {HASH_FILE}")
def verify_hashes() -> int:
"""Verify that migration hashes match the stored hashes. Returns 0 on success, 1 on failure."""
if not HASH_FILE.exists():
print(f"SKIP: No hash file at {HASH_FILE}. Run with --generate first.")
return 0 # Don't fail CI if no hash file exists yet
stored = {}
for line in HASH_FILE.read_text().strip().split("\n"):
parts = line.split(" ", 1)
if len(parts) == 2:
stored[parts[1]] = parts[0]
files = get_migration_files()
errors = 0
for f in files:
current_hash = compute_hash(f)
if f.name not in stored:
print(f"NEW: {f.name} (not in hash file)")
errors += 1
elif stored[f.name] != current_hash:
print(f"CHANGED: {f.name}")
errors += 1
else:
print(f"OK: {f.name}")
# Check for missing files (in hash file but not on disk)
current_names = {f.name for f in files}
for name in stored:
if name not in current_names:
print(f"MISSING: {name}")
errors += 1
if errors > 0:
print(f"\nFAILED: {errors} migration(s) changed or missing")
return 1
else:
print(f"\nOK: All {len(files)} migration hashes verified")
return 0
if __name__ == "__main__":
if "--generate" in sys.argv:
generate_hashes()
else:
sys.exit(verify_hashes())