From 6b3986411ced52c73a69775955ca456a50893930 Mon Sep 17 00:00:00 2001 From: tianM <46552064+a3384379@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:59:16 +0800 Subject: [PATCH 1/2] ci: refine standalone and mock dependency routing --- backend/tests/test_ci_plan.py | 51 ++++++++++++++++++++++++++++++++++- scripts/ci_plan.py | 9 ++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/backend/tests/test_ci_plan.py b/backend/tests/test_ci_plan.py index 80a6d5ba..3007e236 100644 --- a/backend/tests/test_ci_plan.py +++ b/backend/tests/test_ci_plan.py @@ -99,13 +99,34 @@ def test_alembic_config_and_real_migration_root_are_routed() -> None: "backend/alembic.ini", "backend/migrations/env.py", "backend/migrations/versions/20260809_0001_access_control.py", - "backend/app/schemas/workflows.py", + "backend/app/models/workflows.py", + "backend/app/repositories/workflows.py", ): selected = plan([path]) assert selected.tier == "integration" assert {"backend-full", "upgrade", "windows"} <= set(selected.required) +@pytest.mark.parametrize("labels", [set(), {"ci:light"}]) +@pytest.mark.parametrize( + "paths", + [ + ["backend/app/core/standalone_schema.py"], + ["backend/app/core/standalone_schema.py", "backend/tests/test_standalone_runtime.py"], + ], +) +def test_standalone_schema_requires_runtime_validation(paths: list[str], labels: set[str]) -> None: + selected = plan(paths, labels) + assert selected.tier_floor == "integration" + assert set(selected.required) == {"quick", "backend-full", "windows"} + + +def test_api_schema_keeps_consumers_without_database_upgrade() -> None: + selected = plan(["backend/app/schemas/workflows.py"]) + assert selected.tier_floor == "integration" + assert set(selected.required) == {"quick", "backend-full", "frontend-full", "compose"} + + @pytest.mark.parametrize( ("path", "required"), [ @@ -119,6 +140,8 @@ def test_alembic_config_and_real_migration_root_are_routed() -> None: ("frontend/nginx.conf", {"frontend-full", "compose", "security"}), ("frontend/.dockerignore", {"frontend-full", "compose", "security"}), ("mock-target/Dockerfile", {"compose", "security"}), + ("mock-target/pyproject.toml", {"compose", "security"}), + ("mock-target/uv.lock", {"compose", "security"}), ("deploy/compact/images.env.example", {"compact", "compose", "security"}), ], ) @@ -176,6 +199,32 @@ def test_renamed_image_input_uses_both_directories() -> None: assert {"backend-full", "frontend-full", "compose", "security"} <= set(selected.required) +@pytest.mark.parametrize("old_path", ["mock-target/pyproject.toml", "mock-target/uv.lock"]) +def test_renamed_mock_dependency_keeps_old_image_risk(old_path: str) -> None: + files = [ + {"filename": "docs/mock-dependency.md", "previous_filename": old_path, "status": "renamed"} + ] + paths = ci_plan._paths_from_files(files) + selected = plan( + paths, + {"ci:light"}, + ci_plan._removed_paths_from_files(files), + ) + assert selected.tier_floor == "integration" + assert {"quick", "compose", "security"} <= set(selected.required) + + +@pytest.mark.parametrize("path", ["mock-target/pyproject.toml", "mock-target/uv.lock"]) +def test_mock_dependency_requires_security_without_label(path: str) -> None: + selected = plan([path]) + assert set(selected.required) == {"quick", "compose", "security"} + + +def test_mock_application_source_does_not_require_image_scan() -> None: + selected = plan(["mock-target/app/main.py"]) + assert set(selected.required) == {"quick", "compose"} + + def test_policy_entrypoints_use_isolated_python() -> None: workflow = (Path(__file__).parents[2] / ".github/workflows/pr-validation.yml").read_text() assert "python3 -I scripts/ci_plan.py" in workflow diff --git a/scripts/ci_plan.py b/scripts/ci_plan.py index 5e73ad3d..e3ace561 100644 --- a/scripts/ci_plan.py +++ b/scripts/ci_plan.py @@ -90,7 +90,8 @@ "backend/app/repositories/", "backend/migrations/", ) -MIGRATION_PREFIXES = ("backend/migrations/", "backend/app/schemas/") +MIGRATION_PREFIXES = ("backend/migrations/",) +STANDALONE_SCHEMA_FILE = "backend/app/core/standalone_schema.py" BACKEND_IMAGE_INPUTS = frozenset({"backend/Dockerfile", "backend/.dockerignore"}) FRONTEND_IMAGE_INPUTS = frozenset( {"frontend/Dockerfile", "frontend/.dockerignore", "frontend/nginx.conf"} @@ -100,6 +101,8 @@ "compose.yaml", "mock-target/Dockerfile", "mock-target/.dockerignore", + "mock-target/pyproject.toml", + "mock-target/uv.lock", "deploy/postgres-walg/Dockerfile", "deploy/compact/compose.yaml", "deploy/compact/compose.build.yaml", @@ -257,6 +260,10 @@ def backend(self, path: str) -> None: self.tier = _tier_max(self.tier, "integration") self.domains.add("compose") self.reasons.add("关键后端运行或数据路径") + if path == STANDALONE_SCHEMA_FILE: + self.tier = _tier_max(self.tier, "integration") + self.domains.add("windows") + self.reasons.add("Standalone SQLite 初始化与现存安装升级需运行形态验证") if path.startswith(("backend/app/domain/execution", "backend/app/engine/")): self.domains.update(("frontend", "windows")) self.reasons.add("引擎结果需前端契约与 Standalone 验证") From 17ba17860ed303d289cf89ee0ca4b51a1b0425ad Mon Sep 17 00:00:00 2001 From: tianM <46552064+a3384379@users.noreply.github.com> Date: Sun, 27 Sep 2026 21:12:53 +0800 Subject: [PATCH 2/2] build: reject stale mock target lockfile --- mock-target/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mock-target/Dockerfile b/mock-target/Dockerfile index a8cc087e..75ebcf15 100644 --- a/mock-target/Dockerfile +++ b/mock-target/Dockerfile @@ -97,9 +97,9 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ WORKDIR /app COPY --from=uv /uv /uvx /bin/ COPY pyproject.toml uv.lock ./ -RUN uv sync --frozen --no-dev --no-install-project +RUN uv sync --locked --no-dev --no-install-project COPY app ./app -RUN uv sync --frozen --no-dev +RUN uv sync --locked --no-dev ENV PATH="/app/.venv/bin:$PATH"