3d1c3946f6
* feat: add nightly prerelease release flow and updater support * feat(ci): auto-generate nightly release notes from latest stable tag * fix(ci): correct nightly release notes heredoc YAML indentation * fix(ci): align nightly notes heredoc terminator * fix(ci): remove heredoc body indentation in nightly notes script * fix: align nightly release metadata and prerelease rules * fix: harden nightly release flow and updater release resolution * fix: improve nightly branch resolution and updater logging * fix: simplify updater target resolution and nightly release assets * fix: avoid inputs lookup on non-dispatch release events * fix: split nightly release fetch and simplify updater flow * refactor: simplify updater target resolvers and nightly error checks * fix: type release fetch errors and streamline updater resolution * refactor: simplify updater target branching and release artifacts * refactor: simplify release fetching and harden nightly git diagnostics * fix: validate release payload shape before parsing * refactor: harden prerelease handling and nightly constants * refactor: derive archive urls and enrich fetch errors * refactor: simplify update target resolution flow * refactor: linearize update target resolution * refactor: validate update target inputs and sync nightly tag source * refactor: simplify updater mode resolution and prerelease tests * refactor: simplify update target resolution flow * fix: avoid package import when resolving nightly tag * refactor: simplify updater resolution and centralize release constants * fix: harden nightly release notes generation in workflow * refactor: streamline update target resolution and errors * refactor: simplify updater target resolution and nightly handling * refactor: simplify updater errors and package release scripts * refactor: centralize release api constants and loader * fix(ci): resolve dispatch fallback tag from stable releases
137 lines
4.1 KiB
Python
137 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import subprocess
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
if __package__:
|
|
from .release_constants_loader import load_release_constants
|
|
else:
|
|
import sys
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
from scripts.release.release_constants_loader import load_release_constants
|
|
|
|
_constants = load_release_constants("NIGHTLY_TAG", "GITHUB_REPO_SLUG")
|
|
NIGHTLY_TAG = _constants["NIGHTLY_TAG"]
|
|
DEFAULT_REPO_SLUG = _constants["GITHUB_REPO_SLUG"]
|
|
|
|
|
|
def _run_git(*args: str) -> str:
|
|
try:
|
|
result = subprocess.run(
|
|
["git", *args],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
except subprocess.CalledProcessError as e:
|
|
stderr = (e.stderr or "").strip()
|
|
stdout = (e.stdout or "").strip()
|
|
detail = stderr or stdout or "no output"
|
|
raise RuntimeError(f"git {' '.join(args)} failed: {detail}") from e
|
|
return result.stdout.strip()
|
|
|
|
|
|
def _is_valid_ref(ref: str) -> bool:
|
|
if not ref:
|
|
return False
|
|
result = subprocess.run(
|
|
["git", "rev-parse", "--verify", "--quiet", ref],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
return result.returncode == 0
|
|
|
|
|
|
def _classify(subject: str) -> str:
|
|
lowered = subject.lower().strip()
|
|
if lowered.startswith("feat") or "新增" in subject:
|
|
return "新增"
|
|
if lowered.startswith("fix") or "修复" in subject:
|
|
return "修复"
|
|
if (
|
|
lowered.startswith("perf")
|
|
or lowered.startswith("refactor")
|
|
or "优化" in subject
|
|
):
|
|
return "优化"
|
|
return "其他"
|
|
|
|
|
|
def _write_fallback(output_path: Path) -> None:
|
|
short_sha = _run_git("rev-parse", "--short=8", "HEAD")
|
|
output_path.write_text(
|
|
f"## What's Changed\n\n- {NIGHTLY_TAG.capitalize()} build from `{short_sha}`\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def generate_notes(base_tag: str, repo: str, output_path: Path) -> None:
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
if not _is_valid_ref(base_tag):
|
|
_write_fallback(output_path)
|
|
return
|
|
|
|
log_output = _run_git(
|
|
"log",
|
|
"--no-merges",
|
|
"--pretty=format:%h%x1f%s",
|
|
f"{base_tag}..HEAD",
|
|
)
|
|
sections: dict[str, list[str]] = defaultdict(list)
|
|
for line in log_output.splitlines():
|
|
if not line.strip() or "\x1f" not in line:
|
|
continue
|
|
short_sha, subject = line.split("\x1f", 1)
|
|
commit_link = f"https://github.com/{repo}/commit/{short_sha}"
|
|
sections[_classify(subject)].append(
|
|
f"- {subject} ([`{short_sha}`]({commit_link}))"
|
|
)
|
|
|
|
nightly_commit = _run_git("rev-parse", "--short=8", "HEAD")
|
|
with output_path.open("w", encoding="utf-8") as file:
|
|
file.write("## What's Changed\n\n")
|
|
file.write(f"- Baseline tag: `{base_tag}`\n")
|
|
file.write(f"- {NIGHTLY_TAG.capitalize()} commit: `{nightly_commit}`\n")
|
|
|
|
if not any(sections.values()):
|
|
file.write(f"- No changes since `{base_tag}`\n\n")
|
|
return
|
|
|
|
file.write("\n")
|
|
for title in ("新增", "修复", "优化", "其他"):
|
|
items = sections.get(title, [])
|
|
if not items:
|
|
continue
|
|
file.write(f"### {title}\n")
|
|
file.write("\n".join(items))
|
|
file.write("\n\n")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(
|
|
description="Generate release notes for nightly release.",
|
|
)
|
|
parser.add_argument("--base-tag", default="", help="Baseline stable tag.")
|
|
parser.add_argument(
|
|
"--repo",
|
|
default=DEFAULT_REPO_SLUG,
|
|
help="GitHub repository slug.",
|
|
)
|
|
parser.add_argument("--output", required=True, help="Output markdown path.")
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
generate_notes(args.base_tag.strip(), args.repo.strip(), Path(args.output))
|
|
except Exception as e:
|
|
raise SystemExit(f"Failed to generate nightly release notes: {e}") from e
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|