aboutsummaryrefslogtreecommitdiff
path: root/scripts/changelog.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/changelog.py')
-rw-r--r--scripts/changelog.py99
1 files changed, 88 insertions, 11 deletions
diff --git a/scripts/changelog.py b/scripts/changelog.py
index e930fd82..56838baa 100644
--- a/scripts/changelog.py
+++ b/scripts/changelog.py
@@ -17,6 +17,7 @@ SECTION_RE = re.compile(r"^##\s+(?:\[(?P<bracketed>[^\]]+)\]|(?P<plain>.+?))\s*$
VERSION_WITH_DATE_RE = re.compile(r"^(?P<version>.+?)\s+-\s+\d{4}-\d{2}-\d{2}$")
DEFAULT_RELEASE_REPO = "ogulcancelik/herdr"
DEFAULT_LATEST_JSON_PATH = Path("website/latest.json")
+DEFAULT_PRODUCT_ANNOUNCEMENT_PATH = Path("docs/next/product-announcement.json")
PROTOCOL_SOURCE_PATH = Path("src/server/protocol.rs")
ASSET_TARGETS = (
"linux-x86_64",
@@ -134,7 +135,11 @@ def read_protocol_version(source_path: Path = PROTOCOL_SOURCE_PATH) -> int:
def build_latest_json(
- version: str, notes: str, assets: dict[str, str], protocol: int | None = None
+ version: str,
+ notes: str,
+ assets: dict[str, str],
+ protocol: int | None = None,
+ announcement: dict[str, str] | None = None,
) -> str:
normalized_version = normalize_version(version)
normalized_notes = notes.strip()
@@ -150,15 +155,16 @@ def build_latest_json(
ordered_assets = {target: assets[target] for target in ASSET_TARGETS}
- return json.dumps(
- {
- "version": normalized_version,
- "protocol": protocol,
- "notes": normalized_notes,
- "assets": ordered_assets,
- },
- indent=2,
- ) + "\n"
+ manifest: dict[str, Any] = {
+ "version": normalized_version,
+ "protocol": protocol,
+ "notes": normalized_notes,
+ "assets": ordered_assets,
+ }
+ if announcement is not None:
+ manifest["announcement"] = announcement
+
+ return json.dumps(manifest, indent=2) + "\n"
def default_release_assets(version: str, repo: str = DEFAULT_RELEASE_REPO) -> dict[str, str]:
@@ -284,6 +290,44 @@ def load_json(path: Path) -> dict[str, Any]:
return data
+def load_product_announcement(path: Path) -> dict[str, str] | None:
+ try:
+ content = path.read_text(encoding="utf-8")
+ except FileNotFoundError as exc:
+ raise ChangelogError(f"product announcement file not found: {path}") from exc
+
+ try:
+ data = json.loads(content)
+ except json.JSONDecodeError as exc:
+ raise ChangelogError(f"invalid JSON in {path}: {exc}") from exc
+
+ if data is None:
+ return None
+ if not isinstance(data, dict):
+ raise ChangelogError(f"expected announcement object or null in {path}")
+
+ allowed_keys = {"id", "title", "body"}
+ extra_keys = sorted(set(data) - allowed_keys)
+ if extra_keys:
+ raise ChangelogError(
+ f"announcement in {path} has unsupported field(s): {', '.join(extra_keys)}"
+ )
+
+ announcement: dict[str, str] = {}
+ for key in ("id", "title", "body"):
+ value = data.get(key)
+ if not isinstance(value, str) or not value.strip():
+ raise ChangelogError(f"announcement in {path} is missing non-empty string field: {key}")
+ announcement[key] = value.strip()
+
+ if not re.fullmatch(r"[a-z0-9][a-z0-9._-]*", announcement["id"]):
+ raise ChangelogError(
+ f"announcement in {path} has invalid id; use lowercase letters, numbers, dots, underscores, or dashes"
+ )
+
+ return announcement
+
+
def write_text(path: Path, text: str) -> None:
path.write_text(text, encoding="utf-8")
@@ -412,10 +456,22 @@ def cmd_sync_latest_json(args: argparse.Namespace) -> int:
release_payload = fetch_release_payload(version, args.repo)
new_manifest = manifest_from_release_payload(release_payload, version)
- output = build_latest_json(version, str(new_manifest["notes"]), dict(new_manifest["assets"]))
+ announcement_path = Path(args.announcement)
+ announcement = load_product_announcement(announcement_path)
+ output = build_latest_json(
+ version,
+ str(new_manifest["notes"]),
+ dict(new_manifest["assets"]),
+ announcement=announcement,
+ )
write_text(manifest_path, output)
+ if announcement is not None:
+ write_text(announcement_path, "null\n")
print(f"updated {manifest_path} from GitHub release v{version}")
+ if announcement is not None:
+ print(f"included product announcement from {announcement_path}")
+ print(f"cleared {announcement_path}")
status_lines = git_status_lines(manifest_path)
print("files changed:")
if status_lines:
@@ -432,6 +488,17 @@ def cmd_sync_latest_json(args: argparse.Namespace) -> int:
return 0
+def cmd_validate_product_announcement(args: argparse.Namespace) -> int:
+ announcement = load_product_announcement(Path(args.path))
+ if announcement is None:
+ print(f"product announcement ({args.path}): none")
+ else:
+ print(
+ f"product announcement ({args.path}): {announcement['id']} - {announcement['title']}"
+ )
+ return 0
+
+
def cmd_verify_release_state(args: argparse.Namespace) -> int:
version = normalize_version(args.version)
release_payload = fetch_release_payload(version, args.repo)
@@ -484,8 +551,18 @@ def build_parser() -> argparse.ArgumentParser:
sync_latest_json.add_argument("--version", required=True)
sync_latest_json.add_argument("--repo", default=DEFAULT_RELEASE_REPO)
sync_latest_json.add_argument("--output", default=str(DEFAULT_LATEST_JSON_PATH))
+ sync_latest_json.add_argument("--announcement", default=str(DEFAULT_PRODUCT_ANNOUNCEMENT_PATH))
sync_latest_json.set_defaults(func=cmd_sync_latest_json)
+ validate_product_announcement = subparsers.add_parser(
+ "validate-product-announcement",
+ help="Validate docs/next product announcement JSON",
+ )
+ validate_product_announcement.add_argument(
+ "--path", default=str(DEFAULT_PRODUCT_ANNOUNCEMENT_PATH)
+ )
+ validate_product_announcement.set_defaults(func=cmd_validate_product_announcement)
+
verify_release_state = subparsers.add_parser(
"verify-release-state",
help="Verify GitHub release, local manifest, live manifest, and asset URLs all match",