CLean up site generation
This commit is contained in:
197
scripts/check_env_sync.py
Normal file
197
scripts/check_env_sync.py
Normal file
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Sync .env.example: keep structure/comments, merge in keys from .env."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
SKIP_DIRS = {".git", "site", "__pycache__"}
|
||||
|
||||
|
||||
class EnvParseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def discover_pairs(repo_root: Path) -> list[tuple[Path, Path]]:
|
||||
pairs: list[tuple[Path, Path]] = []
|
||||
for example_path in sorted(repo_root.rglob(".env.example")):
|
||||
rel_parts = set(example_path.relative_to(repo_root).parts)
|
||||
if rel_parts & SKIP_DIRS:
|
||||
continue
|
||||
env_path = example_path.with_name(".env")
|
||||
pairs.append((example_path, env_path))
|
||||
return pairs
|
||||
|
||||
|
||||
def parse_env_key_values(path: Path) -> tuple[list[str], dict[str, str]]:
|
||||
keys: list[str] = []
|
||||
values: dict[str, str] = {}
|
||||
seen: set[str] = set()
|
||||
|
||||
for idx, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
|
||||
if "=" not in line:
|
||||
raise EnvParseError(f"{path}: line {idx} is not KEY=VALUE format")
|
||||
|
||||
key, value = line.split("=", 1)
|
||||
key = key.strip()
|
||||
if not key:
|
||||
raise EnvParseError(f"{path}: line {idx} has empty key")
|
||||
|
||||
if key in seen:
|
||||
raise EnvParseError(f"{path}: duplicate key '{key}'")
|
||||
seen.add(key)
|
||||
keys.append(key)
|
||||
values[key] = value
|
||||
|
||||
return keys, values
|
||||
|
||||
|
||||
def parse_env_keys(path: Path) -> list[str]:
|
||||
keys, _ = parse_env_key_values(path)
|
||||
return keys
|
||||
|
||||
|
||||
def merge_env_into_example(env_path: Path, example_values: dict[str, str]) -> str:
|
||||
"""
|
||||
Build .env.example from .env structure.
|
||||
.env defines: keys, order, comments.
|
||||
.env.example provides: default values (or placeholders for new keys).
|
||||
"""
|
||||
out_lines: list[str] = []
|
||||
|
||||
# Read .env to get structure/order/comments, fill in values from .env.example
|
||||
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#") or "=" not in raw_line:
|
||||
out_lines.append(raw_line)
|
||||
continue
|
||||
|
||||
key, value_from_env = raw_line.split("=", 1)
|
||||
key = key.strip()
|
||||
|
||||
# Use default from .env.example if it exists, otherwise use placeholder
|
||||
if key in example_values:
|
||||
out_lines.append(f"{key}={example_values[key]}")
|
||||
else:
|
||||
# New key not in template: use placeholder
|
||||
out_lines.append(f"{key}=__CHANGEME__")
|
||||
|
||||
return "\n".join(out_lines) + "\n"
|
||||
|
||||
|
||||
def compare_env(example_path: Path, env_path: Path) -> list[str]:
|
||||
"""Check if .env.example matches .env schema."""
|
||||
errors: list[str] = []
|
||||
|
||||
if not env_path.exists():
|
||||
rel_env = env_path.relative_to(REPO_ROOT)
|
||||
errors.append(f"Missing .env (schema): {rel_env}")
|
||||
return errors
|
||||
|
||||
try:
|
||||
env_keys, _ = parse_env_key_values(env_path)
|
||||
example_keys = parse_env_keys(example_path) if example_path.exists() else []
|
||||
except EnvParseError as exc:
|
||||
errors.append(str(exc))
|
||||
return errors
|
||||
|
||||
example_set = set(example_keys)
|
||||
env_set = set(env_keys)
|
||||
|
||||
missing_in_example = [k for k in env_keys if k not in example_set]
|
||||
|
||||
if missing_in_example:
|
||||
rel_example = example_path.relative_to(REPO_ROOT)
|
||||
errors.append(f"{rel_example}: missing keys from .env schema: {', '.join(missing_in_example)}")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def fix_env_sync(env_path: Path, example_path: Path) -> tuple[bool, str | None]:
|
||||
"""Update .env.example: .env defines schema, .env.example provides defaults."""
|
||||
rel_env = env_path.relative_to(REPO_ROOT)
|
||||
rel_example = example_path.relative_to(REPO_ROOT)
|
||||
|
||||
if not env_path.exists():
|
||||
return False, f"Missing .env file (schema source): {rel_env}"
|
||||
|
||||
try:
|
||||
_, example_values = parse_env_key_values(example_path) if example_path.exists() else ([], {})
|
||||
desired = merge_env_into_example(env_path, example_values)
|
||||
except EnvParseError as exc:
|
||||
return False, str(exc)
|
||||
|
||||
current = example_path.read_text(encoding="utf-8") if example_path.exists() else None
|
||||
if current == desired:
|
||||
return False, None
|
||||
|
||||
example_path.write_text(desired, encoding="utf-8")
|
||||
if current is None:
|
||||
return True, f"Created {rel_example} from {rel_env} schema"
|
||||
return True, f"Synced {rel_example} to match {rel_env}"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--fix",
|
||||
action="store_true",
|
||||
help="Update .env.example from .env schema, keeping default values",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
pairs = discover_pairs(REPO_ROOT)
|
||||
if not pairs:
|
||||
print("No .env.example files found.")
|
||||
return 0
|
||||
|
||||
if args.fix:
|
||||
changed: list[str] = []
|
||||
errors: list[str] = []
|
||||
|
||||
for example_path, env_path in pairs:
|
||||
did_change, message = fix_env_sync(env_path, example_path)
|
||||
if message:
|
||||
if did_change:
|
||||
changed.append(message)
|
||||
else:
|
||||
errors.append(message)
|
||||
|
||||
if changed:
|
||||
print("Synced .env.example from .env schema:\n")
|
||||
for item in changed:
|
||||
print(f"- {item}")
|
||||
print("\nRe-stage changed .env.example files and commit again.")
|
||||
return 1
|
||||
|
||||
if errors:
|
||||
print("Environment file sync failed:\n")
|
||||
for err in errors:
|
||||
print(f"- {err}")
|
||||
return 1
|
||||
|
||||
print(f"Environment files in sync ({len(pairs)} checked).")
|
||||
return 0
|
||||
|
||||
all_errors: list[str] = []
|
||||
for example_path, env_path in pairs:
|
||||
all_errors.extend(compare_env(example_path, env_path))
|
||||
|
||||
if all_errors:
|
||||
print("Environment file validation failed:\n")
|
||||
for err in all_errors:
|
||||
print(f"- {err}")
|
||||
return 1
|
||||
|
||||
print(f"Environment files look good ({len(pairs)} checked).")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user