增加 Release 自动同步 workflow

This commit is contained in:
2026-08-21 17:48:19 +08:00
parent d3e19bcc46
commit 1e5b2d0b99
6 changed files with 683 additions and 5 deletions
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""Validate the distribution repository and its plugin manifest."""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parents[1]
SOURCES_PATH = REPO_ROOT / ".dist-sync" / "sources.json"
VERSIONS_PATH = REPO_ROOT / ".dist-sync" / "versions.json"
MARKETPLACE_PATH = REPO_ROOT / ".claude-plugin" / "marketplace.json"
class ValidationError(RuntimeError):
pass
def read_json(path: Path) -> Any:
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise ValidationError(f"无法读取 JSON:{path}: {error}") from error
def frontmatter_name(skill_file: Path) -> str:
try:
lines = skill_file.read_text(encoding="utf-8-sig").splitlines()
except (OSError, UnicodeDecodeError) as error:
raise ValidationError(f"无法读取 {skill_file}: {error}") from error
if not lines or lines[0].strip() != "---":
raise ValidationError(f"SKILL.md 缺少 YAML frontmatter:{skill_file}")
for line in lines[1:]:
if line.strip() == "---":
break
if line.startswith("name:"):
return line.split(":", 1)[1].strip().strip("'\"")
raise ValidationError(f"SKILL.md frontmatter 缺少 name:{skill_file}")
def main() -> int:
sources_doc = read_json(SOURCES_PATH)
sources = sources_doc.get("sources") if isinstance(sources_doc, dict) else None
if not isinstance(sources, list) or not sources:
raise ValidationError("sources.json 必须包含非空 sources 数组")
allowlisted: set[str] = set()
for source in sources:
if not isinstance(source, dict) or not all(
isinstance(source.get(field), str) and source[field] for field in ("name", "repository", "asset")
):
raise ValidationError("sources.json 包含无效来源")
name = source["name"]
if name in allowlisted:
raise ValidationError(f"sources.json 包含重复名称:{name}")
allowlisted.add(name)
versions = read_json(VERSIONS_PATH)
if not isinstance(versions, dict) or not all(
isinstance(name, str) and isinstance(tag, str) and tag for name, tag in versions.items()
):
raise ValidationError("versions.json 必须是非空 tag 字符串映射")
unknown_versions = set(versions) - allowlisted
if unknown_versions:
raise ValidationError(f"versions.json 包含未列入同步白名单的 skill:{sorted(unknown_versions)}")
marketplace = read_json(MARKETPLACE_PATH)
plugins = marketplace.get("plugins") if isinstance(marketplace, dict) else None
if not isinstance(plugins, list) or not plugins:
raise ValidationError("marketplace.json 必须包含非空 plugins 数组")
manifest_names: list[str] = []
manifest_paths: set[Path] = set()
for plugin in plugins:
skills = plugin.get("skills") if isinstance(plugin, dict) else None
if not isinstance(skills, list) or not skills:
raise ValidationError("marketplace.json 的每个 plugin 都必须列出 skills")
for raw_path in skills:
if not isinstance(raw_path, str) or not raw_path.startswith("./skills/"):
raise ValidationError(f"无效 marketplace skill 路径:{raw_path!r}")
relative = Path(raw_path[2:])
skill_dir = (REPO_ROOT / relative).resolve()
skills_root = (REPO_ROOT / "skills").resolve()
if skill_dir.parent != skills_root:
raise ValidationError(f"skill 必须直接位于 skills/<name>:{raw_path}")
if skill_dir in manifest_paths:
raise ValidationError(f"marketplace.json 重复列出路径:{raw_path}")
manifest_paths.add(skill_dir)
skill_file = skill_dir / "SKILL.md"
if not skill_file.is_file():
raise ValidationError(f"marketplace 路径缺少 SKILL.md:{raw_path}")
actual_name = frontmatter_name(skill_file)
if actual_name != skill_dir.name:
raise ValidationError(
f"{raw_path}/SKILL.md 的 name 为 {actual_name!r},目录名为 {skill_dir.name!r}"
)
manifest_names.append(actual_name)
if len(manifest_names) != len(set(manifest_names)):
raise ValidationError("marketplace.json 中存在重复的 skill name")
disk_dirs = {
path.resolve()
for path in (REPO_ROOT / "skills").iterdir()
if path.is_dir() and not path.name.startswith(".")
}
unlisted = disk_dirs - manifest_paths
missing = manifest_paths - disk_dirs
if unlisted:
raise ValidationError(f"skills/ 中存在未列入 marketplace 的目录:{sorted(path.name for path in unlisted)}")
if missing:
raise ValidationError(f"marketplace 指向不存在的目录:{sorted(path.name for path in missing)}")
for skill_dir in sorted(manifest_paths):
for path in skill_dir.rglob("*"):
if not path.is_file() or path.stat().st_size > 1024:
continue
if path.read_bytes().startswith(b"version https://git-lfs.github.com/spec/v1"):
raise ValidationError(f"发现 Git LFS pointer,分发副本缺少真实文件:{path.relative_to(REPO_ROOT)}")
print(f"分发仓校验通过:{len(manifest_names)} 个 skill,{len(allowlisted)} 个 Release 同步来源")
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except ValidationError as error:
print(f"校验失败:{error}", file=sys.stderr)
raise SystemExit(1)