Skip to content

Commit ec79e23

Browse files
authored
Create gen_changelog.py
添加脚本
1 parent 82a3d61 commit ec79e23

1 file changed

Lines changed: 139 additions & 0 deletions

File tree

.github/scripts/gen_changelog.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/env python3
2+
"""
3+
If version already exists in changelog.md, exit 0 without modification.
4+
Usage: python .github/scripts/gen_changelog.py v4.10.9
5+
"""
6+
from __future__ import annotations
7+
import sys, json, re, html, urllib.request
8+
from datetime import datetime, timezone, timedelta
9+
from pathlib import Path
10+
11+
API_URL = "https://community.fit2cloud.com/v1/products/dataease/releases"
12+
CHANGELOG = Path('docs/changelog.md')
13+
MARKER = '## 2 更新内容'
14+
TZ = timezone(timedelta(hours=8))
15+
16+
TITLE_MAP = {
17+
'安全漏洞修复': ('Warning', '**安全漏洞修复**', 'fix'),
18+
'新增功能': ('Abstract', '新增功能 :star2:', 'feat'),
19+
'功能优化': ('Abstract', '功能优化 :sunflower:', 'refactor'),
20+
'问题修复': ('Abstract', '问题修复 :palm_tree:', 'fix'),
21+
"What's new": ('Abstract', '新增功能 :star2:', 'feat'),
22+
'Improvements': ('Abstract', '功能优化 :sunflower:', 'refactor'),
23+
'Bug fixes': ('Abstract', '问题修复 :palm_tree:', 'fix'),
24+
}
25+
H2_UL = re.compile(r'<h2><a[^>]*></a>([^<]+)</h2>\n?(<ul>.*?</ul>)', re.S)
26+
# 【新增】用于抓取 h2 后面紧跟的 p 标签 (感谢信息)
27+
# 逻辑:匹配 <h2>...</h2> 后面可能有的换行,然后匹配 <p>...</p>
28+
H2_P_THANKS = re.compile(r'<h2><a[^>]*></a>[^<]*</h2>\n?(<p>.*?</p>)', re.S)
29+
LI = re.compile(r'<li>(.*?)</li>', re.S)
30+
31+
32+
def fetch():
33+
with urllib.request.urlopen(API_URL, timeout=30) as r:
34+
return json.loads(r.read().decode())
35+
36+
37+
def normalize(v: str) -> str:
38+
return v.replace('-lts','')
39+
40+
41+
def find_release(target: str, data):
42+
for rel in data:
43+
if normalize(rel.get('version','')) == target:
44+
return rel
45+
return None
46+
47+
48+
def build_block(rel: dict):
49+
version = normalize(rel['version'])
50+
ts = rel.get('publishTime')
51+
dt = datetime.fromtimestamp(ts/1000, tz=TZ) if ts else datetime.now(tz=TZ)
52+
date_str = f"{dt.year}{dt.month}{dt.day}日"
53+
html_content = rel.get('releaseNoteH', '')
54+
sections = []
55+
56+
for title, ul_html in H2_UL.findall(html_content):
57+
items = LI.findall(ul_html)
58+
cleaned = []
59+
for it in items:
60+
if not it.strip():
61+
continue
62+
# 处理 CVE 链接:将 <a href="url">CVE-XXX</a> 转换为 [(CVE-XXX)](url)
63+
def replace_cve_link(match):
64+
url = match.group(1)
65+
cve_text = html.unescape(re.sub(r'<[^>]+>', '', match.group(2)).strip())
66+
return f'[({cve_text})]({url})'
67+
# 先处理 CVE 链接
68+
processed = re.sub(r'<a\s+href="([^"]+)"[^>]*>(.*?)</a>', replace_cve_link, it)
69+
# 再清理其他 HTML 标签
70+
processed = html.unescape(re.sub(r'\s+', ' ', re.sub(r'<[^>]+>', '', processed)).strip())
71+
if processed:
72+
cleaned.append(processed)
73+
thanks_note = ""
74+
# 简单有效的正则:查找当前 title 对应的 h2 后面的 p
75+
# 构造动态正则: <h2>...Title...</h2> ... <ul>...</ul> ... <p>(内容)</p>
76+
# 由于 ul_html 内容可能很长且有特殊字符,直接用 title 定位最安全
77+
safe_title = re.escape(title.strip())
78+
# 正则解释:匹配包含 title 的 h2,后面任意字符(非贪婪),然后匹配一个 <p>...</p>
79+
p_pattern = re.compile(r'<h2><a[^>]*></a>' + safe_title + r'</h2>.*?<p>(.*?)</p>', re.S)
80+
p_match = p_pattern.search(html_content)
81+
if p_match:
82+
p_content = p_match.group(1)
83+
# 清理 HTML 标签,只留文本
84+
clean_p = html.unescape(re.sub(r'<[^>]+>', '', p_content)).strip()
85+
# 只有当内容包含"感谢"或者标题包含"安全"/"漏洞"时,才采纳
86+
if "感谢" in clean_p and "漏洞" in title:
87+
thanks_note = f"\n {clean_p}"
88+
if not cleaned:
89+
continue
90+
admon, nice, tag = TITLE_MAP.get(title, ('info', title, 'note'))
91+
lines = '\n'.join(f" - {i}" for i in cleaned)
92+
if thanks_note:
93+
if lines:
94+
lines += "\n" + thanks_note
95+
sections.append(f"!!! {admon} \"{nice}\"\n\n{lines}\n")
96+
if not sections:
97+
clean = html.unescape(re.sub(r'<[^>]+>','', html_content)).strip()
98+
if clean:
99+
sections.append(f"!!! info \"发布说明\"\n - note: {clean}\n")
100+
block = '\n'.join([f"### {version}", date_str, ''] + sections)
101+
return block
102+
103+
104+
def main():
105+
if len(sys.argv) < 2:
106+
print('Version arg required, e.g. v2.10.1 or v2.10.1-lts or 2.10.1', file=sys.stderr)
107+
return 1
108+
raw = sys.argv[1].strip()
109+
# Accept forms: v2.10.1 or v2.10.1-lts or 2.10.1
110+
if not raw.startswith('v'):
111+
raw = 'v' + raw
112+
target = normalize(raw) # remove -lts suffix if present
113+
data = fetch()
114+
rel = find_release(target, data)
115+
if not rel:
116+
print(f'Target version {raw} (normalized {target}) not found in API list', file=sys.stderr)
117+
return 1
118+
if not CHANGELOG.exists():
119+
print('Changelog file missing.')
120+
return 1
121+
content = CHANGELOG.read_text(encoding='utf-8')
122+
# 【修复】使用正则严格匹配 Markdown 标题行 (例如: ### v2.10.19)
123+
# ^ 表示行首,#+ 表示一个或多个#,\s* 表示可选空格,re.escape 防止版本号中的点被当作通配符
124+
pattern = re.compile(r'^#+\s*' + re.escape(target) + r'\s*$', re.MULTILINE)
125+
126+
if pattern.search(content):
127+
print(f'⚠️ Version {target} already exists in changelog.md (Detected by regex). Skip.')
128+
return 0
129+
block = build_block(rel)
130+
if MARKER in content:
131+
new_content = content.replace(MARKER, MARKER + '\n\n' + block, 1)
132+
else:
133+
new_content = MARKER + '\n\n' + block + content
134+
CHANGELOG.write_text(new_content, encoding='utf-8')
135+
print('Inserted changelog for', target)
136+
return 0
137+
138+
if __name__ == '__main__':
139+
raise SystemExit(main())

0 commit comments

Comments
 (0)