From cba9779a8a4fa5cc6f2c23e618d8217283af324e Mon Sep 17 00:00:00 2001 From: InfinityPacer Date: Thu, 22 Oct 2026 14:59:42 +0800 Subject: [PATCH] build(docker): refresh system packages against a tracked baseline Co-Authored-By: Claude Opus 5.5 --- .github/workflows/beta.yml | 25 +++++++++++++++++++ .github/workflows/build-v3.yml | 25 +++++++++++++++++++ docker/Dockerfile | 17 +++++++++++++ docker/system-baseline | 1 + .../12-collaboration-and-distribution.md | 2 ++ tests/test_release_supply_chain.py | 10 ++++++-- 6 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 docker/system-baseline diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 7ea6b3b58..5d033e7bd 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -106,6 +106,27 @@ jobs: echo "digest=sha256:$(sha256sum app/agent/llm/resources/models.json | awk '{print $1}')" >> "$GITHUB_OUTPUT" echo "Downloaded models.dev catalog ($(wc -c < app/agent/llm/resources/models.json) bytes)" + - name: Resolve System Refresh Key + id: system_refresh + run: | + # 刷新键决定镜像末尾系统安全更新层何时重建:每 ISO 周最多一次, + # docker/system-baseline 变化时立即重建。计算失败只降级为基线值,不阻断发布。 + baseline=$(tr -d '[:space:]' < docker/system-baseline 2>/dev/null || true) + if ! [[ "$baseline" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then + echo "::warning::docker/system-baseline 缺失或格式无效,系统基线记为 unknown" + baseline=unknown + fi + week=$(date -u +%G-W%V 2>/dev/null || true) + if [[ "$week" =~ ^[0-9]{4}-W[0-9]{2}$ ]]; then + key="${week}-${baseline}" + else + echo "::warning::无法计算 ISO 周,系统刷新键仅使用系统基线" + key="${baseline}" + fi + echo "System refresh key: ${key}" + echo "baseline=${baseline}" >> "$GITHUB_OUTPUT" + echo "key=${key}" >> "$GITHUB_OUTPUT" + - name: Docker Meta id: meta uses: docker/metadata-action@v6 @@ -162,8 +183,10 @@ jobs: MOVIEPILOT_PLUGINS_REF=${{ steps.payloads.outputs.plugins_revision }} MOVIEPILOT_RESOURCES_REF=${{ steps.payloads.outputs.resources_revision }} MOVIEPILOT_PYTHON_VARIANT=standard + MOVIEPILOT_SYSTEM_REFRESH_ID=${{ steps.system_refresh.outputs.key }} labels: | ${{ steps.meta.outputs.labels }} + org.moviepilot.system-baseline=${{ steps.system_refresh.outputs.baseline }} org.opencontainers.image.revision=${{ env.SOURCE_COMMIT }} org.moviepilot.source-revision=${{ env.SOURCE_COMMIT }} org.moviepilot.frontend-version=${{ steps.release_version.outputs.frontend_version }} @@ -195,8 +218,10 @@ jobs: MOVIEPILOT_PLUGINS_REF=${{ steps.payloads.outputs.plugins_revision }} MOVIEPILOT_RESOURCES_REF=${{ steps.payloads.outputs.resources_revision }} MOVIEPILOT_PYTHON_VARIANT=free-threaded + MOVIEPILOT_SYSTEM_REFRESH_ID=${{ steps.system_refresh.outputs.key }} labels: | ${{ steps.meta_ft.outputs.labels }} + org.moviepilot.system-baseline=${{ steps.system_refresh.outputs.baseline }} org.opencontainers.image.revision=${{ env.SOURCE_COMMIT }} org.moviepilot.source-revision=${{ env.SOURCE_COMMIT }} org.moviepilot.frontend-version=${{ steps.release_version.outputs.frontend_version }} diff --git a/.github/workflows/build-v3.yml b/.github/workflows/build-v3.yml index bede1330e..6ee1da06d 100644 --- a/.github/workflows/build-v3.yml +++ b/.github/workflows/build-v3.yml @@ -172,6 +172,27 @@ jobs: fi echo "release_commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + - name: Resolve System Refresh Key + id: system_refresh + run: | + # 刷新键决定镜像末尾系统安全更新层何时重建:每 ISO 周最多一次, + # docker/system-baseline 变化时立即重建。计算失败只降级为基线值,不阻断发布。 + baseline=$(tr -d '[:space:]' < docker/system-baseline 2>/dev/null || true) + if ! [[ "$baseline" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then + echo "::warning::docker/system-baseline 缺失或格式无效,系统基线记为 unknown" + baseline=unknown + fi + week=$(date -u +%G-W%V 2>/dev/null || true) + if [[ "$week" =~ ^[0-9]{4}-W[0-9]{2}$ ]]; then + key="${week}-${baseline}" + else + echo "::warning::无法计算 ISO 周,系统刷新键仅使用系统基线" + key="${baseline}" + fi + echo "System refresh key: ${key}" + echo "baseline=${baseline}" >> "$GITHUB_OUTPUT" + echo "key=${key}" >> "$GITHUB_OUTPUT" + - name: Docker Meta id: meta uses: docker/metadata-action@v6 @@ -231,8 +252,10 @@ jobs: MOVIEPILOT_PLUGINS_REF=${{ steps.payloads.outputs.plugins_revision }} MOVIEPILOT_RESOURCES_REF=${{ steps.payloads.outputs.resources_revision }} MOVIEPILOT_PYTHON_VARIANT=standard + MOVIEPILOT_SYSTEM_REFRESH_ID=${{ steps.system_refresh.outputs.key }} labels: | ${{ steps.meta.outputs.labels }} + org.moviepilot.system-baseline=${{ steps.system_refresh.outputs.baseline }} org.opencontainers.image.revision=${{ steps.release_snapshot.outputs.release_commit }} org.moviepilot.source-revision=${{ env.SOURCE_COMMIT }} org.moviepilot.release-snapshot-revision=${{ steps.release_snapshot.outputs.release_commit }} @@ -265,8 +288,10 @@ jobs: MOVIEPILOT_PLUGINS_REF=${{ steps.payloads.outputs.plugins_revision }} MOVIEPILOT_RESOURCES_REF=${{ steps.payloads.outputs.resources_revision }} MOVIEPILOT_PYTHON_VARIANT=free-threaded + MOVIEPILOT_SYSTEM_REFRESH_ID=${{ steps.system_refresh.outputs.key }} labels: | ${{ steps.meta_ft.outputs.labels }} + org.moviepilot.system-baseline=${{ steps.system_refresh.outputs.baseline }} org.opencontainers.image.revision=${{ steps.release_snapshot.outputs.release_commit }} org.moviepilot.source-revision=${{ env.SOURCE_COMMIT }} org.moviepilot.release-snapshot-revision=${{ steps.release_snapshot.outputs.release_commit }} diff --git a/docker/Dockerfile b/docker/Dockerfile index 81438a2bf..92ad7e2dd 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -367,6 +367,23 @@ RUN mkdir -p ${HOME} \ && echo "zh_CN.UTF-8 UTF-8" >> /etc/locale.gen \ && locale-gen zh_CN.UTF-8 +# 系统安全更新层:放在所有大层之后、--link 载荷层之前,刷新时只重建这一小层。 +# CI 传入 "-<系统基线>" 作为刷新键,每周最多重建一次;docker/system-baseline +# 变化时 COPY 内容变化,同样立即重建。未传键的本地构建按普通缓存规则复用。 +# 基线文件写入镜像,供应用内更新判断当前镜像是否满足目标版本的 MIN_SYSTEM_BASELINE。 +ARG MOVIEPILOT_SYSTEM_REFRESH_ID="" +COPY docker/system-baseline /etc/moviepilot/system-baseline +RUN set -eu; \ + printf 'System refresh key: %s, baseline: %s\n' \ + "${MOVIEPILOT_SYSTEM_REFRESH_ID:-unset}" "$(cat /etc/moviepilot/system-baseline)"; \ + apt-get update; \ + DEBIAN_FRONTEND=noninteractive apt-get -y --no-install-recommends \ + -o Dpkg::Options::=--force-confdef \ + -o Dpkg::Options::=--force-confold \ + upgrade; \ + apt-get clean; \ + rm -rf /var/lib/apt/lists/* /var/tmp/* + # 载荷按独立更新频率写入镜像,后端源码保持在最后一层。 WORKDIR /app diff --git a/docker/system-baseline b/docker/system-baseline new file mode 100644 index 000000000..06c8cac80 --- /dev/null +++ b/docker/system-baseline @@ -0,0 +1 @@ +2026-10-22 diff --git a/docs/rules/12-collaboration-and-distribution.md b/docs/rules/12-collaboration-and-distribution.md index 1e18e75d2..caecb01b5 100644 --- a/docs/rules/12-collaboration-and-distribution.md +++ b/docs/rules/12-collaboration-and-distribution.md @@ -71,6 +71,8 @@ ci: improve docker build cache - The primary Docker image bundles the backend (Python app), frontend static files (from `public/`), and resource data. - Docker build and release are managed by CI. Do not manually trigger or alter the Docker release flow unless the task explicitly requires it. - If a Dockerfile change is needed, update `Dockerfile` and verify the build locally before submitting. +- The image ends with a small system security update layer. CI rebuilds it at most once per ISO week; changing the ISO date in `docker/system-baseline` rebuilds it immediately and stamps the new date into `/etc/moviepilot/system-baseline`. +- `MIN_SYSTEM_BASELINE` in `version.py` is the oldest image baseline that may install this release through the in-app or Dev updater. Raise it only for severe system-level vulnerabilities, and set it to the same date as `docker/system-baseline` in that release. Older Docker images then refuse the update and ask the user to pull the new image. --- diff --git a/tests/test_release_supply_chain.py b/tests/test_release_supply_chain.py index c3c2d86c8..a124dc8c9 100644 --- a/tests/test_release_supply_chain.py +++ b/tests/test_release_supply_chain.py @@ -119,15 +119,21 @@ def _run_release_script( ) -def test_base_image_uses_refreshable_tag_and_apt_does_not_upgrade_in_place() -> None: +def test_base_image_uses_refreshable_tag_and_upgrades_only_in_refresh_layer() -> None: """ - 使用官方基础镜像及所需运行依赖,不追加系统包扫描补丁。 + 使用官方基础镜像及所需运行依赖,不追加针对扫描结果的逐包补丁; + 系统包只在 final 末尾由刷新键控制的系统安全更新层统一升级。 """ dockerfile = DOCKERFILE.read_text(encoding="utf-8") assert 'ARG MOVIEPILOT_PYTHON_VERSION="3.14.7"' in dockerfile assert "FROM python:${MOVIEPILOT_PYTHON_VERSION}-slim-trixie AS base" in dockerfile assert 'uv python install --no-bin "${MOVIEPILOT_PYTHON_VERSION}t"' in dockerfile assert "apt-get upgrade" not in dockerfile + assert dockerfile.count("upgrade;") == 1 + assert "dist-upgrade" not in dockerfile + refresh_layer = dockerfile.split('ARG MOVIEPILOT_SYSTEM_REFRESH_ID=""', maxsplit=1)[1] + assert "upgrade;" in refresh_layer + assert "apt-get install" not in refresh_layer assert "pip uninstall" not in dockerfile assert "break-system-packages" not in dockerfile browser_install = dockerfile.split("python -m playwright install-deps chromium", maxsplit=1)[1]