mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 15:38:19 +08:00
fix(ci): isolate plugin market snapshot generator
This commit is contained in:
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user