✨ feat(vendor): add playbook snapshot generation
This commit is contained in:
parent
8cfcc25f98
commit
49bbfa13e4
|
|
@ -1,10 +1,15 @@
|
|||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from shutil import copy2, copytree
|
||||
import subprocess
|
||||
|
||||
import tomllib
|
||||
|
||||
ORDER = ["vendor", "sync_templates", "sync_standards", "install_skills", "format_md"]
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
PLAYBOOK_ROOT = SCRIPT_DIR.parent
|
||||
|
||||
|
||||
def usage() -> str:
|
||||
|
|
@ -15,8 +20,217 @@ def load_config(path: Path) -> dict:
|
|||
return tomllib.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def log(message: str) -> None:
|
||||
print(message)
|
||||
|
||||
|
||||
def ensure_dir(path: Path) -> None:
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def normalize_langs(raw: object) -> list[str]:
|
||||
if raw is None:
|
||||
return ["tsl"]
|
||||
if isinstance(raw, str):
|
||||
langs = [raw]
|
||||
else:
|
||||
langs = list(raw)
|
||||
cleaned: list[str] = []
|
||||
for lang in langs:
|
||||
item = str(lang).strip()
|
||||
if not item:
|
||||
continue
|
||||
if "/" in item or "\\" in item or ".." in item:
|
||||
raise ValueError(f"invalid lang: {item}")
|
||||
cleaned.append(item)
|
||||
if not cleaned:
|
||||
raise ValueError("langs is empty")
|
||||
return cleaned
|
||||
|
||||
|
||||
def read_git_commit(root: Path) -> str:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(root), "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
return "N/A"
|
||||
return result.stdout.strip() or "N/A"
|
||||
|
||||
|
||||
def write_docs_index(dest_prefix: Path, langs: list[str]) -> None:
|
||||
lines = [
|
||||
"# 文档导航(Docs Index)",
|
||||
"",
|
||||
f"本快照为裁剪版 Playbook(langs: {','.join(langs)})。",
|
||||
"",
|
||||
"## 跨语言(common)",
|
||||
"",
|
||||
"- 提交信息与版本号:`common/commit_message.md`",
|
||||
]
|
||||
for lang in langs:
|
||||
if lang == "tsl":
|
||||
lines += [
|
||||
"",
|
||||
"## TSL(tsl)",
|
||||
"",
|
||||
"- 代码风格:`tsl/code_style.md`",
|
||||
"- 命名规范:`tsl/naming.md`",
|
||||
"- 语法手册:`tsl/syntax_book/index.md`",
|
||||
"- 工具链与验证命令(模板):`tsl/toolchain.md`",
|
||||
]
|
||||
elif lang == "cpp":
|
||||
lines += [
|
||||
"",
|
||||
"## C++(cpp)",
|
||||
"",
|
||||
"- 代码风格:`cpp/code_style.md`",
|
||||
"- 命名规范:`cpp/naming.md`",
|
||||
"- 工具链与验证命令(模板):`cpp/toolchain.md`",
|
||||
"- 第三方依赖(Conan):`cpp/dependencies_conan.md`",
|
||||
"- clangd 配置:`cpp/clangd.md`",
|
||||
]
|
||||
elif lang == "python":
|
||||
lines += [
|
||||
"",
|
||||
"## Python(python)",
|
||||
"",
|
||||
"- 代码风格:`python/style_guide.md`",
|
||||
"- 工具链:`python/tooling.md`",
|
||||
"- 配置清单:`python/configuration.md`",
|
||||
]
|
||||
elif lang == "markdown":
|
||||
lines += [
|
||||
"",
|
||||
"## Markdown(markdown)",
|
||||
"",
|
||||
"- 代码块与行内代码格式:`markdown/index.md`",
|
||||
]
|
||||
docs_index = dest_prefix / "docs/index.md"
|
||||
ensure_dir(docs_index.parent)
|
||||
docs_index.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def write_snapshot_readme(dest_prefix: Path, langs: list[str]) -> None:
|
||||
lines = [
|
||||
"# Playbook(裁剪快照)",
|
||||
"",
|
||||
f"本目录为从 Playbook vendoring 的裁剪快照(langs: {','.join(langs)})。",
|
||||
"",
|
||||
"## 使用",
|
||||
"",
|
||||
"在目标项目根目录执行:",
|
||||
"",
|
||||
"```sh",
|
||||
"python docs/standards/playbook/scripts/playbook.py -config playbook.toml",
|
||||
"```",
|
||||
"",
|
||||
"配置示例:`docs/standards/playbook/playbook.toml.example`",
|
||||
"",
|
||||
"文档入口:",
|
||||
"",
|
||||
"- `docs/standards/playbook/docs/index.md`",
|
||||
"- `.agents/index.md`",
|
||||
]
|
||||
(dest_prefix / "README.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def write_source_file(dest_prefix: Path, langs: list[str]) -> None:
|
||||
commit = read_git_commit(PLAYBOOK_ROOT)
|
||||
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
lines = [
|
||||
"# SOURCE",
|
||||
"",
|
||||
f"- Source: {PLAYBOOK_ROOT}",
|
||||
f"- Commit: {commit}",
|
||||
f"- Date: {timestamp}",
|
||||
f"- Langs: {','.join(langs)}",
|
||||
"- Generated-by: scripts/playbook.py",
|
||||
]
|
||||
(dest_prefix / "SOURCE.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def vendor_action(config: dict, context: dict) -> int:
|
||||
try:
|
||||
langs = normalize_langs(config.get("langs"))
|
||||
except ValueError as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
target_dir = config.get("target_dir", "docs/standards/playbook")
|
||||
target_path = Path(target_dir)
|
||||
if target_path.is_absolute() or ".." in target_path.parts:
|
||||
print(f"ERROR: invalid target_dir: {target_dir}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
project_root: Path = context["project_root"]
|
||||
dest_prefix = project_root / target_path
|
||||
dest_standards = dest_prefix.parent
|
||||
|
||||
ensure_dir(dest_standards)
|
||||
|
||||
if dest_prefix.exists():
|
||||
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
backup = dest_standards / f"{dest_prefix.name}.bak.{timestamp}"
|
||||
dest_prefix.rename(backup)
|
||||
log(f"Backed up existing snapshot -> {backup}")
|
||||
|
||||
ensure_dir(dest_prefix)
|
||||
|
||||
gitattributes_src = PLAYBOOK_ROOT / ".gitattributes"
|
||||
if gitattributes_src.is_file():
|
||||
copy2(gitattributes_src, dest_prefix / ".gitattributes")
|
||||
|
||||
copytree(PLAYBOOK_ROOT / "scripts", dest_prefix / "scripts")
|
||||
copytree(PLAYBOOK_ROOT / "codex", dest_prefix / "codex")
|
||||
copy2(PLAYBOOK_ROOT / "SKILLS.md", dest_prefix / "SKILLS.md")
|
||||
|
||||
common_docs = PLAYBOOK_ROOT / "docs/common"
|
||||
if common_docs.is_dir():
|
||||
copytree(common_docs, dest_prefix / "docs/common")
|
||||
|
||||
rulesets_root = PLAYBOOK_ROOT / "rulesets"
|
||||
ensure_dir(dest_prefix / "rulesets")
|
||||
if (rulesets_root / "index.md").is_file():
|
||||
copy2(rulesets_root / "index.md", dest_prefix / "rulesets/index.md")
|
||||
|
||||
templates_ci = PLAYBOOK_ROOT / "templates/ci"
|
||||
if templates_ci.is_dir():
|
||||
copytree(templates_ci, dest_prefix / "templates/ci")
|
||||
|
||||
for lang in langs:
|
||||
docs_src = PLAYBOOK_ROOT / "docs" / lang
|
||||
rules_src = PLAYBOOK_ROOT / "rulesets" / lang
|
||||
if not docs_src.is_dir():
|
||||
print(f"ERROR: docs not found for lang={lang}", file=sys.stderr)
|
||||
return 2
|
||||
if not rules_src.is_dir():
|
||||
print(f"ERROR: rulesets not found for lang={lang}", file=sys.stderr)
|
||||
return 2
|
||||
copytree(docs_src, dest_prefix / "docs" / lang)
|
||||
copytree(rules_src, dest_prefix / "rulesets" / lang)
|
||||
templates_src = PLAYBOOK_ROOT / "templates" / lang
|
||||
if templates_src.is_dir():
|
||||
copytree(templates_src, dest_prefix / "templates" / lang)
|
||||
|
||||
example_config = PLAYBOOK_ROOT / "playbook.toml.example"
|
||||
if example_config.is_file():
|
||||
copy2(example_config, dest_prefix / "playbook.toml.example")
|
||||
|
||||
write_docs_index(dest_prefix, langs)
|
||||
write_snapshot_readme(dest_prefix, langs)
|
||||
write_source_file(dest_prefix, langs)
|
||||
|
||||
log(f"Vendored snapshot -> {dest_prefix}")
|
||||
return 0
|
||||
|
||||
|
||||
def run_action(name: str, config: dict, context: dict) -> int:
|
||||
_ = config, context
|
||||
if name == "vendor":
|
||||
return vendor_action(config, context)
|
||||
print(f"[action] {name}")
|
||||
return 0
|
||||
|
||||
|
|
|
|||
|
|
@ -47,5 +47,23 @@ langs = ["tsl"]
|
|||
self.assertIn("sync_standards", output)
|
||||
self.assertIn("format_md", output)
|
||||
|
||||
def test_vendor_creates_snapshot(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
|
||||
[vendor]
|
||||
langs = ["tsl"]
|
||||
"""
|
||||
config_path = Path(tmp_dir) / "playbook.toml"
|
||||
config_path.write_text(config_body, encoding="utf-8")
|
||||
|
||||
result = run_cli("-config", str(config_path))
|
||||
|
||||
snapshot = Path(tmp_dir) / "docs/standards/playbook/SOURCE.md"
|
||||
self.assertEqual(result.returncode, 0)
|
||||
self.assertTrue(snapshot.is_file())
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Reference in New Issue