← Files Marketing CouncilARCHIVED FILE
scripts/build_host_packages.py
6.62 KB · Oct 2, 2026 · 00:31 UTC
#!/usr/bin/env python3
"""Build deterministic Marketing Council release archives for supported hosts."""
from __future__ import annotations
import argparse
import hashlib
import json
import shutil
import tempfile
import zipfile
from pathlib import Path
from build_dist import build_dist
ROOT = Path(__file__).resolve().parents[1]
FIXED_ZIP_TIME = (2026, 9, 3, 0, 0, 0)
RUNTIME_DIRS = ("skills", "agents", "hooks", "references", "neural", "routing", "scripts", "tools", "workflows", "assets")
ROOT_RUNTIME_FILES = ("manifest.json", "LICENSE", "README.md", "SECURITY.md")
SOURCE_EXCLUDES = {".git", "dist", "__pycache__", ".pytest_cache", ".DS_Store"}
def reject_symlink(path: Path, source_root: Path) -> None:
if path.is_symlink():
try:
rel = path.relative_to(source_root)
except ValueError:
rel = path
raise ValueError(f"symlink source not allowed: {rel}")
def version() -> str:
path = ROOT / "manifest.json"
reject_symlink(path, ROOT)
return json.loads(path.read_text(encoding="utf-8"))["version"]
def copy_tree_clean(src: Path, dst: Path) -> None:
if not src.exists():
return
reject_symlink(src, src)
for path in sorted(src.rglob("*")):
reject_symlink(path, src)
if path.is_dir():
continue
if any(part in SOURCE_EXCLUDES for part in path.parts):
continue
if path.suffix in {".pyc", ".pyo"}:
continue
rel = path.relative_to(src)
target = dst / rel
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(path, target)
def copy_runtime(dst: Path, *, plugin_manifest: str) -> None:
dst.mkdir(parents=True, exist_ok=True)
manifest_src = ROOT / plugin_manifest
reject_symlink(manifest_src, ROOT)
manifest_dst = dst / plugin_manifest
manifest_dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(manifest_src, manifest_dst)
for name in RUNTIME_DIRS:
copy_tree_clean(ROOT / name, dst / name)
for name in ROOT_RUNTIME_FILES:
src = ROOT / name
if src.exists():
reject_symlink(src, ROOT)
shutil.copy2(src, dst / name)
def deterministic_zip(source: Path, archive: Path, *, prefix: str = "") -> Path:
archive.parent.mkdir(parents=True, exist_ok=True)
if archive.exists():
archive.unlink()
reject_symlink(source, source)
with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
for path in sorted(source.rglob("*")):
reject_symlink(path, source)
if not path.is_file():
continue
rel = path.relative_to(source).as_posix()
arcname = f"{prefix.rstrip('/')}/{rel}" if prefix else rel
info = zipfile.ZipInfo(arcname, FIXED_ZIP_TIME)
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o100644 << 16
info.create_system = 3
zf.writestr(info, path.read_bytes(), compress_type=zipfile.ZIP_DEFLATED, compresslevel=9)
return archive
def build_openai(output_root: Path, ver: str) -> Path:
with tempfile.TemporaryDirectory() as td:
root = Path(td) / "plugin"
copy_runtime(root, plugin_manifest=".codex-plugin/plugin.json")
return deterministic_zip(root, output_root / f"marketing-council-openai-plugin-v{ver}.zip")
def build_claude(output_root: Path, ver: str) -> Path:
with tempfile.TemporaryDirectory() as td:
market = Path(td) / "marketplace"
plugin = market / "plugins" / "marketing-council"
copy_runtime(plugin, plugin_manifest=".claude-plugin/plugin.json")
marketplace_path = ROOT / ".claude-plugin" / "marketplace.json"
reject_symlink(marketplace_path, ROOT)
marketplace = json.loads(marketplace_path.read_text(encoding="utf-8"))
marketplace["plugins"][0]["source"] = "./plugins/marketing-council"
market_meta = market / ".claude-plugin" / "marketplace.json"
market_meta.parent.mkdir(parents=True, exist_ok=True)
market_meta.write_text(json.dumps(marketplace, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
for source, target in (
(ROOT / "README.md", market / "README.md"),
(ROOT / "LICENSE", market / "LICENSE"),
):
reject_symlink(source, ROOT)
shutil.copy2(source, target)
return deterministic_zip(market, output_root / f"marketing-council-claude-marketplace-v{ver}.zip")
def build_skill(output_root: Path, ver: str) -> Path:
with tempfile.TemporaryDirectory() as td:
skill = Path(td) / "marketing-council"
build_dist(skill)
return deterministic_zip(skill, output_root / f"marketing-council-skill-v{ver}.zip", prefix="marketing-council")
def build_source(output_root: Path, ver: str) -> Path:
with tempfile.TemporaryDirectory() as td:
source = Path(td) / "marketing-council-pack"
source.mkdir(parents=True)
for path in sorted(ROOT.rglob("*")):
reject_symlink(path, ROOT)
if path.is_dir():
continue
rel = path.relative_to(ROOT)
if any(part in SOURCE_EXCLUDES for part in rel.parts):
continue
if path.suffix in {".pyc", ".pyo"}:
continue
target = source / rel
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(path, target)
return deterministic_zip(source, output_root / f"marketing-council-pack-v{ver}.zip", prefix="marketing-council-pack")
def sha256(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest()
def build_all(output_root: Path) -> list[Path]:
ver = version()
output_root.mkdir(parents=True, exist_ok=True)
archives = [
build_openai(output_root, ver),
build_claude(output_root, ver),
build_skill(output_root, ver),
build_source(output_root, ver),
]
sums = output_root / f"marketing-council-v{ver}-SHA256SUMS.txt"
sums.write_text("".join(f"{sha256(path)} {path.name}\n" for path in archives), encoding="utf-8")
return archives
def main() -> None:
parser = argparse.ArgumentParser(description="Build deterministic Marketing Council host packages.")
parser.add_argument("--output-root", type=Path, default=ROOT / "dist" / "release")
args = parser.parse_args()
archives = build_all(args.output_root.resolve())
print(json.dumps({"version": version(), "archives": [str(p) for p in archives]}, indent=2))
if __name__ == "__main__":
main()
SHA-256: 3c9f08c56271f320a6f8617fb5a4939d87d5c7cf7d64923dbe682dcbc113891b