Add release ZIP validation

Add release ZIP validation

- Add a Python release guard for plugin ZIP assets.
- Validate flat ZIP layout, manifest names, cache-file exclusion, and the standalone Liquiditätsplanung generator.
- Run the guard from the release workflow before publishing assets.

Validated with structure checks, Claude plugin marketplace validation, Python compile, diff check, and local ZIP simulation for all 17 plugin ZIPs.
This commit is contained in:
Klotzkette
2026-05-19 11:29:24 -07:00
committed by GitHub
parent 00171d9145
commit ceb2af7c1d
2 changed files with 72 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Validate plugin release ZIPs before publishing them."""
from __future__ import annotations
import json
import sys
import zipfile
from pathlib import Path
def fail(message: str) -> None:
print(f"validate-release-zips failed: {message}", file=sys.stderr)
raise SystemExit(1)
def zip_names(zip_path: Path) -> set[str]:
try:
with zipfile.ZipFile(zip_path) as archive:
return {name.replace("\\", "/") for name in archive.namelist()}
except zipfile.BadZipFile as exc:
fail(f"{zip_path}: invalid ZIP: {exc}")
def validate_plugin_zip(dist_dir: Path, plugin_name: str) -> None:
zip_path = dist_dir / f"{plugin_name}.zip"
if not zip_path.exists():
fail(f"{zip_path}: missing plugin ZIP")
names = zip_names(zip_path)
if ".claude-plugin/plugin.json" not in names:
fail(f"{zip_path}: .claude-plugin/plugin.json must be at ZIP root")
if f"{plugin_name}/.claude-plugin/plugin.json" in names:
fail(f"{zip_path}: ZIP is nested under {plugin_name}/; upload ZIPs must be flat")
if any(name.startswith(f"{plugin_name}/") for name in names):
fail(f"{zip_path}: contains nested {plugin_name}/ root")
if any("__pycache__/" in name or name.endswith(".pyc") for name in names):
fail(f"{zip_path}: contains Python cache files")
with zipfile.ZipFile(zip_path) as archive:
manifest = json.loads(archive.read(".claude-plugin/plugin.json"))
if manifest.get("name") != plugin_name:
fail(f"{zip_path}: manifest name {manifest.get('name')!r} does not match {plugin_name!r}")
if plugin_name == "liquiditaetsplanung":
generator = "skills/liquiditaetsvorschau-3-6-12-monate/werkzeuge/build_liquiditaetsplan.py"
if generator not in names:
fail(f"{zip_path}: missing standalone Liquiditätsplan generator {generator}")
def main() -> None:
dist_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("dist")
marketplace_path = Path(sys.argv[2]) if len(sys.argv) > 2 else Path(".claude-plugin/marketplace.json")
marketplace = json.loads(marketplace_path.read_text(encoding="utf-8"))
plugins = [plugin["name"] for plugin in marketplace["plugins"]]
for plugin_name in plugins:
validate_plugin_zip(dist_dir, plugin_name)
marketplace_zip_copy = dist_dir / "marketplace.json"
if not marketplace_zip_copy.exists():
fail(f"{marketplace_zip_copy}: missing marketplace.json release asset")
json.loads(marketplace_zip_copy.read_text(encoding="utf-8"))
print(f"validate-release-zips OK ({len(plugins)} plugin ZIPs)")
if __name__ == "__main__":
main()