mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-28 19:06:36 +08:00
fix(ci): align release image tags and coverage gates
* ci: defer coverage from pull requests * ci: parallelize coverage shards * fix(ci): restore atomic latest image promotion * docs: align coverage gate description * fix(ci): align release image tags with upstream * fix(ci): sanitize coverage artifact names * fix(ci): refresh coverage ratchet baseline
This commit is contained in:
78
.github/workflows/test.yml
vendored
78
.github/workflows/test.yml
vendored
@@ -123,10 +123,30 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
run: uv run --locked --no-sync python tests/run.py --shard "${{ matrix.shard }}"
|
||||
|
||||
coverage:
|
||||
coverage-shard:
|
||||
runs-on: ubuntu-latest
|
||||
name: Coverage Report
|
||||
timeout-minutes: 20
|
||||
name: Coverage Shard (${{ matrix.shard }})
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- shard: '1/8'
|
||||
index: '1'
|
||||
- shard: '2/8'
|
||||
index: '2'
|
||||
- shard: '3/8'
|
||||
index: '3'
|
||||
- shard: '4/8'
|
||||
index: '4'
|
||||
- shard: '5/8'
|
||||
index: '5'
|
||||
- shard: '6/8'
|
||||
index: '6'
|
||||
- shard: '7/8'
|
||||
index: '7'
|
||||
- shard: '8/8'
|
||||
index: '8'
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -144,14 +164,55 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: uv sync --locked
|
||||
|
||||
- name: Generate coverage reports
|
||||
timeout-minutes: 15
|
||||
- name: Generate coverage data
|
||||
timeout-minutes: 10
|
||||
run: uv run --locked --no-sync python -m coverage run --parallel-mode tests/run.py --shard "${{ matrix.shard }}"
|
||||
|
||||
- name: Upload coverage data
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: coverage-data-${{ matrix.index }}
|
||||
path: .coverage.*
|
||||
include-hidden-files: true
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
coverage-report:
|
||||
runs-on: ubuntu-latest
|
||||
name: Coverage Report
|
||||
needs: coverage-shard
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v10.0.1
|
||||
with:
|
||||
python-version: '3.14'
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
pyproject.toml
|
||||
uv.lock
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --locked
|
||||
|
||||
- name: Download coverage data
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: coverage-data-*
|
||||
path: coverage-data
|
||||
merge-multiple: true
|
||||
|
||||
- name: Combine coverage data and check ratchet
|
||||
run: |
|
||||
uv run --locked --no-sync python -m coverage erase
|
||||
uv run --locked --no-sync python -m coverage run tests/run.py --serial
|
||||
uv run --locked --no-sync python -m coverage combine coverage-data
|
||||
uv run --locked --no-sync python -m coverage report
|
||||
uv run --locked --no-sync python -m coverage json
|
||||
uv run --locked --no-sync python -m coverage xml
|
||||
uv run --locked --no-sync python scripts/architecture/coverage_ratchet.py
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -161,6 +222,3 @@ jobs:
|
||||
coverage.xml
|
||||
coverage.json
|
||||
retention-days: 7
|
||||
|
||||
- name: Check coverage ratchet
|
||||
run: uv run --locked --no-sync python scripts/architecture/coverage_ratchet.py
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
| RES-002 | `DELIVERED`(R2) | 复杂度门禁漏扫私有方法、类/文件和 Scheduler;原生并发清单不完整 | `f7ca7e517`、`5cd5780d3`:完整宿主 AST、canonical owner/count、低水位与零增长门禁已落地 |
|
||||
| RES-003 | `DELIVERED`(R3) | Outbox after_commit 失败只有内存 pending 标记,重启不可恢复 | `9d06f91bb`:持久 intent、唯一 handler、claim/fencing、重启回放、幂等和失败观测完整闭环 |
|
||||
| RES-004 | `DELIVERED`(R4) | Startup initializer 与插件市场存在多套 Transport/Adapter/Manager 构造 | `7f5b8b469` 至 `046b0b305`:构造回收到 composition,兼容门面消费同一 owner,canonical 无重复正式实现 |
|
||||
| RES-005 | `DELIVERED`(R5) | 审计声明、机器门禁、CI 与远端交付状态需要重新校准 | 当前文档、生成 fixture 与规则一致;锁定全量 `7684 passed, 9 skipped`,Application/Domain 覆盖率为 `81.80%` / `81.03%`,Pylint `10.00/10`、架构/兼容、真实启动和最终 exact-head GitHub CI 闭环,远端 `0/0` |
|
||||
| RES-005 | `DELIVERED`(R5) | 审计声明、机器门禁、CI 与远端交付状态需要重新校准 | 当前文档、生成 fixture 与规则一致;锁定全量 `7684 passed, 9 skipped`,Application/Domain 覆盖率为 `81.86%` / `81.03%`,Pylint `10.00/10`、架构/兼容、真实启动和最终 exact-head GitHub CI 闭环,远端 `0/0` |
|
||||
R2 门禁现已完整覆盖私有、dunder、任意控制流嵌套方法、类、文件与
|
||||
`app/scheduler/`;`concurrency.py` 扫描完整宿主源码,按 canonical import/alias、
|
||||
TaskGroup、可证明的 loop/executor 来源和词法 owner 聚合数量。新增 owner、数量增长以及
|
||||
@@ -101,9 +101,9 @@ ARCH-201 至 ARCH-204 均达到实现、验证、提交、推送和远端门禁
|
||||
| Event Contract | 53 | 均已有 payload model,但当前全部是 diagnostic enforcement |
|
||||
| Python 源码量 | 305,884 行 | 排除 `app/plugins/**`;61 个文件超过 1,000 行,11 个超过 2,000 行 |
|
||||
| 长方法 | 290 个超过 80 行 | AST 统计排除 `app/plugins/**`;65 个超过 150 行,21 个超过 250 行 |
|
||||
| 全量 mypy 历史债务 | 9,986 / 591 文件 | canonical `SearchChain` Facade 已补齐显式类型转发;strict frontier 当前覆盖 41 个文件,低水位只允许继续下降 |
|
||||
| 全量 mypy 历史债务 | 9,983 / 591 文件 | canonical `SearchChain` Facade 已补齐显式类型转发;strict frontier 当前覆盖 41 个文件,低水位只允许继续下降 |
|
||||
| Ruff 历史诊断 | 630 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` |
|
||||
| 覆盖率低水位 | Application 81.80%,Domain 81.03% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 |
|
||||
| 覆盖率低水位 | Application 81.86%,Domain 81.03% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 |
|
||||
|
||||
### 3.3 热点文件
|
||||
|
||||
@@ -805,9 +805,11 @@ Pylint 10/10、Ruff、mypy/复杂度 ratchet、宿主与最新官方插件基线
|
||||
# 广泛变更的最终本地回归
|
||||
uv run --locked --no-sync python tests/run.py
|
||||
|
||||
# 需要覆盖率证据时按 CI 串行生成真实报告,再检查低水位
|
||||
uv run --locked --no-sync python -m coverage erase
|
||||
uv run --locked --no-sync python -m coverage run tests/run.py --serial
|
||||
# 需要覆盖率证据时按 CI 的 8 个分片采集并合并,再检查低水位
|
||||
for shard in 1/8 2/8 3/8 4/8 5/8 6/8 7/8 8/8; do
|
||||
uv run --locked --no-sync python -m coverage run --parallel-mode tests/run.py --shard "$shard"
|
||||
done
|
||||
uv run --locked --no-sync python -m coverage combine
|
||||
uv run --locked --no-sync python -m coverage json
|
||||
uv run --locked --no-sync python scripts/architecture/coverage_ratchet.py
|
||||
|
||||
|
||||
@@ -199,10 +199,11 @@ uvx --from pip-audit pip-audit \
|
||||
`python tests/run.py` 在本地默认把排序后的测试文件按向上取整的连续区间切成 4 片,
|
||||
并启动 4 个独立 pytest 进程;GitHub Actions 使用同一入口的 `--shard N/TOTAL`
|
||||
参数启动对应分片。需要单进程调试时使用 `python tests/run.py --serial`。Coverage job
|
||||
会在 `v3` 的 PR / push 中串行运行同一全量入口,并检查 Application 与 Domain 的
|
||||
已提交低水位;它不是只在手工触发时运行的建议性报告。该 job 总预算为 20 分钟,
|
||||
其中全量测试及报告生成 step 预算为 15 分钟,用于容纳 Ubuntu Runner 的性能波动,
|
||||
不得通过跳过测试文件或覆盖率产物规避超时。
|
||||
会在 `v3` 的 PR / push 中将同一全量入口切成 8 个并行分片,分别上传覆盖率数据,再由
|
||||
单一报告 job 合并并检查 Application 与 Domain 的已提交低水位;它不是只在手工触发时
|
||||
运行的建议性报告。每个 Coverage 分片预算为 15 分钟(其中测试 step 为 10 分钟),
|
||||
报告合并与 ratchet 预算为 10 分钟,用于容纳 Ubuntu Runner 的性能波动,不得通过跳过
|
||||
测试文件或覆盖率产物规避超时。
|
||||
|
||||
4. **运行架构与静态门禁**:主仓架构检查不依赖独立插件仓;官方插件兼容观察单独运行,
|
||||
任何检查命令都不会写入 fixture。
|
||||
|
||||
@@ -145,9 +145,9 @@ def test_recognize_prefers_explicit_identity(sample_meta, monkeypatch):
|
||||
|
||||
## CI 与 PR
|
||||
|
||||
- **门禁**:`.github/workflows/test.yml` 在指向 `v3` 的 `pull_request` / `push` 及手动触发时,从 `uv.lock` 同步环境。独立 `architecture` job 先运行宿主依赖、运行契约和基线 CLI 快速门禁;全量测试再通过 `tests/run.py --shard N/TOTAL` 稳定分到 4 个 pytest job。每个分片都有独立进程和临时 `CONFIG_DIR`,不共用 SQLite 或进程级状态。
|
||||
- **门禁**:`.github/workflows/test.yml` 在指向 `v3` 的 `pull_request` / `push` 及手动触发时,从 `uv.lock` 同步环境。独立 `architecture` job 先运行宿主依赖、运行契约和基线 CLI 快速门禁;全量测试再通过 `tests/run.py --shard N/TOTAL` 稳定分到 4 个 pytest job。每个分片都有独立进程和临时 `CONFIG_DIR`,不共用 SQLite 或进程级状态。Coverage 另以 8 个并行分片采集数据,由单一报告 job 合并后检查低水位。
|
||||
- **跨仓观察**:`.github/workflows/architecture-observe.yml` 每周或手工检出官方插件仓最新 `main`,使用 `--check-plugins` 比较公开导入、Hook 和动态 API 契约。它只上传 `official-plugin-architecture-report.json`,不会自动刷新 fixture;语义变化必须人工审查后显式执行 `--write-plugins`。
|
||||
- **静态检查**:`.github/workflows/pylint.yml` 对指向 `v3` 的 PR、推送和手工触发运行 Pylint。PR/推送改动到的 Python 文件是硬门禁;`app/` 全量扫描保留为建议性 JSON 构建工件,存量告警不会掩盖或阻塞本次增量治理。
|
||||
- **PR 本地验证**:提交前运行受影响测试和适用的静态检查。涉及依赖或锁文件、共享测试基建、数据库、启动链、跨模块生命周期、兼容层或大范围行为变化时,运行 `uv run --locked --no-sync python tests/run.py` 完成本地全量;需要断点、输出顺序或测试污染诊断时使用 `--serial`。所有测试都应确认受影响路径通过且 socket 探针无真实出站,验证说明准确标注执行范围。若存在无关失败,必须在当前 `upstream/v3` 基线上独立复现并在 PR 中如实说明;不得静默扩大当前 PR 去修复基线问题。纯文档变更执行适用的文本、结构和 diff 检查,CI 继续运行全量门禁。
|
||||
- **覆盖率门禁**:`Coverage Report` job 会在 `v3` 的 PR、push 和手工触发中通过 `tests/run.py --serial` 跑串行全量,先上传 JSON / XML 工件,再只读检查 Application 与 Domain 的 Ubuntu/Python 3.14 canonical 低水位。覆盖率下降会阻塞,提升或等比例快照变化也必须显式刷新 fixture;macOS 本地报告只用于诊断,不直接作为可提交基线。
|
||||
- **覆盖率门禁**:`Coverage Shard` jobs 会在 `v3` 的 PR、push 和手工触发中通过 `tests/run.py --shard N/8` 并行采集覆盖率数据,`Coverage Report` 再合并全部分片并只读检查 Application 与 Domain 的 Ubuntu/Python 3.14 canonical 低水位,同时上传 JSON / XML 工件。覆盖率下降会阻塞,提升或等比例快照变化也必须显式刷新 fixture;macOS 本地报告只用于诊断,不直接作为可提交基线。
|
||||
- 复现 CI 使用 `uv sync --locked`;主程序运行依赖位于 `[project].dependencies`,pytest 与覆盖率工具位于默认 `dev` 依赖组。
|
||||
|
||||
@@ -13,8 +13,9 @@ MoviePilot 同一版本提供两套镜像:
|
||||
|
||||
两套镜像必须来自相同源码 revision、使用相同产品版本和同一份 `uv.lock`。V3t 不替换默认镜像,
|
||||
也不在业务代码中维护一套平行实现;解释器、原生 ABI 或插件不兼容时,应切回同版本标准 V3。
|
||||
正式发布按同版本制品对验收和晋升 `latest`;任一变体未通过构建、扫描或发布时,本次版本不移动
|
||||
两边的 `latest`。这不会回退已经发布的标准 V3,且避免两个 `latest` 指向不同源码版本。
|
||||
正式发布为两个变体分别生成同一版本号和 `latest` 标签;两个发布步骤都必须在各自构建、扫描通过后
|
||||
执行。版本号标签保留用于回溯和固定部署,`latest` 直接由对应的多架构发布步骤写入,不再额外运行
|
||||
标签晋升步骤。两套镜像仍必须使用同一源码 revision,避免同一发布批次出现来源不一致。
|
||||
|
||||
V3t 的目标不是让所有请求都更快。它主要改善可并行的 Python CPU 热点,同时验证主程序、原生扩展
|
||||
和插件生态在 free-threaded 解释器下的正确性。启动、内存、普通 API 和数据库路径不得为获得局部
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"application": {
|
||||
"covered_lines": 12969,
|
||||
"percent": 81.8,
|
||||
"statements": 15854
|
||||
"covered_lines": 12996,
|
||||
"percent": 81.86,
|
||||
"statements": 15875
|
||||
},
|
||||
"domain": {
|
||||
"covered_lines": 3686,
|
||||
|
||||
@@ -83,52 +83,86 @@ def test_official_plugin_observation_is_scheduled_and_never_writes_fixture():
|
||||
)
|
||||
|
||||
|
||||
def test_coverage_job_runs_full_suite_and_read_only_ratchet() -> None:
|
||||
"""PR 与推送的 Coverage job 必须串行采集全量报告并只读检查低水位。"""
|
||||
def test_coverage_jobs_parallelize_data_and_keep_one_global_ratchet() -> None:
|
||||
"""Coverage 分片并行采集数据,再由单一报告 job 合并并检查全局低水位。"""
|
||||
workflow = _load_workflow("test.yml")
|
||||
coverage_job = workflow["jobs"]["coverage"]
|
||||
steps = coverage_job["steps"]
|
||||
shard_job = workflow["jobs"]["coverage-shard"]
|
||||
shard_steps = shard_job["steps"]
|
||||
report_job = workflow["jobs"]["coverage-report"]
|
||||
report_steps = report_job["steps"]
|
||||
assert workflow["on"]["pull_request"]["branches"] == ["v3"]
|
||||
assert workflow["on"]["push"]["branches"] == ["v3"]
|
||||
assert "workflow_dispatch" in workflow["on"]
|
||||
assert coverage_job["runs-on"] == "ubuntu-latest"
|
||||
assert coverage_job["timeout-minutes"] == 20
|
||||
assert "if" not in coverage_job
|
||||
assert shard_job["runs-on"] == "ubuntu-latest"
|
||||
assert shard_job["timeout-minutes"] == 15
|
||||
shard_matrix = shard_job["strategy"]["matrix"]["include"]
|
||||
assert [item["shard"] for item in shard_matrix] == [
|
||||
"1/8",
|
||||
"2/8",
|
||||
"3/8",
|
||||
"4/8",
|
||||
"5/8",
|
||||
"6/8",
|
||||
"7/8",
|
||||
"8/8",
|
||||
]
|
||||
assert [item["index"] for item in shard_matrix] == [str(index) for index in range(1, 9)]
|
||||
assert report_job["needs"] == "coverage-shard"
|
||||
assert report_job["runs-on"] == "ubuntu-latest"
|
||||
assert report_job["timeout-minutes"] == 10
|
||||
|
||||
setup_step = next(step for step in steps if step.get("name") == "Set up uv")
|
||||
setup_step = next(step for step in shard_steps if step.get("name") == "Set up uv")
|
||||
install_step = next(
|
||||
step for step in steps if step.get("name") == "Install dependencies"
|
||||
step for step in shard_steps if step.get("name") == "Install dependencies"
|
||||
)
|
||||
generate_step = next(
|
||||
step for step in steps if step.get("name") == "Generate coverage reports"
|
||||
step for step in shard_steps if step.get("name") == "Generate coverage data"
|
||||
)
|
||||
assert generate_step["timeout-minutes"] == 15
|
||||
upload_step = next(
|
||||
step for step in steps if step.get("name") == "Upload coverage report"
|
||||
assert generate_step["timeout-minutes"] == 10
|
||||
assert "coverage run --parallel-mode tests/run.py --shard" in generate_step["run"]
|
||||
upload_data_step = next(
|
||||
step for step in shard_steps if step.get("name") == "Upload coverage data"
|
||||
)
|
||||
ratchet_step = next(
|
||||
step for step in steps if step.get("name") == "Check coverage ratchet"
|
||||
assert upload_data_step["with"]["path"] == ".coverage.*"
|
||||
assert upload_data_step["with"]["name"] == "coverage-data-${{ matrix.index }}"
|
||||
assert upload_data_step["with"]["include-hidden-files"] is True
|
||||
|
||||
download_step = next(
|
||||
step for step in report_steps if step.get("name") == "Download coverage data"
|
||||
)
|
||||
assert download_step["uses"] == "actions/download-artifact@v8"
|
||||
assert download_step["with"]["merge-multiple"] is True
|
||||
report_step = next(
|
||||
step
|
||||
for step in report_steps
|
||||
if step.get("name") == "Combine coverage data and check ratchet"
|
||||
)
|
||||
assert setup_step["with"]["python-version"] == "3.14"
|
||||
assert install_step["run"] == "uv sync --locked"
|
||||
commands = generate_step["run"]
|
||||
commands = report_step["run"]
|
||||
expected_commands = [
|
||||
"python -m coverage erase",
|
||||
"python -m coverage run tests/run.py --serial",
|
||||
"python -m coverage combine coverage-data",
|
||||
"python -m coverage report",
|
||||
"python -m coverage json",
|
||||
"python -m coverage xml",
|
||||
"scripts/architecture/coverage_ratchet.py",
|
||||
]
|
||||
positions = [commands.index(command) for command in expected_commands]
|
||||
assert positions == sorted(positions)
|
||||
assert ratchet_step["run"].endswith("scripts/architecture/coverage_ratchet.py")
|
||||
assert steps.index(generate_step) < steps.index(upload_step) < steps.index(ratchet_step)
|
||||
|
||||
all_commands = _step_commands(workflow, "coverage")
|
||||
all_commands = _step_commands(workflow, "coverage-shard") + _step_commands(
|
||||
workflow, "coverage-report"
|
||||
)
|
||||
assert "--write" not in all_commands
|
||||
assert "|| true" not in all_commands
|
||||
assert all(step.get("continue-on-error") is not True for step in steps)
|
||||
assert all("always()" not in str(step.get("if", "")) for step in steps)
|
||||
assert all(
|
||||
step.get("continue-on-error") is not True
|
||||
for step in shard_steps + report_steps
|
||||
)
|
||||
assert all(
|
||||
"always()" not in str(step.get("if", ""))
|
||||
for step in shard_steps + report_steps
|
||||
)
|
||||
|
||||
coverage_config = configparser.ConfigParser()
|
||||
coverage_config.read(PROJECT_ROOT / ".coveragerc", encoding="utf-8")
|
||||
@@ -175,7 +209,10 @@ def test_upload_artifact_actions_share_node24_major():
|
||||
"native-dependency-update.yml": ["actions/upload-artifact@v7"],
|
||||
"pylint.yml": ["actions/upload-artifact@v7"],
|
||||
"site-adapter-collector.yml": ["actions/upload-artifact@v7"],
|
||||
"test.yml": ["actions/upload-artifact@v7"],
|
||||
"test.yml": [
|
||||
"actions/upload-artifact@v7",
|
||||
"actions/upload-artifact@v7",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -440,27 +440,28 @@ def test_release_publishes_free_threaded_image_with_separate_metadata_and_cache(
|
||||
assert "scope=moviepilot-v3t-docker-arm64" in publish["with"]["cache-from"]
|
||||
|
||||
|
||||
def test_release_promotes_latest_only_after_both_versioned_images() -> None:
|
||||
"""只有两个版本制品都发布成功后才可移动 latest 标签。"""
|
||||
def test_release_publishes_version_and_latest_tags_for_both_image_variants() -> None:
|
||||
"""正式版元数据同时发布版本号与 latest,两个变体直接复用各自发布结果。"""
|
||||
workflow = _load_workflow()
|
||||
steps = workflow["jobs"]["Docker-build"]["steps"]
|
||||
names = [step.get("name") for step in steps]
|
||||
indexed = _steps_by_name(workflow)
|
||||
|
||||
assert "value=latest" not in indexed["Docker Meta"]["with"]["tags"]
|
||||
assert "value=latest" not in indexed["Docker Meta free-threaded"]["with"]["tags"]
|
||||
assert names.index("Publish multi-architecture image") < names.index("Promote latest image pair")
|
||||
assert names.index("Publish free-threaded multi-architecture image") < names.index(
|
||||
"Promote latest image pair"
|
||||
for name in ("Docker Meta", "Docker Meta free-threaded"):
|
||||
tags = indexed[name]["with"]["tags"]
|
||||
assert "type=raw,value=${{ env.app_version }}" in tags
|
||||
assert "type=raw,value=latest" in tags
|
||||
|
||||
assert "Promote latest image pair" not in names
|
||||
assert names.index("Docker Meta") < names.index("Publish multi-architecture image")
|
||||
assert names.index("Docker Meta free-threaded") < names.index(
|
||||
"Publish free-threaded multi-architecture image"
|
||||
)
|
||||
promote = indexed["Promote latest image pair"]["run"]
|
||||
assert "moviepilot-v3:latest" not in promote
|
||||
assert 'ghcr_repository="${GITHUB_REPOSITORY,,}"' in promote
|
||||
assert "ghcr.io/${GITHUB_REPOSITORY}" not in promote
|
||||
assert "ghcr.io/${ghcr_repository}-v3" in promote
|
||||
assert "ghcr.io/${ghcr_repository}-v3t" in promote
|
||||
assert '"${image}:latest"' in promote
|
||||
assert '"${image}:${app_version}"' in promote
|
||||
|
||||
standard_images = indexed["Docker Meta"]["with"]["images"]
|
||||
assert "${{ secrets.DOCKER_USERNAME }}/moviepilot" in standard_images
|
||||
assert "${{ secrets.DOCKER_USERNAME }}/moviepilot-v3" in standard_images
|
||||
assert "ghcr.io/${{ github.repository }}" in standard_images
|
||||
|
||||
|
||||
def test_beta_applies_the_same_variant_scan_and_publish_contract() -> None:
|
||||
|
||||
@@ -146,10 +146,10 @@ def test_v3_release_workflows_use_main_wiki_and_isolated_images():
|
||||
assert "name: MoviePilot Builder v3" in build_workflow
|
||||
assert " - v3" in build_workflow
|
||||
assert " repository: jxxghp/MoviePilot-Wiki\n ref: main" in build_workflow
|
||||
assert "${{ secrets.DOCKER_USERNAME }}/moviepilot\n" in build_workflow
|
||||
assert "${{ secrets.DOCKER_USERNAME }}/moviepilot-v3" in build_workflow
|
||||
assert "ghcr.io/${{ github.repository }}-v3" in build_workflow
|
||||
assert "${{ secrets.DOCKER_USERNAME }}/moviepilot-v2" not in build_workflow
|
||||
assert "${{ secrets.DOCKER_USERNAME }}/moviepilot\n" not in build_workflow
|
||||
assert "git tag -l 'v3.*'" in build_workflow
|
||||
|
||||
assert " repository: jxxghp/MoviePilot-Wiki\n ref: main" in beta_workflow
|
||||
|
||||
@@ -86,10 +86,10 @@ def test_invalid_shard_values_are_rejected(value: str) -> None:
|
||||
|
||||
|
||||
def test_workflow_uses_the_shared_runner_contract() -> None:
|
||||
"""CI 不得另行维护 shell 分片算法,覆盖率必须显式使用串行模式。"""
|
||||
"""CI 不得另行维护 shell 分片算法,Coverage 必须复用同一分片入口。"""
|
||||
workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||
|
||||
assert 'python tests/run.py --shard "${{ matrix.shard }}"' in workflow
|
||||
assert "python -m coverage run tests/run.py --serial" in workflow
|
||||
assert "python -m coverage run --parallel-mode tests/run.py --shard" in workflow
|
||||
assert "mapfile" not in workflow
|
||||
assert "SHARD_INDEX" not in workflow
|
||||
|
||||
Reference in New Issue
Block a user