115 lines
3.9 KiB
Python
115 lines
3.9 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""树脂模板打包与版本发布 —— issue #85(父 Issue #13 子任务)。
|
||
|
||
按 `version.yaml`(#13)的 assets 清单把 iAOP-Template-Resin 打包发布:
|
||
|
||
- `validate_version()`:semver 校验 + assets 清单文件/目录存在性检查
|
||
(发布前门禁,防止打包不完整);
|
||
- `package_template()`:按清单打包 zip(含 version.yaml + README),
|
||
输出 `dist/resin-{version}.zip`;
|
||
- `render_release_notes()`:从 version.yaml 生成 Markdown 发布说明
|
||
(版本/行业/基线/资产清单/备注),供仓库 Release 使用。
|
||
|
||
纯标准库实现(zipfile),无第三方依赖。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import re
|
||
import zipfile
|
||
from typing import List, Optional
|
||
|
||
import yaml
|
||
|
||
#: 模板根目录(相对本模块)
|
||
TEMPLATE_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||
#: 版本清单(与 #13 同构)
|
||
VERSION_PATH = os.path.join(TEMPLATE_ROOT, "version.yaml")
|
||
|
||
#: semver 校验(x.y.z)
|
||
_SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$")
|
||
|
||
#: 打包时始终包含的清单外文件
|
||
_ALWAYS_INCLUDE = ["version.yaml", "README.md"]
|
||
|
||
|
||
def load_version(path: str = VERSION_PATH) -> dict:
|
||
"""加载版本清单。"""
|
||
with open(path, "r", encoding="utf-8") as fh:
|
||
return yaml.safe_load(fh) or {}
|
||
|
||
|
||
def validate_version(path: str = VERSION_PATH) -> List[str]:
|
||
"""版本清单校验:semver 合法 + assets 文件/目录存在。
|
||
|
||
Returns:
|
||
问题列表(空 = 校验通过)。
|
||
"""
|
||
problems: List[str] = []
|
||
version = load_version(path)
|
||
ver = str(version.get("version", ""))
|
||
if not _SEMVER_RE.match(ver):
|
||
problems.append(f"version 非法(需 semver x.y.z):{ver!r}")
|
||
for rel in version.get("assets", []):
|
||
full = os.path.join(TEMPLATE_ROOT, rel)
|
||
if not os.path.exists(full):
|
||
problems.append(f"资产缺失:{rel}")
|
||
return problems
|
||
|
||
|
||
def _add_entry(zf: zipfile.ZipFile, rel: str) -> None:
|
||
"""把单个文件或目录加入 zip(保留相对路径)。"""
|
||
full = os.path.join(TEMPLATE_ROOT, rel)
|
||
if os.path.isdir(full):
|
||
for root, _, files in os.walk(full):
|
||
for name in files:
|
||
abs_path = os.path.join(root, name)
|
||
zf.write(abs_path, os.path.relpath(abs_path, TEMPLATE_ROOT))
|
||
else:
|
||
zf.write(full, rel)
|
||
|
||
|
||
def package_template(path: str = VERSION_PATH,
|
||
output_dir: Optional[str] = None) -> str:
|
||
"""按 assets 清单打包模板为 zip。
|
||
|
||
Returns:
|
||
生成的 zip 文件路径。
|
||
"""
|
||
version = load_version(path)
|
||
ver = str(version.get("version", ""))
|
||
problems = validate_version(path)
|
||
if problems:
|
||
raise ValueError("打包前校验未通过:" + "; ".join(problems))
|
||
|
||
dist = output_dir or os.path.join(TEMPLATE_ROOT, "dist")
|
||
os.makedirs(dist, exist_ok=True)
|
||
zip_path = os.path.join(dist, f"resin-{ver}.zip")
|
||
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||
entries = _ALWAYS_INCLUDE + list(version.get("assets", []))
|
||
for rel in dict.fromkeys(entries): # 去重保序
|
||
_add_entry(zf, rel)
|
||
return zip_path
|
||
|
||
|
||
def render_release_notes(path: str = VERSION_PATH) -> str:
|
||
"""从 version.yaml 生成 Markdown 发布说明。"""
|
||
version = load_version(path)
|
||
lines = [
|
||
f"# iAOP-Template-Resin v{version.get('version', '?')}",
|
||
"",
|
||
f"- 行业:{version.get('industry', '')}",
|
||
f"- 基线:{version.get('baseline', '')}",
|
||
f"- 并行:{version.get('parallel_with', '')}",
|
||
f"- 状态:{version.get('status', '')}",
|
||
"",
|
||
"## 资产清单",
|
||
]
|
||
lines += [f"- `{a}`" for a in version.get("assets", [])]
|
||
notes = version.get("notes", [])
|
||
if notes:
|
||
lines.append("")
|
||
lines.append("## 备注")
|
||
lines += [f"- {n}" for n in notes]
|
||
return "\n".join(lines) + "\n"
|