Files
open-project/build.py
T
2026-06-27 17:25:55 -05:00

78 lines
2.7 KiB
Python

#!/usr/bin/env python3
"""
Package the openproject plugin into a distributable `.plugin` file.
A `.plugin` is just a zip archive of the plugin tree with
`.claude-plugin/plugin.json` at the root. Timestamps are normalized and entries
are sorted so builds are deterministic (identical input -> identical bytes).
Usage:
python3 build.py # credential-free build (no config.json)
python3 build.py --bake # personal build WITH config.json (your API token!)
Output: <name>-<version>.plugin (or <name>-<version>-baked.plugin with --bake)
"""
from __future__ import annotations
import argparse
import json
import os
import zipfile
ROOT = os.path.dirname(os.path.abspath(__file__))
FIXED_DATE = (2026, 1, 1, 0, 0, 0) # deterministic mtime for every entry
# Directories/files never included in any build.
EXCLUDE_DIRS = {".git", "__pycache__", ".claude"}
EXCLUDE_FILES = {".DS_Store", ".gitignore", "build.py"}
# Excluded unless --bake (carries the API token).
SECRET_FILES = {"config.json"}
def iter_files(bake):
for dirpath, dirnames, filenames in os.walk(ROOT):
dirnames[:] = sorted(d for d in dirnames if d not in EXCLUDE_DIRS)
for fn in sorted(filenames):
if fn in EXCLUDE_FILES:
continue
if fn in SECRET_FILES and not bake:
continue
full = os.path.join(dirpath, fn)
rel = os.path.relpath(full, ROOT)
if rel.endswith(".plugin"):
continue # never nest a previous artifact
yield full, rel
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--bake", action="store_true",
help="include config.json (your API token) in the artifact")
args = ap.parse_args()
with open(os.path.join(ROOT, ".claude-plugin", "plugin.json")) as fh:
meta = json.load(fh)
name, version = meta["name"], meta["version"]
suffix = "-baked" if args.bake else ""
out = os.path.join(ROOT, f"{name}-{version}{suffix}.plugin")
files = sorted(iter_files(args.bake), key=lambda x: x[1])
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as zf:
for full, rel in files:
with open(full, "rb") as fh:
data = fh.read()
info = zipfile.ZipInfo(rel.replace(os.sep, "/"), date_time=FIXED_DATE)
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o644 << 16
zf.writestr(info, data)
size = os.path.getsize(out)
print(f"Built {os.path.basename(out)} ({size:,} bytes, {len(files)} entries)")
if args.bake:
print(" ⚠ This artifact contains your API token — do not share it.")
if __name__ == "__main__":
main()