Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion backend/tests/test_ci_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
[
Expand All @@ -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"}),
],
)
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions mock-target/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
9 changes: 8 additions & 1 deletion scripts/ci_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand All @@ -100,6 +101,8 @@
"compose.yaml",
"mock-target/Dockerfile",
"mock-target/.dockerignore",
"mock-target/pyproject.toml",
Comment thread
a3384379 marked this conversation as resolved.
"mock-target/uv.lock",
"deploy/postgres-walg/Dockerfile",
"deploy/compact/compose.yaml",
"deploy/compact/compose.build.yaml",
Expand Down Expand Up @@ -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 验证")
Expand Down
Loading