Files
XStats/Scripts/appcast.py
T
ysicing f09861f6ec feat(release): 安装包迁移对象存储,版本号回归 semver
发布链路:安装包不再 scp 到 VPS,改用 mc 上传到对象存储
c.ysicing.net/oss/apps/macOS/XStats,上传后从 CDN 回读比对 sha256;
GitHub Release 挂 dmg 作为手动下载入口,应用内“手动下载”在未取到清单时
跳到 releases/latest。版本清单仍由自建 API 返回,publish_api.py 与
server/api 不变,且始终最后提交——安装包校验通过后客户端才看到新版本。
发布前只读校验 HEAD 与 origin/main 一致,脚本不代为 commit 或 push。

版本号:公开版本号从“年.月.日.索引”回归标准 semver,真源收敛到
CHANGELOG.md 顶部的 “## X.Y.Z · YYYY-MM-DD”;version.sh 收敛为
show/build/release 三个子命令,build 只推进构建号,删除 next 与 tag。
内部构建号改为单调递增整数(0109 规范化为 109)。日期校验回读比对,
因为 BSD date 会把 2026-02-29 规整成 2026-03-01 而非报错。

CI 不再参与发版:删除 tag 触发与 Set version from tag 步骤。

其他:appcast 的 changelog 指向本仓库而非上游站点;arm64_release_test
改用 fixture CHANGELOG,与仓库版本历史解耦;丢弃上游 0.2.0–0.6.1 的
历史条目;sync_changelog 不再改写 web/index.html(上游历史站点),
连带删除仅服务于它的 Website 段落。
2026-09-22 08:25:45 +08:00

84 lines
2.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""生成应用内在线升级读取的版本清单 appcast.json。
Scripts/appcast.py <版本> <build> <下载地址前缀> <Apple 芯片 zip> <Apple 芯片 dmg> > dist/appcast.json
清单只发布 Apple Silicon 安装包。
更新摘要取自 CHANGELOG.md 中该版本的条目:每条取冒号或句号之前的部分(过短时带上冒号后的内容),最多 10 条。
"""
import hashlib
import json
import os
import re
import sys
MAX_NOTES = 10
MAX_LENGTH = 48
def summarize(line: str) -> str:
text = re.sub(r"`([^`]*)`", r"\1", line).strip()
text = re.sub(r"\*\*([^*]*)\*\*", r"\1", text)
head = re.split(r"[:。;]", text, maxsplit=1)[0]
# 冒号前只是个标题(如“主窗口”)时带上后面的内容,否则摘要看不出改了什么
text = head if len(head) >= 10 else text.split("。", 1)[0]
text = text.rstrip(",、 ")
return text if len(text) <= MAX_LENGTH else text[: MAX_LENGTH - 1] + "…"
def release_notes(changelog: str, version: str) -> tuple[str, list[str]]:
section = re.search(rf"^## {re.escape(version)} · (\S+)\n(.*?)(?=^## |\Z)", changelog, re.S | re.M)
if not section:
sys.exit(f"CHANGELOG.md 里没有 {version} 的条目")
date, body = section.group(1), section.group(2)
notes = []
for line in body.splitlines():
if line.startswith("- "):
summary = summarize(line[2:])
if summary and summary not in notes:
notes.append(summary)
if len(notes) > MAX_NOTES:
rest = len(notes) - MAX_NOTES + 1
notes = notes[: MAX_NOTES - 1] + [f"以及另外 {rest} 项新增、调整与修复"]
return date, notes
def sha256(path: str) -> str:
digest = hashlib.sha256()
with open(path, "rb") as handle:
for chunk in iter(lambda: handle.read(1 << 20), b""):
digest.update(chunk)
return digest.hexdigest()
def asset(base: str, zip_path: str, dmg_path: str) -> dict:
return {
"url": f"{base}/{os.path.basename(zip_path)}",
"sha256": sha256(zip_path),
"size": os.path.getsize(zip_path),
"dmg": f"{base}/{os.path.basename(dmg_path)}",
}
def main() -> None:
version, build, base, arm_zip, arm_dmg = sys.argv[1:6]
root = os.path.join(os.path.dirname(__file__), "..")
with open(os.path.join(root, "CHANGELOG.md"), encoding="utf-8") as handle:
date, notes = release_notes(handle.read(), version)
feed = {
"version": version,
"build": build,
"date": date,
"minimumSystem": "14.0",
**asset(base, arm_zip, arm_dmg),
"notes": notes,
"changelog": "https://github.com/ysicing/xstats/blob/main/CHANGELOG.md",
}
json.dump(feed, sys.stdout, ensure_ascii=False, indent=2)
sys.stdout.write("\n")
if __name__ == "__main__":
main()