mirror of
https://github.com/Shu-Ying/main_plugin_farm.git
synced 2026-10-29 05:29:58 +08:00
add # 新增Nonebot发布工作流
This commit is contained in:
+7
-1
@@ -5,4 +5,10 @@ check_baseline.py 为已实现的离线校验工具。
|
||||
检查清单哈希、缺失/新增文件、Python 3.10 语法、JSON 与静态作物库,
|
||||
不导入旧插件、不联网、不读真实玩家库。
|
||||
|
||||
迁移、分包和远程同步工具尚未实施,不提供会覆盖仓库的占位脚本。
|
||||
`build_zhenxun.py` / `build_nonebot.py` 从白名单生成对应框架仓库和 ZIP。
|
||||
`verify_zhenxun.py` / `verify_nonebot.py` 用标准库校验生成清单与文件。
|
||||
`publish_zhenxun.py` / `publish_nonebot.py` 默认离线报告,显式
|
||||
`--publish` 才读取 `FARM_RELEASE_TOKEN` 并同步固定目标仓库。
|
||||
`check_nonebot_runtime.py` 在临时数据目录检查已安装的 NoneBot 插件;
|
||||
也可用 `--repository` 指定生成仓库,需要 NoneBot 依赖和 OneBot 适配器。
|
||||
`check_workflows.py` 检查源工作流、生成工作流及发布权限边界。
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
"""从主仓库白名单生成标准 NoneBot 包仓库及可复现 ZIP。"""
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
|
||||
|
||||
from build_zhenxun import read_version, source_identity
|
||||
from verify_nonebot import MANIFEST, ROOT_NAME, verify_files, verify_package
|
||||
|
||||
|
||||
def build(root, output):
|
||||
root, output = Path(root).resolve(), Path(output).resolve()
|
||||
version = read_version(root)
|
||||
if output.exists():
|
||||
raise FileExistsError("Use a new output directory")
|
||||
if output.is_relative_to(root) and not output.is_relative_to(root / "dist"):
|
||||
raise ValueError("In-project output must be inside dist")
|
||||
files = {}
|
||||
prefix = ROOT_NAME + "/"
|
||||
integration = root / "integrations/nonebot"
|
||||
for name in ("__init__.py", "config.py", "commands.py", "data_paths.py"):
|
||||
files[prefix + name] = (integration / name).read_bytes()
|
||||
# 同一运行时源,避免两个适配器的业务命令与渲染行为发生分叉。
|
||||
files[prefix + "runtime.py"] = (
|
||||
root / "integrations/zhenxun/runtime.py"
|
||||
).read_bytes()
|
||||
files[prefix + "_vendor/__init__.py"] = b'"""Private farm modules."""\n'
|
||||
for package in ("farm_core", "farm_render"):
|
||||
source = root / "src" / package
|
||||
selected = list(source.glob("*.py")) + [
|
||||
path for path in (source / "data").iterdir()
|
||||
if path.suffix in (".json", ".zip", ".md")
|
||||
]
|
||||
for path in selected:
|
||||
if path.is_symlink():
|
||||
raise ValueError("Source links are not allowed")
|
||||
content = path.read_bytes()
|
||||
if package == "farm_render" and path.suffix == ".py":
|
||||
content = re.sub(
|
||||
rb"(?m)^from farm_core(?=[. ])", b"from ..farm_core",
|
||||
content,
|
||||
)
|
||||
relative = path.relative_to(source).as_posix()
|
||||
files[f"{prefix}_vendor/{package}/{relative}"] = content
|
||||
for name, source in {
|
||||
"LICENSE": "LICENSE", "README.md": "docs/12-nonebot.md",
|
||||
"version.txt": "version.txt",
|
||||
"images/farm-preview.png": "images/farm-preview.png",
|
||||
"pyproject.toml": "release/nonebot/pyproject.toml",
|
||||
"MANIFEST.in": "release/nonebot/MANIFEST.in",
|
||||
"verify_package.py": "scripts/verify_nonebot.py",
|
||||
".github/workflows/verify.yml": "release/nonebot/verify.yml",
|
||||
".github/workflows/publish.yaml": "release/nonebot/publish.yaml",
|
||||
}.items():
|
||||
files[name] = (root / source).read_bytes()
|
||||
files[prefix + "_version.py"] = f'__version__ = "{version}"\n'.encode()
|
||||
for name, content in files.items():
|
||||
if name.endswith(".py"):
|
||||
ast.parse(content, filename=name, feature_version=(3, 10))
|
||||
manifest = {
|
||||
"schema": 1, "target": "nonebot", "plugin": ROOT_NAME,
|
||||
"version": version, "source": source_identity(root),
|
||||
"files": {name: hashlib.sha256(content).hexdigest()
|
||||
for name, content in sorted(files.items())},
|
||||
}
|
||||
files[MANIFEST] = (json.dumps(
|
||||
manifest, ensure_ascii=False, sort_keys=True, indent=2,
|
||||
) + "\n").encode()
|
||||
verify_files(files)
|
||||
output.mkdir(parents=True)
|
||||
for name, content in sorted(files.items()):
|
||||
path = output / "repository" / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(content)
|
||||
archive_path = output / f"{ROOT_NAME}-{version}.zip"
|
||||
with ZipFile(archive_path, "w", compression=ZIP_DEFLATED) as archive:
|
||||
for name, content in sorted(files.items()):
|
||||
item = ZipInfo(f"{ROOT_NAME}/{name}", (2020, 1, 1, 0, 0, 0))
|
||||
item.create_system = 3
|
||||
item.compress_type = ZIP_DEFLATED
|
||||
item.external_attr = 0o100644 << 16
|
||||
archive.writestr(item, content)
|
||||
verify_package(archive_path)
|
||||
digest = hashlib.sha256(archive_path.read_bytes()).hexdigest()
|
||||
archive_path.with_suffix(".zip.sha256").write_text(
|
||||
f"{digest} {archive_path.name}\n", encoding="ascii",
|
||||
)
|
||||
return archive_path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
print(build(Path(__file__).resolve().parents[1], args.output))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,104 @@
|
||||
"""在临时目录加载真实 NoneBot 插件,离线验证生命周期与命令适配。"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
async def exercise(module, bot, event, Message, UniMessage):
|
||||
from nonebot_plugin_alconna import Image
|
||||
|
||||
runtime = module.runtime
|
||||
await runtime.start()
|
||||
try:
|
||||
session = SimpleNamespace(user=SimpleNamespace(id="10001", name="测试"))
|
||||
with patch.object(UniMessage, "send", autospec=True) as send:
|
||||
await module.handler_for("register")(
|
||||
bot, event, session, Message(),
|
||||
)
|
||||
if "开通" not in str(send.call_args.args[0]):
|
||||
raise AssertionError("Registration handler failed")
|
||||
await module.handler_for("farm")(
|
||||
bot, event, session, Message(),
|
||||
)
|
||||
# 无字体环境允许文字退化;有字体时必须生成统一图片段。
|
||||
response = send.call_args.args[0]
|
||||
if runtime.rendering and not response[Image]:
|
||||
raise AssertionError("Image response was not a UniMessage")
|
||||
await module.handler_for("steal")(
|
||||
bot, event, session, Message("[CQ:at,qq=10002]"),
|
||||
)
|
||||
if "目标尚未" not in str(send.call_args.args[0]):
|
||||
raise AssertionError("Mention target was not mapped")
|
||||
await module.handler_for("steal")(
|
||||
bot, event, session, Message("[CQ:at,qq=all]"),
|
||||
)
|
||||
if "具体" not in str(send.call_args.args[0]):
|
||||
raise AssertionError("Everyone mention was not rejected")
|
||||
context = dict(
|
||||
bot_id=str(bot.self_id), platform=bot.adapter.get_name(),
|
||||
user_id="10001", message_id="buy",
|
||||
)
|
||||
first = await runtime.execute("buy", "胡萝卜 1", **context)
|
||||
second = await runtime.execute("buy", "胡萝卜 1", **context)
|
||||
if first != second:
|
||||
raise AssertionError("Repeated event was not idempotent")
|
||||
context["message_id"] = None
|
||||
refused = await runtime.execute("sow", "胡萝卜 1", **context)
|
||||
if "未执行" not in refused:
|
||||
raise AssertionError("Write without message ID was accepted")
|
||||
finally:
|
||||
await runtime.close()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--repository", type=Path)
|
||||
args = parser.parse_args()
|
||||
if args.repository:
|
||||
sys.path.insert(0, str(args.repository.resolve()))
|
||||
import nonebot
|
||||
from nonebot.adapters.onebot.v11 import Adapter, Bot, Message
|
||||
from nonebot.adapters.onebot.v11.event import PrivateMessageEvent
|
||||
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
nonebot.init(
|
||||
driver="~none",
|
||||
localstore_data_dir=root / "data",
|
||||
localstore_config_dir=root / "config",
|
||||
localstore_cache_dir=root / "cache",
|
||||
)
|
||||
driver = nonebot.get_driver()
|
||||
driver.register_adapter(Adapter)
|
||||
plugin = nonebot.load_plugin("nonebot_plugin_farm")
|
||||
if plugin is None:
|
||||
raise AssertionError("NoneBot plugin failed to load")
|
||||
from nonebot_plugin_farm import commands, __version__
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
|
||||
expected = (root / "data/nonebot_plugin_farm"
|
||||
/ "nonebot_plugin_farm/farm_db")
|
||||
if commands.runtime.data_dir != expected.resolve():
|
||||
raise AssertionError("Legacy localstore directory changed")
|
||||
adapter = driver._adapters[Adapter.get_name()]
|
||||
bot = Bot(adapter, "12345")
|
||||
event = PrivateMessageEvent(
|
||||
time=0, self_id=12345, post_type="message",
|
||||
sub_type="friend", user_id=10001, message_type="private",
|
||||
message_id=1, message=Message(), original_message=Message(),
|
||||
raw_message="", font=0, sender={"user_id": 10001},
|
||||
)
|
||||
asyncio.run(exercise(commands, bot, event, Message, UniMessage))
|
||||
if any(name == "zhenxun" or name.startswith("zhenxun.")
|
||||
for name in sys.modules):
|
||||
raise AssertionError("Unexpected Zhenxun import")
|
||||
print(f"NoneBot {__version__}: load, lifecycle, messages checked")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,4 +1,4 @@
|
||||
"""检查两个生成工作流的YAML及发布权限;依赖仅用于开发检查。"""
|
||||
"""检查真寻与 NoneBot 工作流的 YAML 和发布边界。"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
@@ -46,6 +46,46 @@ def main():
|
||||
raise ValueError("Generated workflow permissions differ")
|
||||
if generated["on"]["push"]["branches"] != ["main"]:
|
||||
raise ValueError("Generated branch differs")
|
||||
nonebot = yaml.load((
|
||||
root / ".github/workflows/nonebot-preview.yml"
|
||||
).read_text(encoding="utf-8"), Loader=yaml.BaseLoader)
|
||||
if nonebot["permissions"] != {"contents": "read"}:
|
||||
raise ValueError("NoneBot source permissions differ")
|
||||
trigger = nonebot["on"]
|
||||
if (set(trigger) != {"push", "pull_request", "workflow_dispatch"}
|
||||
or trigger["push"]["branches"] != ["main"]
|
||||
or set(trigger["workflow_dispatch"]["inputs"]) != {"publish"}
|
||||
or trigger["workflow_dispatch"]["inputs"]["publish"]["default"]
|
||||
!= "false"):
|
||||
raise ValueError("NoneBot source triggers differ")
|
||||
sync = nonebot["jobs"]["sync-preview"]
|
||||
if (sync["environment"] != "zhenxun-preview"
|
||||
or " ".join(sync["if"].split()) != expected_condition
|
||||
or nonebot["jobs"]["build"]["needs"] != "test"
|
||||
or sync["needs"] != ["test", "build"]):
|
||||
raise ValueError("NoneBot sync boundary differs")
|
||||
token = sync["steps"][-1]["env"]["FARM_RELEASE_TOKEN"]
|
||||
if token != "${{ secrets.FARM_RELEASE_TOKEN }}":
|
||||
raise ValueError("NoneBot must reuse FARM_RELEASE_TOKEN")
|
||||
generated = yaml.load((
|
||||
root / "release/nonebot/verify.yml"
|
||||
).read_text(encoding="utf-8"), Loader=yaml.BaseLoader)
|
||||
if (generated["permissions"] != {"contents": "read"}
|
||||
or generated["on"]["push"]["branches"] != ["main"]):
|
||||
raise ValueError("NoneBot generated verification boundary differs")
|
||||
publishing = yaml.load((
|
||||
root / "release/nonebot/publish.yaml"
|
||||
).read_text(encoding="utf-8"), Loader=yaml.BaseLoader)
|
||||
if (publishing["permissions"] != {"contents": "read"}
|
||||
or publishing["on"] != {"push": {"tags": ["v*"]}}
|
||||
or publishing["jobs"]["publish"]["needs"] != "build"
|
||||
or publishing["jobs"]["publish"]["if"] != (
|
||||
"github.repository == 'Shu-Ying/nonebot_plugin_farm'"
|
||||
)):
|
||||
raise ValueError("NoneBot PyPI boundary differs")
|
||||
steps = publishing["jobs"]["publish"]["steps"]
|
||||
if steps[-1]["with"]["password"] != "${{ secrets.PYPI_TOKEN }}":
|
||||
raise ValueError("Keep the existing PyPI secret")
|
||||
print("Workflow YAML and publication guards checked")
|
||||
|
||||
|
||||
|
||||
@@ -156,6 +156,12 @@ def export(root):
|
||||
bundle.writestr(item, content)
|
||||
apply_positions(root, metadata)
|
||||
apply_decorations(root, metadata)
|
||||
# 与已发布的地块编号一致:前两行交错排列,初始六块组成 2×3。
|
||||
# 标定仍使用上面的旧资源顺序;这里只调整最终运行时的编号位置。
|
||||
positions = metadata["positions"]
|
||||
metadata["positions"] = [positions[index] for index in (
|
||||
0, 6, 1, 7, 2, 8, 3, 9, 4, 10, 5, 11,
|
||||
)] + positions[12:]
|
||||
return output.getvalue(), (
|
||||
json.dumps(metadata, ensure_ascii=False, sort_keys=True, indent=2)
|
||||
+ "\n"
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
"""固定同步 NoneBot main;默认离线 dry-run,普通提交保留历史。"""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
from urllib.error import HTTPError
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from verify_nonebot import read_package, verify_files
|
||||
|
||||
REPOSITORY = "Shu-Ying/nonebot_plugin_farm"
|
||||
BRANCH = "main"
|
||||
|
||||
|
||||
def github(method, path, payload, token, allow_missing=False):
|
||||
url = f"https://api.github.com/repos/{REPOSITORY}"
|
||||
if path:
|
||||
url += f"/{path}"
|
||||
request = Request(
|
||||
url,
|
||||
data=json.dumps(payload).encode() if payload is not None else None,
|
||||
headers={"Authorization": f"Bearer {token}",
|
||||
"Accept": "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
"User-Agent": "farm-nonebot-publisher"}, method=method,
|
||||
)
|
||||
try:
|
||||
with urlopen(request, timeout=30) as response:
|
||||
return json.load(response)
|
||||
except HTTPError as error:
|
||||
if error.code == 404 and method == "GET" and allow_missing:
|
||||
return None
|
||||
raise RuntimeError(
|
||||
f"GitHub request failed: {method} {url}: HTTP {error.code}",
|
||||
) from None
|
||||
|
||||
|
||||
def publish(package, confirmed=False, request=None):
|
||||
files = read_package(package)
|
||||
manifest = verify_files(files)
|
||||
result = {"repository": REPOSITORY, "branch": BRANCH,
|
||||
"version": manifest["version"], "published": False}
|
||||
if not confirmed:
|
||||
return result
|
||||
if manifest["source"]["dirty"]:
|
||||
raise ValueError("Publish requires a clean source checkout")
|
||||
token = os.environ.get("FARM_RELEASE_TOKEN")
|
||||
if not token:
|
||||
raise ValueError("Configure FARM_RELEASE_TOKEN in a protected job")
|
||||
call = request or (lambda method, path, data: github(
|
||||
method, path, data, token,
|
||||
allow_missing=path.startswith(("git/ref/heads/", "contents/")),
|
||||
))
|
||||
if not call("GET", "", None):
|
||||
raise ValueError(f"Target repository is not accessible: {REPOSITORY}")
|
||||
ref = call("GET", f"git/ref/heads/{BRANCH}", None)
|
||||
parent = ref["object"]["sha"] if ref else None
|
||||
if parent:
|
||||
owned = call("GET", f"contents/FARM-MANIFEST.json?ref={BRANCH}", None)
|
||||
if owned is not None:
|
||||
if owned.get("encoding") != "base64":
|
||||
raise ValueError("Existing Farm manifest encoding is invalid")
|
||||
previous = json.loads(base64.b64decode(owned["content"]))
|
||||
if (previous.get("target") != "nonebot"
|
||||
or previous.get("plugin") != manifest["plugin"]):
|
||||
raise ValueError("Existing branch ownership differs")
|
||||
tree = []
|
||||
for name, content in sorted(files.items()):
|
||||
blob = call("POST", "git/blobs", {
|
||||
"content": base64.b64encode(content).decode(),
|
||||
"encoding": "base64",
|
||||
})
|
||||
tree.append({"path": name, "mode": "100644", "type": "blob",
|
||||
"sha": blob["sha"]})
|
||||
generated = call("POST", "git/trees", {"tree": tree})
|
||||
commit = call("POST", "git/commits", {
|
||||
"message": f"Generate NoneBot farm {manifest['version']} "
|
||||
f"from {manifest['source']['commit']}",
|
||||
"tree": generated["sha"], "parents": [parent] if parent else [],
|
||||
})
|
||||
if parent:
|
||||
current = call("GET", f"git/ref/heads/{BRANCH}", None)
|
||||
if not current or current["object"]["sha"] != parent:
|
||||
raise RuntimeError("Target branch changed; retry explicitly")
|
||||
call("PATCH", f"git/refs/heads/{BRANCH}", {
|
||||
"sha": commit["sha"], "force": False,
|
||||
})
|
||||
else:
|
||||
call("POST", "git/refs", {
|
||||
"ref": f"refs/heads/{BRANCH}", "sha": commit["sha"],
|
||||
})
|
||||
return {**result, "published": True, "commit": commit["sha"]}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("package")
|
||||
parser.add_argument("--publish", action="store_true")
|
||||
args = parser.parse_args()
|
||||
print(json.dumps(publish(args.package, args.publish), ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,105 @@
|
||||
"""仅用标准库校验 NoneBot 生成仓库或 ZIP;不启动插件。"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path, PurePosixPath
|
||||
import stat
|
||||
from zipfile import ZipFile
|
||||
|
||||
ROOT_NAME = "nonebot_plugin_farm"
|
||||
MANIFEST = "FARM-MANIFEST.json"
|
||||
MAX_BYTES = 100 * 1024 * 1024
|
||||
|
||||
|
||||
def safe_path(name):
|
||||
path = PurePosixPath(name)
|
||||
if (not name or "\\" in name or ":" in name or path.is_absolute()
|
||||
or any(part in (".", "..", "") for part in name.split("/"))
|
||||
or path.as_posix() != name):
|
||||
raise ValueError("Unsafe package path")
|
||||
return path
|
||||
|
||||
|
||||
def verify_files(files):
|
||||
if MANIFEST not in files or len(files[MANIFEST]) > 1024 * 1024:
|
||||
raise ValueError("Missing or oversized manifest")
|
||||
manifest = json.loads(files[MANIFEST])
|
||||
if (manifest.get("schema") != 1 or manifest.get("target") != "nonebot"
|
||||
or manifest.get("plugin") != ROOT_NAME):
|
||||
raise ValueError("Invalid package identity")
|
||||
expected = manifest["files"]
|
||||
if (not isinstance(expected, dict)
|
||||
or set(files) != set(expected) | {MANIFEST}):
|
||||
raise ValueError("Package files differ from manifest")
|
||||
for name, digest in expected.items():
|
||||
safe_path(name)
|
||||
if hashlib.sha256(files[name]).hexdigest() != digest:
|
||||
raise ValueError("Package checksum mismatch")
|
||||
required = {
|
||||
"pyproject.toml", "MANIFEST.in", "version.txt", "LICENSE",
|
||||
"nonebot_plugin_farm/__init__.py", "nonebot_plugin_farm/config.py",
|
||||
"nonebot_plugin_farm/commands.py", "nonebot_plugin_farm/runtime.py",
|
||||
"nonebot_plugin_farm/_vendor/farm_core/__init__.py",
|
||||
"nonebot_plugin_farm/_vendor/farm_render/data/assets.zip",
|
||||
".github/workflows/publish.yaml", ".github/workflows/verify.yml",
|
||||
}
|
||||
if not required <= set(files):
|
||||
raise ValueError("Incomplete plugin repository")
|
||||
if files["version.txt"].decode().strip() != manifest["version"]:
|
||||
raise ValueError("Version file differs from manifest")
|
||||
return manifest
|
||||
|
||||
|
||||
def read_package(path):
|
||||
with ZipFile(path) as archive:
|
||||
items = archive.infolist()
|
||||
names = [item.filename for item in items]
|
||||
if (len(names) != len(set(names)) or len(names) > 200
|
||||
or sum(item.file_size for item in items) > MAX_BYTES):
|
||||
raise ValueError("Duplicate paths or package too large")
|
||||
files = {}
|
||||
for item in items:
|
||||
name = safe_path(item.filename)
|
||||
if (len(name.parts) < 2 or name.parts[0] != ROOT_NAME
|
||||
or item.is_dir()
|
||||
or stat.S_ISLNK(item.external_attr >> 16)):
|
||||
raise ValueError("Unexpected package root or link")
|
||||
files[name.relative_to(ROOT_NAME).as_posix()] = archive.read(item)
|
||||
verify_files(files)
|
||||
return files
|
||||
|
||||
|
||||
def verify_package(path):
|
||||
return verify_files(read_package(path))
|
||||
|
||||
|
||||
def verify_directory(directory):
|
||||
directory = Path(directory).resolve()
|
||||
files = {}
|
||||
for path in directory.rglob("*"):
|
||||
relative = path.relative_to(directory)
|
||||
if any(part in (".git", "__pycache__") for part in relative.parts):
|
||||
continue
|
||||
if path.is_symlink():
|
||||
raise ValueError("Unexpected symbolic link")
|
||||
if path.is_file():
|
||||
files[relative.as_posix()] = path.read_bytes()
|
||||
return verify_files(files)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("package", nargs="?", type=Path)
|
||||
parser.add_argument("--directory", type=Path)
|
||||
args = parser.parse_args()
|
||||
if bool(args.package) == bool(args.directory):
|
||||
parser.error("Choose a ZIP or --directory")
|
||||
manifest = (verify_directory(args.directory) if args.directory
|
||||
else verify_package(args.package))
|
||||
print(f"Verified nonebot {manifest['version']}: "
|
||||
f"{len(manifest['files'])} files")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user