#!/usr/bin/env python3 """Create a sanitized GitHub source ZIP with one assetmanager/ root folder.""" from __future__ import annotations import argparse import shutil import subprocess import sys import tempfile from pathlib import Path from zipfile import ZIP_DEFLATED, ZipFile ROOT = Path(__file__).resolve().parents[1] EXCLUDED_NAMES = { ".git", ".venv", "venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", "dist", "THIRD_PARTY_LICENSES", } EXCLUDED_FILES = { ".env", "data/config/config.json", "data/config/APPINFO.json", "app/main_old.py", "assetmanager.db", ".public-release-local-patterns", } RUNTIME_DIRS = { "data/postgres", "data/uploads", "data/logs", "data/backups", "data/backup", "data/scripts", "app/static/uploads", } def should_exclude(relative: Path) -> bool: posix = relative.as_posix() if any(part in EXCLUDED_NAMES for part in relative.parts): return True if posix in EXCLUDED_FILES: return True if relative.name.startswith(".env") and relative.name != ".env.example": return True if relative.suffix.lower() in {".db", ".sqlite", ".sqlite3", ".pyc", ".zip"}: return True return any(posix == directory or posix.startswith(directory + "/") for directory in RUNTIME_DIRS) def copy_source(destination: Path) -> None: for source in ROOT.rglob("*"): relative = source.relative_to(ROOT) if should_exclude(relative): continue target = destination / relative if source.is_dir(): target.mkdir(parents=True, exist_ok=True) elif source.is_file(): target.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(source, target) for directory in sorted(RUNTIME_DIRS): marker = destination / directory / ".gitkeep" marker.parent.mkdir(parents=True, exist_ok=True) marker.touch() def make_zip(source_root: Path, output: Path) -> None: output.parent.mkdir(parents=True, exist_ok=True) with ZipFile(output, "w", compression=ZIP_DEFLATED, compresslevel=9) as archive: for path in sorted(source_root.rglob("*")): if path.is_file(): archive.write(path, Path("assetmanager") / path.relative_to(source_root)) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--output", type=Path) args = parser.parse_args() version = (ROOT / "VERSION").read_text(encoding="utf-8").strip() output = (args.output or ROOT / "dist" / f"assetmanager-github-{version}.zip").resolve() with tempfile.TemporaryDirectory(prefix="assetmanager-public-") as temp_name: staged = Path(temp_name) / "assetmanager" staged.mkdir() copy_source(staged) checker = staged / "tools" / "check_public_release.py" command = [sys.executable, str(checker), "--root", str(staged)] denylist = ROOT / ".public-release-local-patterns" if denylist.is_file(): command.extend(["--denylist", str(denylist)]) result = subprocess.run(command) if result.returncode: print("GitHub ZIP was not created because the staged source failed validation.", file=sys.stderr) return result.returncode make_zip(staged, output) print(output) return 0 if __name__ == "__main__": raise SystemExit(main())