feat(skill): support creating plugin publish repos

This commit is contained in:
jxxghp
2026-06-19 09:26:16 +08:00
parent bd53598704
commit e8f6e8647b
3 changed files with 323 additions and 15 deletions
+158 -1
View File
@@ -1,8 +1,9 @@
import importlib.util
import json
import sys
from argparse import Namespace
from pathlib import Path
from typing import Any
from typing import Any, Optional
def load_publish_plugin_module() -> Any:
@@ -68,3 +69,159 @@ def test_merge_package_content_preserves_other_plugins() -> None:
assert package_data["OtherPlugin"] == {"name": "其他插件", "version": "1.0.0"}
assert package_data["MyPlugin"] == {"name": "新插件", "version": "1.0.0"}
def test_push_creates_public_repo_when_explicitly_requested(tmp_path: Path) -> None:
"""显式允许自动建仓时,推送会默认创建公开仓库。"""
module = load_publish_plugin_module()
plugin_dir = tmp_path / "plugins.v2" / "myplugin"
plugin_dir.mkdir(parents=True)
(plugin_dir / "__init__.py").write_text("class MyPlugin:\n pass\n", encoding="utf-8")
(tmp_path / "package.v2.json").write_text(
json.dumps({"MyPlugin": {"name": "测试插件", "version": "1.0.0"}}, ensure_ascii=False),
encoding="utf-8",
)
class FakeClient:
"""模拟缺失仓库的 GitHub 客户端。"""
def __init__(self) -> None:
"""初始化调用记录。"""
self.created_private = None
self.put_paths: list[str] = []
def repo_exists(self) -> bool:
"""返回仓库不存在。"""
return False
def create_repo(self, private: bool = False) -> dict[str, Any]:
"""记录建仓可见性。"""
self.created_private = private
return {"private": private, "html_url": "https://github.com/example/repo"}
def put_file(
self,
path: str,
content: bytes,
message: str,
sha: Optional[str] = None,
) -> dict[str, Any]:
"""记录上传路径。"""
self.put_paths.append(path)
return {"content": {"path": path}}
def delete_file(self, path: str, message: str, sha: str) -> dict[str, Any]:
"""记录删除路径。"""
return {"content": {"path": path}}
fake_client = FakeClient()
def fake_build_context(args: Namespace, require_token: bool = False) -> dict[str, Any]:
"""构造无需访问网络的推送上下文。"""
layout = module.Layout(package_file="package.v2.json", plugin_root="plugins.v2")
return {
"repo": args.repo,
"branch": args.branch,
"local_repo": tmp_path,
"layout": layout,
"plugin_id": args.plugin_id,
"remote_prefix": module.remote_plugin_prefix(layout, args.plugin_id),
"client": fake_client,
"excludes": list(module.DEFAULT_EXCLUDES),
"includes": [],
}
original_build_context = module.build_context
module.build_context = fake_build_context
try:
payload = module.push(
Namespace(
command="push",
repo="example/repo",
plugin_id="MyPlugin",
local_repo=str(tmp_path),
package_version="v2",
branch="main",
token="token",
message="Publish MyPlugin",
api_base=module.GITHUB_API_BASE,
proxy="",
timeout=module.DEFAULT_TIMEOUT,
include=[],
exclude=[],
delete_remote=False,
create_repo_if_missing=True,
private=False,
force=False,
dry_run=False,
)
)
finally:
module.build_context = original_build_context
assert payload["repo_existed"] is False
assert fake_client.created_private is False
assert "package.v2.json" in fake_client.put_paths
assert "plugins.v2/myplugin/__init__.py" in fake_client.put_paths
def test_create_repo_dry_run_does_not_touch_github() -> None:
"""建仓 dry-run 不访问 GitHub,只返回计划。"""
module = load_publish_plugin_module()
class FakeClient:
"""模拟不应被调用的 GitHub 客户端。"""
def repo_exists(self) -> bool:
"""如果被调用则说明 dry-run 行为错误。"""
raise AssertionError("dry-run should not query GitHub")
def create_repo(self, private: bool = False) -> dict[str, Any]:
"""如果被调用则说明 dry-run 行为错误。"""
raise AssertionError("dry-run should not create GitHub repo")
def fake_build_context(args: Namespace, require_token: bool = False) -> dict[str, Any]:
"""构造无需访问网络的建仓上下文。"""
return {
"repo": args.repo,
"branch": args.branch,
"local_repo": Path.cwd(),
"layout": module.Layout(package_file="package.v2.json", plugin_root="plugins.v2"),
"plugin_id": "",
"remote_prefix": "",
"client": FakeClient(),
"excludes": list(module.DEFAULT_EXCLUDES),
"includes": [],
}
original_build_context = module.build_context
module.build_context = fake_build_context
try:
payload = module.create_repo(
Namespace(
command="create-repo",
repo="example/repo",
plugin_id="",
local_repo="",
package_version="auto",
branch="main",
token="",
message="",
api_base=module.GITHUB_API_BASE,
proxy="",
timeout=module.DEFAULT_TIMEOUT,
include=[],
exclude=[],
delete_remote=False,
create_repo_if_missing=False,
private=False,
force=False,
dry_run=True,
)
)
finally:
module.build_context = original_build_context
assert payload["success"] is True
assert payload["dry_run"] is True
assert payload["private"] is False