diff --git a/yuqei-infra/scripts/linux_release_common.py b/yuqei-infra/scripts/linux_release_common.py index 08bc4f27..6013d90b 100644 --- a/yuqei-infra/scripts/linux_release_common.py +++ b/yuqei-infra/scripts/linux_release_common.py @@ -766,7 +766,7 @@ def validate_compose_gate_report( if not isinstance(expected_digest, str) or not SHA256_RE.fullmatch(expected_digest): raise ReleaseValidationError(f"source manifest lacks Compose input: {expected_path}") validated_inputs.append({"path": expected_path, "sha256": expected_digest}) - if report["application_service_count"] != 9 or report["production_service_count"] != 16: + if report["application_service_count"] != 9 or report["production_service_count"] != 17: raise ReleaseValidationError("production Compose gate service inventory drift") applications_without_web = [ @@ -781,10 +781,10 @@ def validate_compose_gate_report( ] expected_cases = [] for profile_name, profiles, service_count, includes_web in ( - ("default", [], 13, False), - ("backend-hosted-web", ["backend-hosted-web"], 14, True), - ("ops", ["ops"], 15, False), - ("backend-hosted-web+ops", ["backend-hosted-web", "ops"], 16, True), + ("default", [], 14, False), + ("backend-hosted-web", ["backend-hosted-web"], 15, True), + ("ops", ["ops"], 16, False), + ("backend-hosted-web+ops", ["backend-hosted-web", "ops"], 17, True), ): applications = applications_without_web + (["contract-web"] if includes_web else []) for mode in ("online", "offline"): diff --git a/yuqei-infra/scripts/run-linux-release-build.py b/yuqei-infra/scripts/run-linux-release-build.py index 47820142..af976325 100644 --- a/yuqei-infra/scripts/run-linux-release-build.py +++ b/yuqei-infra/scripts/run-linux-release-build.py @@ -102,6 +102,7 @@ TRUSTED_PRODUCTION_SERVICES = ( "postgres", "postgres-init-databases", "redis", + "ai-redis", "minio", "contract-api-migrate", "contract-api", diff --git a/yuqei-infra/tests/test_linux_release_build_tools.py b/yuqei-infra/tests/test_linux_release_build_tools.py index 46f4d2c7..5fca6598 100644 --- a/yuqei-infra/tests/test_linux_release_build_tools.py +++ b/yuqei-infra/tests/test_linux_release_build_tools.py @@ -528,12 +528,10 @@ def _compose_gate_report(source: dict[str, Any]) -> tuple[bytes, dict[str, Any]] ] cases = [] case_index = 0 - for profile_name, profiles, service_count, includes_web in ( - ("default", [], 13, False), - ("backend-hosted-web", ["backend-hosted-web"], 14, True), - ("ops", ["ops"], 15, False), - ("backend-hosted-web+ops", ["backend-hosted-web", "ops"], 16, True), - ): + for profile_name, profile_tuple in builder.TRUSTED_PROFILE_CASES: + profiles = list(profile_tuple) + service_count = len(builder._required_compose_services(profile_tuple)) + includes_web = "backend-hosted-web" in profile_tuple applications = applications_without_web + (["contract-web"] if includes_web else []) for mode in ("online", "offline"): case_index += 1 @@ -555,8 +553,8 @@ def _compose_gate_report(source: dict[str, Any]) -> tuple[bytes, dict[str, Any]] {"path": path, "sha256": source_records[path]["sha256"]} for path in common.COMPOSE_GATE_POLICY["compose_inputs"] ], - "application_service_count": 9, - "production_service_count": 16, + "application_service_count": len(builder.TRUSTED_APPLICATION_SERVICE_IMAGE_ENV), + "production_service_count": len(builder.TRUSTED_PRODUCTION_SERVICES), "cases": cases, } return env_bytes, report @@ -583,6 +581,46 @@ def test_synthetic_compose_gate_env_covers_required_interpolation() -> None: assert values["BACKEND_TLS_TRUSTED_CA_FILE"] == "/synthetic/tls/trusted-ca.pem" +def _compose_service_blocks(relative_path: str) -> dict[str, list[str]]: + blocks: dict[str, list[str]] = {} + current_service: str | None = None + in_services = False + for line in (REPO_ROOT / relative_path).read_text(encoding="utf-8").splitlines(): + if not in_services: + in_services = line == "services:" + continue + if line and not line[0].isspace(): + break + match = re.fullmatch(r" ([a-z0-9][a-z0-9-]*):\s*", line) + if match: + current_service = match.group(1) + blocks[current_service] = [] + elif current_service is not None: + blocks[current_service].append(line) + return blocks + + +def test_compose_gate_inventory_matches_candidate_compose_sources() -> None: + compose_services: set[str] = set() + for relative_path in common.COMPOSE_GATE_POLICY["compose_inputs"]: + compose_services.update(_compose_service_blocks(relative_path)) + assert compose_services == set(builder.TRUSTED_PRODUCTION_SERVICES) + + production_blocks = _compose_service_blocks( + "yuqei-infra/compose/docker-compose.production.yml" + ) + actual_profiles: dict[str, str] = {} + for service_name, lines in production_blocks.items(): + profile_lines = [line for line in lines if line.strip().startswith("profiles:")] + if not profile_lines: + continue + assert len(profile_lines) == 1 + profiles = re.findall(r'"([a-z0-9][a-z0-9-]*)"', profile_lines[0]) + assert len(profiles) == 1 + actual_profiles[service_name] = profiles[0] + assert actual_profiles == dict(builder.TRUSTED_PROFILE_ONLY_SERVICES) + + def _compose_model( *, mode: str, profile_name: str, profiles: tuple[str, ...] ) -> dict[str, Any]: