增加 Release 自动同步 workflow
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user