-
Notifications
You must be signed in to change notification settings - Fork 43
Expand file tree
/
Copy pathbuild_release_body.py
More file actions
121 lines (104 loc) · 3.95 KB
/
Copy pathbuild_release_body.py
File metadata and controls
121 lines (104 loc) · 3.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#!/usr/bin/env python3
"""
Build GitHub release notes: ChangeLog block for a semver (no "v" prefix) plus
GitHub auto-generated compare notes, written to a path for action-gh-release.
"""
from __future__ import annotations
import json
import os
import re
import sys
import urllib.error
import urllib.request
def extract_changelog_block(changelog: str, version: str) -> str:
"""Return lines from "Version {version}..." until the next long-dash separator."""
if not version or not re.match(r"^[\d.]+$", version):
return ""
lines = changelog.splitlines()
ver_re = re.compile(rf"^Version {re.escape(version)}[,\s(].*$")
start_idx: int | None = None
for i, line in enumerate(lines):
if ver_re.match(line):
start_idx = i
break
if start_idx is None:
return ""
sep = re.compile(r"^[-]{20,}\s*$")
out: list[str] = []
for j in range(start_idx, len(lines)):
line = lines[j]
if j > start_idx and sep.match(line):
break
out.append(line)
return "\n".join(out) + "\n" if out else ""
def fetch_generated_notes(
repository: str, tag_name: str, target_commitish: str, token: str
) -> str:
"""Call POST /repos/{owner}/{repo}/releases/generate-notes."""
url = f"https://api.github.com/repos/{repository}/releases/generate-notes"
payload = {
"tag_name": tag_name,
"target_commitish": target_commitish,
}
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
url,
data=data,
method="POST",
headers={
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"Authorization": f"Bearer {token}",
"User-Agent": "loganalyzer-release-workflow",
},
)
try:
with urllib.request.urlopen(req, timeout=120) as resp:
body = json.loads(resp.read().decode("utf-8"))
except (urllib.error.HTTPError, urllib.error.URLError) as e:
return f"_Could not generate auto-release notes: {e}_\n"
return (body or {}).get("body") or "_No body returned from GitHub._\n"
def main() -> int:
if len(sys.argv) != 2:
print("Usage: build_release_body.py <output.md>", file=sys.stderr)
return 1
out_path = sys.argv[1]
tag_name = os.environ.get("GITHUB_REF_NAME", "")
if not tag_name or not tag_name.startswith("v"):
print("GITHUB_REF_NAME must be a v-prefixed tag (e.g. v4.1.13)", file=sys.stderr)
return 1
raw_ver = tag_name[1:]
m = re.match(r"^([\d.]+)(?:[-+].*)?$", raw_ver)
version = m.group(1) if m else raw_ver
if not re.match(r"^[\d.]+$", version):
print("Could not parse semver from tag; expected vX.Y.Z[qualifiers]", file=sys.stderr)
return 1
repo = os.environ.get("GITHUB_REPOSITORY", "")
sha = os.environ.get("GITHUB_SHA", "")
token = os.environ.get("GITHUB_TOKEN", "")
if not repo or not sha or not token:
print("Missing GITHUB_REPOSITORY, GITHUB_SHA, or GITHUB_TOKEN", file=sys.stderr)
return 1
cl_path = os.path.join(os.path.dirname(__file__), "..", "..", "ChangeLog")
cl_path = os.path.normpath(cl_path)
changelog_text = ""
if os.path.isfile(cl_path):
with open(cl_path, encoding="utf-8", errors="replace") as f:
changelog_text = f.read()
cl_block = extract_changelog_block(changelog_text, version)
if not cl_block:
cl_block = (
f"_No matching **Version {version}** block in `ChangeLog`. "
"Add one before publishing this tag, or read the full file in the source tree._\n\n"
)
auto = fetch_generated_notes(repo, tag_name, sha, token)
body = f"""## Change log (from repository `ChangeLog`)
{cl_block}
---
## Commits and pull requests (auto-generated by GitHub)
{auto}"""
with open(out_path, "w", encoding="utf-8", newline="\n") as f:
f.write(body)
return 0
if __name__ == "__main__":
raise SystemExit(main())