-
Notifications
You must be signed in to change notification settings - Fork 674
ci: 增加官方 V3 插件依赖真实安装门禁 #1190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
ci: 增加官方 V3 插件依赖真实安装门禁 #1190
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| """在隔离环境中按声明平台真实安装 V3 插件依赖清单。""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import os | ||
| import shutil | ||
| import subprocess | ||
| import tempfile | ||
| from pathlib import Path | ||
|
|
||
| import tomllib | ||
|
|
||
| REPO_ROOT = Path(__file__).resolve().parents[1] | ||
| V3_ROOT = REPO_ROOT / "plugins.v3" | ||
| SUPPORTED_PLATFORMS = frozenset( | ||
| { | ||
| "linux-x64", | ||
| "linux-arm64", | ||
| "windows-x64", | ||
| "macos-x64", | ||
| "macos-arm64", | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| def discover_manifests(root: Path = V3_ROOT) -> list[Path]: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| """返回仓库中全部 V3 modern manifest。""" | ||
| return sorted(root.glob("*/pyproject.toml")) | ||
|
|
||
|
|
||
| def manifest_platforms(manifest: Path) -> frozenset[str]: | ||
| """返回清单声明的安装门禁平台,未声明时覆盖标准五平台。""" | ||
| with manifest.open("rb") as file_obj: | ||
| document = tomllib.load(file_obj) | ||
| configured = ( | ||
| document.get("tool", {}) | ||
| .get("moviepilot", {}) | ||
| .get("dependency-gate", {}) | ||
| .get("platforms") | ||
| ) | ||
| if configured is None: | ||
| return SUPPORTED_PLATFORMS | ||
| if ( | ||
| not isinstance(configured, list) | ||
| or not configured | ||
| or not all(isinstance(item, str) for item in configured) | ||
| ): | ||
| raise ValueError(f"{manifest} 的 dependency-gate.platforms 必须是非空字符串列表") | ||
| platforms = frozenset(configured) | ||
| unknown = platforms - SUPPORTED_PLATFORMS | ||
| if unknown: | ||
| raise ValueError(f"{manifest} 声明了未知安装平台:{sorted(unknown)}") | ||
| return platforms | ||
|
|
||
|
|
||
| def venv_python(environment: Path, *, windows: bool | None = None) -> Path: | ||
| """返回目标虚拟环境的解释器路径。""" | ||
| is_windows = os.name == "nt" if windows is None else windows | ||
| if is_windows: | ||
| return environment / "Scripts" / "python.exe" | ||
| return environment / "bin" / "python" | ||
|
|
||
|
|
||
| def installation_commands( | ||
| *, | ||
| uv_bin: str, | ||
| python_spec: str, | ||
| environment: Path, | ||
| manifest: Path, | ||
| windows: bool | None = None, | ||
| ) -> tuple[list[str], list[str], list[str]]: | ||
| """构造与宿主插件安装语义一致的隔离安装和健康检查命令。""" | ||
| python_bin = venv_python(environment, windows=windows) | ||
| return ( | ||
| [uv_bin, "venv", "--python", python_spec, str(environment)], | ||
| [ | ||
| uv_bin, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CUDA 源未应用 这里使用 |
||
| "pip", | ||
| "install", | ||
| "--python", | ||
| str(python_bin), | ||
| "-r", | ||
| str(manifest), | ||
| ], | ||
| [uv_bin, "pip", "check", "--python", str(python_bin)], | ||
| ) | ||
|
|
||
|
|
||
| def verify_manifest(*, uv_bin: str, python_spec: str, manifest: Path) -> None: | ||
| """在一次性虚拟环境中安装并检查指定清单。""" | ||
| prefix = f"moviepilot-{manifest.parent.name}-" | ||
| with tempfile.TemporaryDirectory(prefix=prefix) as temp_dir: | ||
| environment = Path(temp_dir) / ".venv" | ||
| for command in installation_commands( | ||
| uv_bin=uv_bin, | ||
| python_spec=python_spec, | ||
| environment=environment, | ||
| manifest=manifest, | ||
| ): | ||
| subprocess.run(command, cwd=REPO_ROOT, check=True) | ||
|
|
||
|
|
||
| def parse_args() -> argparse.Namespace: | ||
| """解析命令行参数。""" | ||
| parser = argparse.ArgumentParser( | ||
| description="Install V3 plugin manifests supported by a CI platform.", | ||
| ) | ||
| parser.add_argument("--python", default="3.14", help="目标 Python 解释器") | ||
| parser.add_argument("--uv", default="uv", help="uv 可执行文件") | ||
| parser.add_argument( | ||
| "--platform", | ||
| required=True, | ||
| choices=sorted(SUPPORTED_PLATFORMS), | ||
| help="当前依赖安装门禁平台", | ||
| ) | ||
| return parser.parse_args() | ||
|
|
||
|
|
||
| def main() -> int: | ||
| """执行当前平台全部适用 V3 插件依赖的真实安装门禁。""" | ||
| args = parse_args() | ||
| uv_bin = shutil.which(args.uv) | ||
| if not uv_bin: | ||
| print(f"未找到 uv 可执行文件:{args.uv}") | ||
| return 1 | ||
|
|
||
| manifests = discover_manifests() | ||
| if not manifests: | ||
| print("未发现 V3 插件 pyproject.toml,拒绝空门禁") | ||
| return 1 | ||
|
|
||
| try: | ||
| selected = [ | ||
| manifest | ||
| for manifest in manifests | ||
| if args.platform in manifest_platforms(manifest) | ||
| ] | ||
| except ValueError as err: | ||
| print(err) | ||
| return 1 | ||
| if not selected: | ||
| print(f"{args.platform} 没有适用的 V3 依赖清单,拒绝空平台门禁") | ||
| return 1 | ||
|
|
||
| for manifest in manifests: | ||
| relative_manifest = manifest.relative_to(REPO_ROOT) | ||
| if manifest not in selected: | ||
| print(f"跳过不支持 {args.platform} 的 {relative_manifest}") | ||
| continue | ||
| print(f"真实安装 {relative_manifest}", flush=True) | ||
| try: | ||
| verify_manifest( | ||
| uv_bin=uv_bin, | ||
| python_spec=args.python, | ||
| manifest=manifest, | ||
| ) | ||
| except subprocess.CalledProcessError as err: | ||
| print(f"{relative_manifest} 安装门禁失败,退出码:{err.returncode}") | ||
| return err.returncode or 1 | ||
|
|
||
| print(f"{args.platform} V3 插件依赖真实安装门禁通过:{len(selected)} 份清单") | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| """V3 插件依赖真实安装门禁测试。""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import importlib.util | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| REPO_ROOT = Path(__file__).resolve().parents[2] | ||
| INSTALL_SCRIPT = REPO_ROOT / "scripts/check_v3_dependency_install.py" | ||
| PR_WORKFLOW = REPO_ROOT / ".github/workflows/plugin-gate.yml" | ||
|
|
||
|
|
||
| def _load_install_module(): | ||
| """按文件路径导入安装门禁脚本。""" | ||
| spec = importlib.util.spec_from_file_location( | ||
| "check_v3_dependency_install", | ||
| INSTALL_SCRIPT, | ||
| ) | ||
| module = importlib.util.module_from_spec(spec) | ||
| assert spec and spec.loader | ||
| sys.modules[spec.name] = module | ||
| spec.loader.exec_module(module) | ||
| return module | ||
|
|
||
|
|
||
| def test_manifest_discovery_covers_every_v3_pyproject() -> None: | ||
| """门禁必须自动覆盖全部 V3 modern manifest。""" | ||
| module = _load_install_module() | ||
| expected = sorted((REPO_ROOT / "plugins.v3").glob("*/pyproject.toml")) | ||
|
|
||
| assert expected | ||
| assert module.discover_manifests() == expected | ||
|
|
||
|
|
||
| def test_manifest_platforms_default_to_product_matrix() -> None: | ||
| """未声明窄平台的普通插件必须覆盖 V3 标准五平台。""" | ||
| module = _load_install_module() | ||
| manifest = REPO_ROOT / "plugins.v3/agentresourceofficer/pyproject.toml" | ||
|
|
||
| assert module.manifest_platforms(manifest) == module.SUPPORTED_PLATFORMS | ||
|
|
||
|
|
||
| def test_animeupscale_dependency_gate_matches_linux_cuda_contract() -> None: | ||
| """AnimeUpscale 的大体积 CUDA 依赖只在真实支持的 Linux x64 安装。""" | ||
| module = _load_install_module() | ||
| manifest = REPO_ROOT / "plugins.v3/animeupscale/pyproject.toml" | ||
|
|
||
| assert module.manifest_platforms(manifest) == frozenset({"linux-x64"}) | ||
|
|
||
|
|
||
| def test_installation_uses_fresh_environment_and_host_manifest_semantics( | ||
| tmp_path: Path, | ||
| ) -> None: | ||
| """安装命令必须面向隔离解释器并通过 -r 消费原始 pyproject。""" | ||
| module = _load_install_module() | ||
| environment = tmp_path / ".venv" | ||
| manifest = REPO_ROOT / "plugins.v3/agentresourceofficer/pyproject.toml" | ||
|
|
||
| create, install, healthcheck = module.installation_commands( | ||
| uv_bin="uv", | ||
| python_spec="3.14", | ||
| environment=environment, | ||
| manifest=manifest, | ||
| windows=False, | ||
| ) | ||
|
|
||
| python_bin = environment / "bin/python" | ||
| assert create == ["uv", "venv", "--python", "3.14", str(environment)] | ||
| assert install == [ | ||
| "uv", | ||
| "pip", | ||
| "install", | ||
| "--python", | ||
| str(python_bin), | ||
| "-r", | ||
| str(manifest), | ||
| ] | ||
| assert healthcheck == ["uv", "pip", "check", "--python", str(python_bin)] | ||
|
|
||
|
|
||
| def test_windows_environment_uses_scripts_python(tmp_path: Path) -> None: | ||
| """Windows runner 必须把依赖安装到目标 venv,而不是 runner 全局环境。""" | ||
| module = _load_install_module() | ||
|
|
||
| assert module.venv_python(tmp_path / ".venv", windows=True) == ( | ||
| tmp_path / ".venv/Scripts/python.exe" | ||
| ) | ||
|
|
||
|
|
||
| def test_workflow_runs_scoped_five_platform_install_matrix() -> None: | ||
| """PR 门禁应在相关变更时按平台执行真实安装脚本。""" | ||
| workflow = PR_WORKFLOW.read_text(encoding="utf-8") | ||
| job_start = workflow.index(" plugin-dependency-install-gate:") | ||
| job_end = len(workflow) | ||
| install_job = workflow[job_start:job_end] | ||
|
|
||
| expected_targets = { | ||
| "ubuntu-latest": "linux-x64", | ||
| "ubuntu-24.04-arm": "linux-arm64", | ||
| "windows-latest": "windows-x64", | ||
| "macos-15-intel": "macos-x64", | ||
| "macos-15": "macos-arm64", | ||
| } | ||
| for runner, platform in expected_targets.items(): | ||
| assert f"os: {runner}" in install_job | ||
| assert f"platform: {platform}" in install_job | ||
| assert "runs-on: ${{ matrix.os }}" in install_job | ||
| assert "fetch-depth: 0" in install_job | ||
| assert ( | ||
| 'git diff --quiet "${{ github.event.pull_request.base.sha }}" HEAD' | ||
| in install_job | ||
| ) | ||
| for pathspec in ( | ||
| "plugins.v3/*/pyproject.toml", | ||
| "scripts/check_v3_dependency_install.py", | ||
| "tests/ci/test_v3_dependency_install_gate.py", | ||
| ".github/workflows/plugin-gate.yml", | ||
| ): | ||
| assert pathspec in install_job | ||
| assert "steps.dependency-scope.outputs.run == 'true'" in install_job | ||
| assert "--platform \"${{ matrix.platform }}\"" in install_job |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
触发漏检
依赖门禁的变更检测路径没有包含
package.v3.json。当 PR 只修改发布范围(例如将一个已有pyproject.toml的插件加入 V3 发布包)时,git diff --quiet会判定无需运行,所有矩阵任务都跳过真实安装,从而绕过新增发布依赖的安装验证,违反发布范围变化必须触发依赖门禁的契约。变更检测应覆盖该发布范围文件。