intermediate commit
This commit is contained in:
214
tools/release/release-catalog.py
Normal file
214
tools/release/release-catalog.py
Normal file
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build GovOPlaN release catalog candidates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from govoplan_release.module_directory import write_module_directory
|
||||
from govoplan_release.model import to_jsonable
|
||||
from govoplan_release.publisher import publish_catalog_candidate
|
||||
from govoplan_release.selective_catalog import build_selective_catalog_candidate
|
||||
from govoplan_release.workspace import DEFAULT_WORKSPACE_ROOT
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
selective = subparsers.add_parser("selective", help="Build a signed selective channel catalog candidate.")
|
||||
selective.add_argument("--workspace-root", type=Path, default=DEFAULT_WORKSPACE_ROOT)
|
||||
selective.add_argument("--channel", default="stable")
|
||||
selective.add_argument("--repo-version", action="append", default=[], metavar="REPO=VERSION", required=True)
|
||||
selective.add_argument("--catalog-signing-key", action="append", default=[], metavar="KEY_ID=PRIVATE_KEY", required=True)
|
||||
selective.add_argument("--base-catalog", help="Catalog path or URL to use as the source. Defaults to local channel, then public channel.")
|
||||
selective.add_argument("--output-dir", type=Path, help="Candidate output directory. Defaults to runtime/release-candidates/<channel>-<timestamp>.")
|
||||
selective.add_argument("--public-base-url", default="https://govoplan.add-ideas.de")
|
||||
selective.add_argument("--repository-base", default="git+ssh://git@git.add-ideas.de/add-ideas")
|
||||
selective.add_argument("--expires-days", type=int, default=90)
|
||||
selective.add_argument("--sequence", type=int)
|
||||
selective.add_argument("--skip-public-check", action="store_true")
|
||||
selective.add_argument("--json", action="store_true", help="Print machine-readable summary JSON.")
|
||||
|
||||
publish = subparsers.add_parser("publish-candidate", help="Publish a reviewed catalog candidate into the website repo.")
|
||||
publish.add_argument("--candidate-dir", type=Path, required=True)
|
||||
publish.add_argument("--workspace-root", type=Path, default=DEFAULT_WORKSPACE_ROOT)
|
||||
publish.add_argument("--web-root", type=Path)
|
||||
publish.add_argument("--channel", default="stable")
|
||||
publish.add_argument("--apply", action="store_true", help="Copy candidate files into the website repo. Without this, only preview.")
|
||||
publish.add_argument("--build-web", action="store_true")
|
||||
publish.add_argument("--commit", action="store_true")
|
||||
publish.add_argument("--tag", action="store_true", help="Create a website catalog publication tag. Implies --commit.")
|
||||
publish.add_argument("--push", action="store_true", help="Push the website branch and tag. Implies --commit.")
|
||||
publish.add_argument("--remote", default="origin")
|
||||
publish.add_argument("--branch")
|
||||
publish.add_argument("--tag-name")
|
||||
publish.add_argument("--npm", default="npm")
|
||||
publish.add_argument("--allow-dirty-website", action="store_true")
|
||||
publish.add_argument("--json", action="store_true", help="Print machine-readable summary JSON.")
|
||||
|
||||
directory = subparsers.add_parser("module-directory", help="Generate browsable module-directory files from a catalog and keyring.")
|
||||
directory.add_argument("--catalog", type=Path, required=True)
|
||||
directory.add_argument("--keyring", type=Path, required=True)
|
||||
directory.add_argument("--output-dir", type=Path, required=True)
|
||||
directory.add_argument("--channel", default="stable")
|
||||
directory.add_argument("--public-base-url", default="https://govoplan.add-ideas.de")
|
||||
directory.add_argument("--json", action="store_true", help="Print machine-readable summary JSON.")
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.command == "selective":
|
||||
result = build_selective_catalog_candidate(
|
||||
repo_versions=parse_repo_versions(tuple(args.repo_version)),
|
||||
channel=args.channel,
|
||||
workspace_root=args.workspace_root,
|
||||
output_dir=args.output_dir,
|
||||
base_catalog=args.base_catalog,
|
||||
signing_keys=tuple(args.catalog_signing_key),
|
||||
public_base_url=args.public_base_url,
|
||||
repository_base=args.repository_base,
|
||||
expires_days=args.expires_days,
|
||||
sequence=args.sequence,
|
||||
check_public=not args.skip_public_check,
|
||||
)
|
||||
payload = to_jsonable(result)
|
||||
if args.json:
|
||||
print(json.dumps(payload, indent=2, sort_keys=True))
|
||||
else:
|
||||
print_text_summary(payload)
|
||||
return 0 if result.status == "ready" else 1
|
||||
if args.command == "publish-candidate":
|
||||
result = publish_catalog_candidate(
|
||||
candidate_dir=args.candidate_dir,
|
||||
workspace_root=args.workspace_root,
|
||||
web_root=args.web_root,
|
||||
channel=args.channel,
|
||||
apply=args.apply,
|
||||
build_web=args.build_web,
|
||||
commit=args.commit,
|
||||
tag=args.tag,
|
||||
push=args.push,
|
||||
remote=args.remote,
|
||||
branch=args.branch,
|
||||
npm=args.npm,
|
||||
tag_name=args.tag_name,
|
||||
allow_dirty_website=args.allow_dirty_website,
|
||||
)
|
||||
payload = to_jsonable(result)
|
||||
if args.json:
|
||||
print(json.dumps(payload, indent=2, sort_keys=True))
|
||||
else:
|
||||
print_publish_summary(payload)
|
||||
return 0 if result.status in {"ready", "applied", "published"} else 1
|
||||
if args.command == "module-directory":
|
||||
catalog_payload = json.loads(args.catalog.read_text(encoding="utf-8"))
|
||||
keyring_payload = json.loads(args.keyring.read_text(encoding="utf-8"))
|
||||
files = write_module_directory(
|
||||
catalog_payload=catalog_payload,
|
||||
keyring_payload=keyring_payload,
|
||||
output_root=args.output_dir,
|
||||
channel=args.channel,
|
||||
public_base_url=args.public_base_url,
|
||||
)
|
||||
payload = {"status": "generated", "output_dir": str(args.output_dir), "files": [str(path) for path in files]}
|
||||
if args.json:
|
||||
print(json.dumps(payload, indent=2, sort_keys=True))
|
||||
else:
|
||||
print("Module directory")
|
||||
print(f" output_dir: {args.output_dir}")
|
||||
print(f" files: {len(files)}")
|
||||
for path in files[:20]:
|
||||
print(f" {path}")
|
||||
if len(files) > 20:
|
||||
print(f" ... {len(files) - 20} more")
|
||||
return 0
|
||||
return 2
|
||||
|
||||
|
||||
def parse_repo_versions(values: tuple[str, ...]) -> dict[str, str]:
|
||||
result: dict[str, str] = {}
|
||||
for value in values:
|
||||
if "=" not in value:
|
||||
raise SystemExit(f"--repo-version must use REPO=VERSION: {value}")
|
||||
repo, version = value.split("=", 1)
|
||||
repo = repo.strip()
|
||||
version = version.strip().removeprefix("v")
|
||||
if not repo or not version:
|
||||
raise SystemExit(f"--repo-version must use REPO=VERSION: {value}")
|
||||
result[repo] = version
|
||||
return result
|
||||
|
||||
|
||||
def print_text_summary(payload: dict[str, object]) -> None:
|
||||
print("Selective catalog candidate")
|
||||
for key in (
|
||||
"status",
|
||||
"channel",
|
||||
"sequence",
|
||||
"catalog_path",
|
||||
"keyring_path",
|
||||
"summary_path",
|
||||
"validation_valid",
|
||||
"validation_error",
|
||||
"candidate_matches_published_catalog",
|
||||
"candidate_matches_published_keyring",
|
||||
):
|
||||
print(f" {key}: {payload.get(key)}")
|
||||
changes = payload.get("changes")
|
||||
if isinstance(changes, list):
|
||||
print(f" changes: {len(changes)}")
|
||||
for item in changes[:20]:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
print(
|
||||
" "
|
||||
f"{item.get('repo')}:{item.get('module_id') or '-'} "
|
||||
f"{item.get('field')} {item.get('before')!r} -> {item.get('after')!r}"
|
||||
)
|
||||
if len(changes) > 20:
|
||||
print(f" ... {len(changes) - 20} more")
|
||||
warnings = payload.get("validation_warnings")
|
||||
if isinstance(warnings, list) and warnings:
|
||||
print(" validation warnings:")
|
||||
for warning in warnings:
|
||||
print(f" {warning}")
|
||||
|
||||
|
||||
def print_publish_summary(payload: dict[str, object]) -> None:
|
||||
print("Catalog publish candidate")
|
||||
for key in (
|
||||
"status",
|
||||
"applied",
|
||||
"channel",
|
||||
"candidate_dir",
|
||||
"web_root",
|
||||
"target_catalog_path",
|
||||
"target_keyring_path",
|
||||
"branch",
|
||||
"tag_name",
|
||||
"validation_valid",
|
||||
"validation_error",
|
||||
"catalog_changed",
|
||||
"keyring_changed",
|
||||
):
|
||||
print(f" {key}: {payload.get(key)}")
|
||||
steps = payload.get("steps")
|
||||
if isinstance(steps, list):
|
||||
print(" steps:")
|
||||
for item in steps:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
print(f" [{item.get('status')}] {item.get('title')}")
|
||||
command = item.get("command")
|
||||
if command:
|
||||
print(f" {command}")
|
||||
notes = payload.get("notes")
|
||||
if isinstance(notes, list) and notes:
|
||||
print(" notes:")
|
||||
for note in notes:
|
||||
print(f" {note}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user