mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
* fix(resource): select free-threaded extension ABI * chore(deps): require moviepilot-rust 0.2.9 * perf: add free-threaded runtime comparison * feat: add free-threaded runtime profile * chore(deps): require moviepilot-rust 0.3.0 * test: isolate system endpoint import graph * fix(docker): preserve runtime profile during recovery * feat(runtime): expose active GIL state * feat(plugin): log GIL fallback attribution * test: refresh runtime observability dependency baseline * fix(plugin): validate the active uv runtime profile * test(runtime): expand free-threaded benchmark evidence * docs(runtime): define v3t governance gates * test(runtime): separate rust benchmark modes * docs: sync free-threaded architecture baseline * perf: add PostgreSQL driver comparison * docs(runtime): record final free-threaded evidence * fix(runtime): converge dual-profile dependency verification * fix(runtime): scope Python 3.14 warning filter * fix(runtime): match actual oss2 syntax warning * docs(runtime): refresh free-threaded benchmark evidence * test(architecture): refresh runtime dependency baseline * ci: skip unused Trivy Java database * build: exclude local verification artifacts * docs(runtime): refresh PostgreSQL driver benchmarks * docs(runtime): record plugin restore acceptance * docs(runtime): record amd64 candidate acceptance * ci: pin beta image publisher action * test(architecture): merge runtime dependency baseline * feat(runtime): expose Python GIL status * docs(runtime): document Python runtime status fields
148 lines
5.5 KiB
Python
148 lines
5.5 KiB
Python
"""Docker 构建输入和镜像载荷分层合同。"""
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
DOCKERFILE = ROOT / "docker" / "Dockerfile"
|
|
RELEASE_WORKFLOW = ROOT / ".github" / "workflows" / "build-v3.yml"
|
|
BETA_WORKFLOW = ROOT / ".github" / "workflows" / "beta.yml"
|
|
|
|
|
|
def _read(path: Path) -> str:
|
|
"""读取构建合同文件。"""
|
|
return path.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_build_context_excludes_runtime_state_and_keeps_release_inputs() -> None:
|
|
"""本地运行数据不得进入构建上下文,发布所需入口必须保留。"""
|
|
dockerignore = _read(ROOT / ".dockerignore")
|
|
|
|
for pattern in (
|
|
".venv/",
|
|
".worktrees/",
|
|
".artifacts/",
|
|
".build/",
|
|
".agent-work/",
|
|
".runtime/",
|
|
".tmp/",
|
|
".cache/",
|
|
"node_modules/",
|
|
"public/",
|
|
".moviepilot.env",
|
|
".env",
|
|
".env.*",
|
|
"**/*.pyc",
|
|
"**/__pycache__/",
|
|
"**/*.pyd",
|
|
"**/*.so",
|
|
"coverage.json",
|
|
"coverage.xml",
|
|
"config/*",
|
|
"app/plugins/**",
|
|
"app/application/site/*.bin",
|
|
):
|
|
assert pattern in dockerignore
|
|
|
|
assert "!config/category.yaml" in dockerignore
|
|
assert "!app/plugins/__init__.py" in dockerignore
|
|
assert "frontend-dist" not in dockerignore
|
|
|
|
|
|
def test_dockerfile_assigns_each_payload_to_an_independent_stage() -> None:
|
|
"""高频载荷必须由独立 stage 生成,并在 final 中分别写入目标目录。"""
|
|
dockerfile = _read(DOCKERFILE)
|
|
|
|
for stage in (
|
|
"prepare_backend",
|
|
"prepare_frontend",
|
|
"prepare_plugins",
|
|
"prepare_resources",
|
|
"prepare_control",
|
|
):
|
|
assert f" AS {stage}" in dockerfile
|
|
|
|
final_start = dockerfile.index("FROM prepare_package AS final")
|
|
final = dockerfile[final_start:]
|
|
copies = (
|
|
"COPY --link --from=prepare_frontend /public /public",
|
|
"COPY --link --from=prepare_plugins /plugins /app/app/plugins",
|
|
"COPY --link --from=prepare_resources /resources /app/app/application/site",
|
|
"COPY --link --from=prepare_backend /app /app",
|
|
)
|
|
positions = [final.index(copy) for copy in copies]
|
|
|
|
assert positions == sorted(positions)
|
|
for copy in copies:
|
|
assert final.count(copy) == 1
|
|
assert "COPY --from=prepare_control /bundle/entrypoint.sh /entrypoint.sh" in final
|
|
assert "RUN rm -rf /app/frontend-dist" in dockerfile
|
|
|
|
|
|
def test_plugin_runtime_updates_preserve_legacy_base_entrypoint() -> None:
|
|
"""更新和性能覆盖镜像必须保留旧插件导入 _PluginBase 所需的兼容入口。"""
|
|
update_script = _read(ROOT / "docker" / "update.sh")
|
|
perf_script = _read(ROOT / "scripts" / "perf" / "moviepilot_docker_ab.py")
|
|
|
|
assert 'rm -f "${stage_plugin_dir}/__init__.py"' not in update_script
|
|
assert "rm -f /frozen/plugins/__init__.py" not in perf_script
|
|
|
|
|
|
def test_release_workflows_pin_and_record_external_payload_identities() -> None:
|
|
"""正式与 Beta 构建都必须以真实制品身份驱动缓存并写入镜像标签。"""
|
|
for workflow_path in (RELEASE_WORKFLOW, BETA_WORKFLOW):
|
|
workflow = _read(workflow_path)
|
|
|
|
for build_arg in (
|
|
"MOVIEPILOT_FRONTEND_VERSION=",
|
|
"MOVIEPILOT_FRONTEND_SHA256=",
|
|
"MOVIEPILOT_PLUGINS_REF=",
|
|
"MOVIEPILOT_RESOURCES_REF=",
|
|
):
|
|
assert build_arg in workflow
|
|
|
|
for label in (
|
|
"org.moviepilot.source-revision=",
|
|
"org.moviepilot.frontend-version=",
|
|
"org.moviepilot.frontend-digest=",
|
|
"org.moviepilot.plugins-revision=",
|
|
"org.moviepilot.resources-revision=",
|
|
"org.moviepilot.plugin-market-wiki-revision=",
|
|
"org.moviepilot.models-catalog-digest=",
|
|
):
|
|
assert label in workflow
|
|
|
|
assert "git ls-remote https://github.com/jxxghp/MoviePilot-Plugins.git" in workflow
|
|
assert "git ls-remote https://github.com/jxxghp/MoviePilot-Resources.git" in workflow
|
|
assert "sha256:*) frontend_sha256=" in workflow
|
|
assert "^[0-9a-f]{40}$" in workflow
|
|
assert "^[0-9a-f]{64}$" in workflow
|
|
|
|
|
|
def test_same_version_rebuild_identity_is_not_derived_from_image_tag() -> None:
|
|
"""重复发布同一版本时,缓存身份必须来自源码和制品而不是镜像 Tag。"""
|
|
release_workflow = _read(RELEASE_WORKFLOW)
|
|
|
|
assert "SOURCE_COMMIT=$(git rev-parse HEAD)" in release_workflow
|
|
assert "org.moviepilot.source-revision=${{ env.SOURCE_COMMIT }}" in release_workflow
|
|
assert (
|
|
"org.moviepilot.release-snapshot-revision="
|
|
"${{ steps.release_snapshot.outputs.release_commit }}" in release_workflow
|
|
)
|
|
assert "type=raw,value=${{ env.app_version }}" in release_workflow
|
|
assert "MOVIEPILOT_PLUGINS_REF=${{ steps.payloads.outputs.plugins_revision }}" in release_workflow
|
|
assert "MOVIEPILOT_RESOURCES_REF=${{ steps.payloads.outputs.resources_revision }}" in release_workflow
|
|
|
|
|
|
def test_custom_frontend_directory_is_stable_but_artifacts_remain_untracked() -> None:
|
|
"""干净 checkout 可直接构建,自定义前端产物仍保持本地状态。"""
|
|
gitignore = _read(ROOT / ".gitignore")
|
|
dockerfile = _read(DOCKERFILE)
|
|
|
|
assert (ROOT / "frontend-dist" / ".gitkeep").is_file()
|
|
assert "frontend-dist/*" in gitignore
|
|
assert "!frontend-dist/.gitkeep" in gitignore
|
|
assert ".artifacts/" in gitignore
|
|
assert "COPY frontend-dist/ /tmp/frontend-dist/" in dockerfile
|
|
assert "! -name '.gitkeep'" in dockerfile
|