Files
wehub-resource-sync f3d80b4628
Auto Release / auto-release (push) Failing after 1s
CI / lint (push) Failing after 0s
CI / screenshot-quality (push) Failing after 3s
CI / pr-policy (push) Has been skipped
CI / test (3.11) (push) Failing after 0s
CI / test (3.12) (push) Failing after 0s
CI / test (3.13) (push) Failing after 3s
CI / coverage (push) Failing after 1s
Legibility / legibility (push) Failing after 3s
chore: import upstream snapshot with attribution
2026-07-13 12:31:48 +08:00

91 lines
2.8 KiB
Python

#!/usr/bin/env python3
"""Ensure released tags are documented in CHANGELOG.md."""
from __future__ import annotations
import argparse
import re
import subprocess
import sys
from pathlib import Path
TAG_RE = re.compile(r"^v(?P<version>\d+\.\d+\.\d+)$")
CHANGELOG_HEADING_RE = re.compile(r"^## \[(?P<version>\d+\.\d+\.\d+)\](?:\s+-\s+\d{4}-\d{2}-\d{2})?\s*$", re.MULTILINE)
def normalize_tag(tag: str) -> str | None:
match = TAG_RE.match(tag.strip())
return match.group("version") if match else None
def changelog_versions(text: str) -> set[str]:
return {match.group("version") for match in CHANGELOG_HEADING_RE.finditer(text)}
def _git(repo_root: Path, args: list[str]) -> str:
return subprocess.check_output(["git", *args], cwd=repo_root, text=True, stderr=subprocess.DEVNULL).strip()
def current_exact_tag(repo_root: Path) -> str | None:
try:
tag = _git(repo_root, ["describe", "--exact-match", "--tags", "HEAD"])
except subprocess.CalledProcessError:
return None
return tag if normalize_tag(tag) else None
def latest_version_tag(repo_root: Path) -> str | None:
try:
tags = _git(repo_root, ["tag", "--list", "v[0-9]*", "--sort=-v:refname"])
except subprocess.CalledProcessError:
return None
for tag in tags.splitlines():
if normalize_tag(tag):
return tag
return None
def required_tag(repo_root: Path, explicit_tag: str | None) -> str | None:
if explicit_tag:
return explicit_tag
return current_exact_tag(repo_root) or latest_version_tag(repo_root)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path.cwd(), help="Repository root path")
parser.add_argument("--tag", help="Release tag to require, e.g. v0.1.40")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
repo_root = args.repo_root.resolve()
tag = required_tag(repo_root, args.tag)
if tag is None:
print("No release tag found; skipping changelog release coverage check.")
return 0
version = normalize_tag(tag)
if version is None:
print(f"ERROR: unsupported release tag format: {tag}", file=sys.stderr)
return 1
changelog_path = repo_root / "CHANGELOG.md"
try:
documented_versions = changelog_versions(changelog_path.read_text(encoding="utf-8"))
except OSError as exc:
print(f"ERROR: cannot read {changelog_path}: {exc}", file=sys.stderr)
return 1
if version not in documented_versions:
print(f"ERROR: CHANGELOG.md is missing release section for [{version}] ({tag})", file=sys.stderr)
return 1
print(f"Changelog release coverage passed for {tag}")
return 0
if __name__ == "__main__":
raise SystemExit(main())