fix(ci): isolate plugin market snapshot generator

This commit is contained in:
jxxghp
2026-08-14 21:29:15 +08:00
parent d6bf78b63a
commit 794bcfdfeb
4 changed files with 122 additions and 14 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
run: | run: |
python3 -m scripts.generate_plugin_market_default \ python3 -m scripts.generate_plugin_market_default \
--wiki-file .build/moviepilot-wiki/plugin.md \ --wiki-file .build/moviepilot-wiki/plugin.md \
--config-file app/core/config.py --config-file app/runtime/config.py
wiki_commit=$(git -C .build/moviepilot-wiki rev-parse HEAD) wiki_commit=$(git -C .build/moviepilot-wiki rev-parse HEAD)
echo "wiki_commit=$wiki_commit" >> "$GITHUB_OUTPUT" echo "wiki_commit=$wiki_commit" >> "$GITHUB_OUTPUT"
+2 -2
View File
@@ -44,7 +44,7 @@ jobs:
run: | run: |
python3 -m scripts.generate_plugin_market_default \ python3 -m scripts.generate_plugin_market_default \
--wiki-file .build/moviepilot-wiki/plugin.md \ --wiki-file .build/moviepilot-wiki/plugin.md \
--config-file app/core/config.py --config-file app/runtime/config.py
wiki_commit=$(git -C .build/moviepilot-wiki rev-parse HEAD) wiki_commit=$(git -C .build/moviepilot-wiki rev-parse HEAD)
echo "wiki_commit=$wiki_commit" >> "$GITHUB_OUTPUT" echo "wiki_commit=$wiki_commit" >> "$GITHUB_OUTPUT"
@@ -55,7 +55,7 @@ jobs:
run: | run: |
git config user.name "github-actions[bot]" git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add app/core/config.py git add app/runtime/config.py
if ! git diff --cached --quiet; then if ! git diff --cached --quiet; then
git commit -m "build(plugin-market): sync default from MoviePilot-Wiki@${WIKI_COMMIT:0:12}" git commit -m "build(plugin-market): sync default from MoviePilot-Wiki@${WIKI_COMMIT:0:12}"
fi fi
+53 -3
View File
@@ -4,23 +4,71 @@
import argparse import argparse
import ast import ast
import re
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from urllib.parse import urlparse
from app.adapters.external.market import extract_plugin_market_repos_from_wiki
OFFICIAL_PLUGIN_MARKET = "https://github.com/jxxghp/MoviePilot-Plugins" OFFICIAL_PLUGIN_MARKET = "https://github.com/jxxghp/MoviePilot-Plugins"
PLUGIN_MARKET_WIKI_START = "<!-- plugin-market-repos:start -->"
PLUGIN_MARKET_WIKI_END = "<!-- plugin-market-repos:end -->"
PLUGIN_MARKET_REPO_PATTERN = re.compile(
r"https?://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:\.git)?/?",
re.IGNORECASE,
)
def _parse_args(args: Optional[list[str]] = None) -> argparse.Namespace: def _parse_args(args: Optional[list[str]] = None) -> argparse.Namespace:
"""解析独立构建脚本需要的 Wiki 和配置文件路径。"""
parser = argparse.ArgumentParser(description="生成插件市场发版默认值") parser = argparse.ArgumentParser(description="生成插件市场发版默认值")
parser.add_argument("--wiki-file", type=Path, required=True) parser.add_argument("--wiki-file", type=Path, required=True)
parser.add_argument("--config-file", type=Path, required=True) parser.add_argument("--config-file", type=Path, required=True)
return parser.parse_args(args) return parser.parse_args(args)
def _normalize_plugin_market_repo_url(repo_url: str) -> Optional[str]:
"""规范化 GitHub 插件仓库地址,供清单解析阶段去重。"""
repo_url = (repo_url or "").strip().rstrip("/").removesuffix(".git")
if not repo_url:
return None
parsed_url = urlparse(repo_url)
if parsed_url.scheme not in {"http", "https"}:
return None
if (parsed_url.hostname or "").lower() != "github.com":
return None
paths = [item for item in parsed_url.path.split("/") if item]
if len(paths) < 2:
return None
return f"https://github.com/{paths[0]}/{paths[1]}"
def _extract_plugin_market_repos_from_wiki(markdown: str) -> list[str]:
"""从唯一且有序的 Wiki 标记区域读取插件仓库清单。"""
start_count = markdown.count(PLUGIN_MARKET_WIKI_START)
end_count = markdown.count(PLUGIN_MARKET_WIKI_END)
start_index = markdown.find(PLUGIN_MARKET_WIKI_START)
end_index = markdown.find(PLUGIN_MARKET_WIKI_END)
if start_count != 1 or end_count != 1 or start_index >= end_index:
raise ValueError("Wiki 插件仓库清单必须包含唯一且有序的开始和结束标记")
content = markdown[
start_index + len(PLUGIN_MARKET_WIKI_START):end_index
]
repos: list[str] = []
seen_repos: set[str] = set()
for item in PLUGIN_MARKET_REPO_PATTERN.findall(content):
normalized_repo = _normalize_plugin_market_repo_url(item)
identity = normalized_repo.lower() if normalized_repo else ""
if not normalized_repo or identity in seen_repos:
continue
repos.append(normalized_repo)
seen_repos.add(identity)
return repos
def _find_plugin_market_assignment(source: str) -> tuple[int, int, str]: def _find_plugin_market_assignment(source: str) -> tuple[int, int, str]:
"""定位 ConfigModel 中插件市场默认值对应的源码范围。"""
tree = ast.parse(source) tree = ast.parse(source)
source_lines = source.splitlines(keepends=True) source_lines = source.splitlines(keepends=True)
for node in tree.body: for node in tree.body:
@@ -41,6 +89,7 @@ def _find_plugin_market_assignment(source: str) -> tuple[int, int, str]:
def _format_plugin_market_assignment(repos: list[str], indent: str) -> str: def _format_plugin_market_assignment(repos: list[str], indent: str) -> str:
"""按配置文件现有缩进格式生成插件市场默认值源码。"""
lines = [f"{indent}PLUGIN_MARKET: str = (\n"] lines = [f"{indent}PLUGIN_MARKET: str = (\n"]
for index, repo in enumerate(repos): for index, repo in enumerate(repos):
suffix = "," if index < len(repos) - 1 else "" suffix = "," if index < len(repos) - 1 else ""
@@ -50,8 +99,9 @@ def _format_plugin_market_assignment(repos: list[str], indent: str) -> str:
def _generate_plugin_market_default(wiki_file: Path, config_file: Path) -> list[str]: def _generate_plugin_market_default(wiki_file: Path, config_file: Path) -> list[str]:
"""读取 Wiki 清单并原地更新配置文件中的插件市场默认值。"""
markdown = wiki_file.read_text(encoding="utf-8") markdown = wiki_file.read_text(encoding="utf-8")
repos = extract_plugin_market_repos_from_wiki(markdown, require_markers=True) repos = _extract_plugin_market_repos_from_wiki(markdown)
if not repos: if not repos:
raise ValueError("Wiki 插件仓库清单为空") raise ValueError("Wiki 插件仓库清单为空")
if OFFICIAL_PLUGIN_MARKET not in repos: if OFFICIAL_PLUGIN_MARKET not in repos:
+66 -8
View File
@@ -1,15 +1,20 @@
import ast import ast
import subprocess
import sys
from pathlib import Path from pathlib import Path
import pytest import pytest
from app.adapters.external.market import extract_plugin_market_repos_from_wiki
from scripts.generate_plugin_market_default import ( from scripts.generate_plugin_market_default import (
OFFICIAL_PLUGIN_MARKET, OFFICIAL_PLUGIN_MARKET,
_extract_plugin_market_repos_from_wiki,
_generate_plugin_market_default, _generate_plugin_market_default,
) )
ROOT_DIR = Path(__file__).resolve().parents[1]
def _read_plugin_market_default(config_file: Path) -> str: def _read_plugin_market_default(config_file: Path) -> str:
tree = ast.parse(config_file.read_text(encoding="utf-8")) tree = ast.parse(config_file.read_text(encoding="utf-8"))
for node in tree.body: for node in tree.body:
@@ -37,9 +42,7 @@ def test_extract_plugin_market_repos_uses_marked_section_and_deduplicates() -> N
- https://github.com/outside/ignored-again - https://github.com/outside/ignored-again
""" """
assert extract_plugin_market_repos_from_wiki( assert _extract_plugin_market_repos_from_wiki(markdown) == [
markdown, require_markers=True
) == [
OFFICIAL_PLUGIN_MARKET, OFFICIAL_PLUGIN_MARKET,
"https://github.com/demo/Market", "https://github.com/demo/Market",
] ]
@@ -50,9 +53,7 @@ def test_extract_plugin_market_repos_requires_unique_markers_for_build() -> None
构建模式拒绝缺失或重复边界标记的 Wiki 文档。 构建模式拒绝缺失或重复边界标记的 Wiki 文档。
""" """
with pytest.raises(ValueError, match="唯一且有序的开始和结束标记"): with pytest.raises(ValueError, match="唯一且有序的开始和结束标记"):
extract_plugin_market_repos_from_wiki( _extract_plugin_market_repos_from_wiki(f"- {OFFICIAL_PLUGIN_MARKET}")
f"- {OFFICIAL_PLUGIN_MARKET}", require_markers=True
)
markdown = f""" markdown = f"""
<!-- plugin-market-repos:start --> <!-- plugin-market-repos:start -->
@@ -61,7 +62,7 @@ def test_extract_plugin_market_repos_requires_unique_markers_for_build() -> None
<!-- plugin-market-repos:end --> <!-- plugin-market-repos:end -->
""" """
with pytest.raises(ValueError, match="唯一且有序的开始和结束标记"): with pytest.raises(ValueError, match="唯一且有序的开始和结束标记"):
extract_plugin_market_repos_from_wiki(markdown, require_markers=True) _extract_plugin_market_repos_from_wiki(markdown)
def test_generate_plugin_market_default_updates_assignment_idempotently( def test_generate_plugin_market_default_updates_assignment_idempotently(
@@ -127,3 +128,60 @@ def test_generate_plugin_market_default_requires_official_repo(
with pytest.raises(ValueError, match="缺少 MoviePilot 官方插件仓库"): with pytest.raises(ValueError, match="缺少 MoviePilot 官方插件仓库"):
_generate_plugin_market_default(wiki_file, config_file) _generate_plugin_market_default(wiki_file, config_file)
def test_generate_plugin_market_default_runs_without_project_dependencies(
tmp_path: Path,
) -> None:
"""发版脚本在禁用 site-packages 时仍应能独立生成默认配置。"""
wiki_file = tmp_path / "plugin.md"
wiki_file.write_text(
f"""
<!-- plugin-market-repos:start -->
- {OFFICIAL_PLUGIN_MARKET}
<!-- plugin-market-repos:end -->
""",
encoding="utf-8",
)
config_file = tmp_path / "config.py"
config_file.write_text(
'class ConfigModel:\n PLUGIN_MARKET: str = "old"\n',
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
"-S",
"-m",
"scripts.generate_plugin_market_default",
"--wiki-file",
str(wiki_file),
"--config-file",
str(config_file),
],
cwd=ROOT_DIR,
capture_output=True,
text=True,
timeout=30,
check=False,
)
assert result.returncode == 0, result.stderr
assert _read_plugin_market_default(config_file) == OFFICIAL_PLUGIN_MARKET
def test_release_workflows_use_canonical_config_path() -> None:
"""正式版和 Beta 发版流程必须修改并暂存 canonical 配置文件。"""
build_workflow = (
ROOT_DIR / ".github" / "workflows" / "build-v3.yml"
).read_text(encoding="utf-8")
beta_workflow = (
ROOT_DIR / ".github" / "workflows" / "beta.yml"
).read_text(encoding="utf-8")
assert "--config-file app/runtime/config.py" in build_workflow
assert "git add app/runtime/config.py" in build_workflow
assert "--config-file app/runtime/config.py" in beta_workflow
assert "app/core/config.py" not in build_workflow
assert "app/core/config.py" not in beta_workflow