Files
enuxia-suite/scripts/clone-locked-apps.py
2026-06-23 00:13:27 +02:00

161 lines
4.2 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import json
import re
import subprocess
from pathlib import Path
COMMIT_RE = re.compile(r"^[0-9a-f]{40}$")
def command(args, cwd=None, capture=False):
result = subprocess.run(
args,
cwd=cwd,
check=True,
text=True,
stdout=subprocess.PIPE if capture else None,
)
return result.stdout.strip() if capture else ""
def load_lock(path):
data = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise SystemExit("Le verrou doit être un objet JSON.")
if "frappe" not in data:
raise SystemExit("Entrée frappe absente du verrou.")
entries = []
for name, metadata in data.items():
if not isinstance(metadata, dict):
raise SystemExit(f"Métadonnées invalides pour {name}.")
resolution = metadata.get("resolution") or {}
url = metadata.get("url")
branch = resolution.get("branch")
commit = resolution.get("commit_hash")
idx = metadata.get("idx")
if not isinstance(url, str) or not url.startswith("https://"):
raise SystemExit(f"URL publique invalide pour {name}: {url!r}")
if not isinstance(branch, str) or not branch:
raise SystemExit(f"Branche absente pour {name}.")
if not isinstance(commit, str) or not COMMIT_RE.fullmatch(commit):
raise SystemExit(f"Commit invalide pour {name}: {commit!r}")
if not isinstance(idx, int):
raise SystemExit(f"Index invalide pour {name}: {idx!r}")
entries.append((name, metadata))
entries.sort(key=lambda item: item[1]["idx"])
expected = list(range(1, len(entries) + 1))
actual = [metadata["idx"] for _, metadata in entries]
if actual != expected:
raise SystemExit(f"Indices non continus dans le verrou: {actual!r}")
return entries
def verify_head(repository, expected_commit, app_name):
actual = command(
["git", "rev-parse", "HEAD"],
cwd=repository,
capture=True,
)
if actual != expected_commit:
raise SystemExit(
f"Commit incorrect pour {app_name}: "
f"attendu={expected_commit} obtenu={actual}"
)
def main():
parser = argparse.ArgumentParser(
description="Clone les applications Frappe aux commits du verrou."
)
parser.add_argument("lock_file", type=Path)
parser.add_argument("bench_path", type=Path)
parser.add_argument("--validate-only", action="store_true")
args = parser.parse_args()
entries = load_lock(args.lock_file)
app_entries = [
(name, metadata)
for name, metadata in entries
if name != "frappe"
]
if args.validate_only:
print(
f"Verrou valide: {len(entries)} entrées, "
f"{len(app_entries)} applications."
)
return
bench = args.bench_path.resolve()
apps_dir = bench / "apps"
sites_dir = bench / "sites"
frappe_metadata = dict(entries)["frappe"]
verify_head(
apps_dir / "frappe",
frappe_metadata["resolution"]["commit_hash"],
"frappe",
)
for name, metadata in app_entries:
target = apps_dir / name
if target.exists():
raise SystemExit(f"Répertoire déjà présent: {target}")
resolution = metadata["resolution"]
branch = resolution["branch"]
commit = resolution["commit_hash"]
url = metadata["url"]
print(f"Clonage de {name} @ {commit}")
command(
[
"git",
"clone",
"--filter=blob:none",
"--no-checkout",
"--single-branch",
"--branch",
branch,
url,
str(target),
]
)
command(
["git", "checkout", "--detach", commit],
cwd=target,
)
verify_head(target, commit, name)
app_names = [name for name, _ in entries]
(sites_dir / "apps.txt").write_text(
"\n".join(app_names) + "\n",
encoding="utf-8",
)
print(f"Applications verrouillées clonées: {len(app_entries)}")
print("Frappe et applications vérifiés: OK")
if __name__ == "__main__":
main()