Files
Auto_Bangumi/scripts/classify_release.py
2026-07-10 12:05:02 +02:00

143 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""Classify a GitHub Actions trigger without deriving versions from PR data."""
from __future__ import annotations
import argparse
import os
import re
import subprocess
import sys
from pathlib import Path
STABLE_TAG = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+$")
PRERELEASE_TAG = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+-(?:alpha|beta)\.[0-9]+$")
class ReleaseClassificationError(ValueError):
"""Raised when a release-shaped trigger is unsafe or unverifiable."""
def classify_release(
*,
event_name: str,
ref_type: str,
ref_name: str,
pr_head: str = "",
pr_title: str = "",
stable_tag_on_main: bool | None = None,
) -> dict[str, str]:
"""Return the four outputs consumed by the release workflow.
``pr_title`` is accepted so callers can pass complete event metadata, but
is deliberately never used as a version source.
"""
del pr_title
result = {
"release": "0",
"dev": "0",
"build_test": "0",
"version": "Test",
}
if ref_type == "tag" and STABLE_TAG.fullmatch(ref_name):
if stable_tag_on_main is None:
raise ReleaseClassificationError(
f"Stable tag {ref_name} ancestry was not verified"
)
if not stable_tag_on_main:
raise ReleaseClassificationError(
f"Stable tag {ref_name} does not point to a commit on main; "
"refusing to release"
)
return {
**result,
"release": "1",
"version": ref_name,
}
if ref_type == "tag" and PRERELEASE_TAG.fullmatch(ref_name):
return {
**result,
"release": "1",
"dev": "1",
"version": ref_name,
}
if event_name == "pull_request" and "dev" in pr_head:
result["build_test"] = "1"
return result
def _is_main_ancestor(sha: str, main_ref: str) -> bool:
if not sha:
raise ReleaseClassificationError(
"GITHUB_SHA is required to verify a stable release tag"
)
completed = subprocess.run(
["git", "merge-base", "--is-ancestor", sha, main_ref],
check=False,
capture_output=True,
text=True,
)
if completed.returncode == 0:
return True
if completed.returncode == 1:
return False
detail = completed.stderr.strip() or "git merge-base failed"
raise ReleaseClassificationError(
f"Unable to verify stable tag ancestry against {main_ref}: {detail}"
)
def _write_outputs(outputs: dict[str, str], destination: str) -> None:
payload = "".join(f"{key}={value}\n" for key, value in outputs.items())
if destination:
with Path(destination).open("a", encoding="utf-8") as output_file:
output_file.write(payload)
else:
sys.stdout.write(payload)
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--event-name", default=os.environ.get("EVENT_NAME", ""))
parser.add_argument("--ref-type", default=os.environ.get("REF_TYPE", ""))
parser.add_argument("--ref-name", default=os.environ.get("REF_NAME", ""))
parser.add_argument("--pr-head", default=os.environ.get("HEAD_REF", ""))
parser.add_argument("--pr-title", default=os.environ.get("PR_TITLE", ""))
parser.add_argument("--sha", default=os.environ.get("GITHUB_SHA", ""))
parser.add_argument("--main-ref", default="origin/main")
parser.add_argument(
"--output",
default=os.environ.get("GITHUB_OUTPUT", ""),
help="GitHub Actions output file; stdout when omitted",
)
return parser.parse_args()
def main() -> int:
args = _parse_args()
try:
stable_tag_on_main = None
if args.ref_type == "tag" and STABLE_TAG.fullmatch(args.ref_name):
stable_tag_on_main = _is_main_ancestor(args.sha, args.main_ref)
outputs = classify_release(
event_name=args.event_name,
ref_type=args.ref_type,
ref_name=args.ref_name,
pr_head=args.pr_head,
pr_title=args.pr_title,
stable_tag_on_main=stable_tag_on_main,
)
_write_outputs(outputs, args.output)
except ReleaseClassificationError as exc:
print(f"::error::{exc}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())