security: close BUILD-05 release integrity gaps

This commit is contained in:
2026-09-27 04:53:47 +08:00
parent a69dd9093b
commit 3182d73b0b
14 changed files with 1397 additions and 175 deletions
@@ -77,6 +77,10 @@ jobs:
yuqei-infra/deploy \
yuqei-infra/backup \
yuqei-infra/observability \
yuqei-infra/scripts/build-infrastructure-image-bundle.py \
yuqei-infra/scripts/build-release-candidate-manifest.py \
yuqei-infra/scripts/build-release-worktree-ownership.py \
yuqei-infra/scripts/build-third-party-materials.py \
yuqei-infra/scripts/run-ai-provider-acceptance.py \
-f json \
-o security-artifacts/bandit-project.json
@@ -2,6 +2,9 @@
COMPOSE_PROJECT_NAME=yuqei-v2-production
RELEASE_ID=CHANGE_ME_IMMUTABLE_RC_TAG
YUQEI_RELEASE_ENVIRONMENT_ID=CHANGE_ME_STABLE_ENVIRONMENT_ID
YUQEI_RELEASE_ENVIRONMENT_CLASS=production
YUQEI_RELEASE_HOST_ID=CHANGE_ME_STABLE_HOST_ID
CONTRACT_API_IMAGE=registry.example.com/yuqei/contract-api@sha256:CHANGE_ME_64_HEX_DIGEST
CONTRACT_WEB_IMAGE=registry.example.com/yuqei/contract-web@sha256:CHANGE_ME_64_HEX_DIGEST
AI_PLATFORM_API_IMAGE=registry.example.com/yuqei/ai-platform-api@sha256:CHANGE_ME_64_HEX_DIGEST
@@ -121,6 +121,25 @@ def canonical_sha256(value: object) -> str:
return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
def runtime_host_identity_sha256(
host_id: object,
docker_daemon_identity: Mapping[str, object],
) -> str:
"""Derive the recovery host identity from stable local Docker/host fields."""
normalized_host_id = _identifier(host_id, label="host_id")
required = ("docker_host", "daemon_id", "daemon_name", "docker_root_dir")
values: dict[str, str] = {}
for field in required:
value = docker_daemon_identity.get(field)
if not isinstance(value, str) or not value.strip():
raise SnapshotRestorePlanError(
f"Docker daemon identity field is missing for host binding: {field}"
)
values[field] = value
return canonical_sha256({"host_id": normalized_host_id, **values})
def file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
@@ -1097,6 +1116,7 @@ def validate_evidence(
"environment_class",
"host_id",
"host_identity_sha256",
"docker_daemon_identity_sha256",
"compose_project_name",
},
label="recovery target",
@@ -1108,6 +1128,10 @@ def validate_evidence(
if target.get("compose_project_name") != package_binding.get("compose_project_name"):
raise SnapshotRestorePlanError("recovery target Compose project differs from the package")
_sha(target.get("host_identity_sha256"), label="host identity SHA-256")
_sha(
target.get("docker_daemon_identity_sha256"),
label="Docker daemon identity SHA-256",
)
consistency_set_id = _identifier(
evidence.get("consistency_set_id"), label="consistency_set_id"
)
+28 -4
View File
@@ -132,6 +132,8 @@ def _exec_verified_module(name: str, path: Path, raw: bytes) -> Any:
def _load_release_package_module(
verified_sources: Mapping[str, bytes] | None = None,
*,
verified_repo_root: Path = REPO_ROOT,
):
name = "yuqei_rollback_rehearsal_release_package"
if verified_sources is not None:
@@ -178,7 +180,7 @@ def _load_release_package_module(
):
loaded[relative] = _exec_verified_module(
module_names[relative],
REPO_ROOT / relative,
verified_repo_root / relative,
verified_sources[relative],
)
release_package = loaded["yuqei-infra/deploy/release-package.py"]
@@ -212,7 +214,7 @@ def _load_release_package_module(
"yuqei-infra/release-trust/third-party-materials-baseline.json"
)
baseline_raw, baseline_sha = _read_secure_bytes(
REPO_ROOT / baseline_relative,
verified_repo_root / baseline_relative,
label="verified third-party materials trust baseline",
max_bytes=MAX_JSON_BYTES,
)
@@ -1710,13 +1712,27 @@ def plan_from_files(
current_package_sha256=current_package_sha256,
repo_root=repo_root,
)
release_package = _load_release_package_module(verified_tool_sources)
predecessor_tool_sources, predecessor_bootstrap_tool_hashes = (
_bootstrap_verified_tool_sources(
current_package_dir=predecessor_package_dir,
current_package_sha256=predecessor_package_sha256,
repo_root=predecessor_repo_root,
)
)
release_package = _load_release_package_module(
verified_tool_sources,
verified_repo_root=repo_root,
)
predecessor_release_package = _load_release_package_module(
predecessor_tool_sources,
verified_repo_root=predecessor_repo_root,
)
current = release_package.validate_release_package(
current_package_dir,
expected_package_sha256=current_package_sha256,
repo_root=repo_root,
)
predecessor = release_package.validate_release_package(
predecessor = predecessor_release_package.validate_release_package(
predecessor_package_dir,
expected_package_sha256=predecessor_package_sha256,
repo_root=predecessor_repo_root,
@@ -1777,6 +1793,14 @@ def plan_from_files(
raise RehearsalProofError(
"Rollback tool inputs changed after the pre-import package bootstrap."
)
predecessor_validated_tool_hashes = _validated_tool_input_hashes(
current_package=predecessor,
repo_root=predecessor_repo_root,
)
if predecessor_validated_tool_hashes != predecessor_bootstrap_tool_hashes:
raise RehearsalProofError(
"Predecessor rollback tool inputs changed after the pre-import package bootstrap."
)
runtime_binding = _build_runtime_binding(
current_package=current,
predecessor_package=predecessor,
@@ -25,6 +25,7 @@ DEFAULT_ARTIFACT_ROOT = REPO_ROOT / "project-docs" / "qa-artifacts" / "v2-produc
IMMUTABLE_IMAGE_PATTERN = re.compile(r"^[^\s]+@sha256:[0-9a-fA-F]{64}$")
IDENTIFIER_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
RELEASE_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
RUNTIME_TARGET_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$")
IMAGE_DIGEST_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$")
PLACEHOLDER_MARKERS = ("change_me", "local-only", "example.com", "your-api-key", "replace_with")
@@ -130,6 +131,9 @@ def validate_environment(values: dict[str, str], *, template_mode: bool = False)
required = (
"COMPOSE_PROJECT_NAME",
"RELEASE_ID",
"YUQEI_RELEASE_ENVIRONMENT_ID",
"YUQEI_RELEASE_ENVIRONMENT_CLASS",
"YUQEI_RELEASE_HOST_ID",
"CONTRACT_API_IMAGE",
"CONTRACT_WEB_IMAGE",
"AI_PLATFORM_API_IMAGE",
@@ -198,6 +202,34 @@ def validate_environment(values: dict[str, str], *, template_mode: bool = False)
if values.get(key, "").strip() != expected
]
checks.append(_result("production_modes", not invalid_exact, "Production modes are strict." if not invalid_exact else f"Invalid production values: {', '.join(invalid_exact)}."))
target_ids = {
"YUQEI_RELEASE_ENVIRONMENT_ID": values.get(
"YUQEI_RELEASE_ENVIRONMENT_ID", ""
).strip(),
"YUQEI_RELEASE_HOST_ID": values.get("YUQEI_RELEASE_HOST_ID", "").strip(),
}
invalid_target_ids = [
key
for key, value in target_ids.items()
if not RUNTIME_TARGET_ID_PATTERN.fullmatch(value)
]
environment_class = values.get("YUQEI_RELEASE_ENVIRONMENT_CLASS", "").strip()
target_ok = not invalid_target_ids and environment_class in {
"isolated_uat",
"production",
}
checks.append(
_result(
"release_runtime_target",
target_ok,
(
"Release environment and host identity are explicit."
if target_ok
else "Release environment_id/host_id must be stable identifiers and "
"environment_class must be isolated_uat or production."
),
)
)
checks.append(_validate_identity_email_delivery(values))
checks.append(_validate_ai_timeout_budget(values))
checks.append(_validate_capacity_thresholds(values))
+215 -40
View File
@@ -23,6 +23,7 @@ import re
import stat
import subprocess
import sys
import time
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import UTC, datetime
@@ -57,8 +58,8 @@ RECEIPT_SCHEMA = "yuqei.release-package-receipt.v3"
PENDING_SCHEMA = "yuqei.release-package-pending.v3"
POST_APPLY_SCHEMA = "yuqei.release-post-apply-check.v2"
PREFLIGHT_BINDING_SCHEMA = "yuqei.production-preflight-binding.v2"
INFRASTRUCTURE_SOURCE_SET_SCHEMA = "yuqei.infrastructure-image-source-set.v1"
INFRASTRUCTURE_BUNDLE_SCHEMA = "yuqei.infrastructure-image-bundle.v1"
INFRASTRUCTURE_SOURCE_SET_SCHEMA = "yuqei.infrastructure-image-source-set.v2"
INFRASTRUCTURE_BUNDLE_SCHEMA = "yuqei.infrastructure-image-bundle.v2"
THIRD_PARTY_INPUT_SCHEMA = "yuqei.third-party-materials-input.v1"
THIRD_PARTY_MANIFEST_SCHEMA = "yuqei.third-party-materials.v1"
THIRD_PARTY_VERIFICATION_SCHEMA = "yuqei.third-party-materials-verification.v1"
@@ -153,6 +154,7 @@ INSTALL_SERVICES = (
"minio",
"contract-api-migrate",
"ai-platform-api-migrate",
"ai-platform-prompt-assets-import",
*RUNTIME_SERVICES,
)
V1_REQUIRED_ARTIFACTS = (
@@ -185,9 +187,22 @@ RELEASE_IMPORT_ROLE_ENV_SERVICES = (
(
"contract-api",
"CONTRACT_API_IMAGE",
("contract-api", "contract-review-worker", "contract-playbook-governance-worker"),
(
"contract-api",
"contract-api-migrate",
"contract-review-worker",
"contract-playbook-governance-worker",
),
),
(
"ai-platform-api",
"AI_PLATFORM_API_IMAGE",
(
"ai-platform-api",
"ai-platform-api-migrate",
"ai-platform-prompt-assets-import",
),
),
("ai-platform-api", "AI_PLATFORM_API_IMAGE", ("ai-platform-api",)),
("ai-worker", "AI_WORKER_IMAGE", ("worker",)),
("postgres-runtime", "POSTGRES_IMAGE", ("postgres",)),
("postgres-client", "POSTGRES_CLIENT_IMAGE", ("postgres-init-databases",)),
@@ -209,8 +224,12 @@ ONE_SHOT_INSTALL_SERVICES = (
"postgres-init-databases",
"contract-api-migrate",
"ai-platform-api-migrate",
"ai-platform-prompt-assets-import",
)
POST_APPLY_WAIT_TIMEOUT_SECONDS = 300
POST_APPLY_POLL_INTERVAL_SECONDS = 2
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
COMMIT_RE = re.compile(r"^(?:[0-9a-f]{40}|[0-9a-f]{64})$")
RELEASE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
@@ -1309,6 +1328,8 @@ def _validate_third_party_materials(
candidate_tag: str,
source_manifest_sha256: str,
release_import_plan: tuple[dict[str, Any], ...],
installable_manifest: Mapping[str, Any],
infrastructure_bundle: Mapping[str, Any],
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], str]:
materials_input = _read_json(input_path, label="third-party materials input")
if materials_input.get("schema_version") != THIRD_PARTY_INPUT_SCHEMA:
@@ -1365,14 +1386,90 @@ def _validate_third_party_materials(
)
for item in release_import_plan
}
def report_fingerprint(raw: object, *, label: str) -> tuple[str, int]:
if not isinstance(raw, Mapping):
raise ReleasePackageError(f"{label} record is missing.")
digest = raw.get("sha256")
size = raw.get("size_bytes")
if not isinstance(digest, str) or not SHA256_RE.fullmatch(digest):
raise ReleasePackageError(f"{label} record has an invalid SHA-256.")
if not isinstance(size, int) or isinstance(size, bool) or size <= 0:
raise ReleasePackageError(f"{label} record has an invalid size.")
return digest, size
formal_reports: dict[str, dict[str, tuple[str, int]]] = {}
formal_image_groups = (
("application", installable_manifest.get("images")),
("infrastructure", infrastructure_bundle.get("infrastructure_images")),
)
for image_class, raw_group in formal_image_groups:
if not isinstance(raw_group, list):
raise ReleasePackageError(
f"Formal {image_class} image security-report inventory is missing."
)
for raw_image in raw_group:
if not isinstance(raw_image, Mapping):
raise ReleasePackageError(
f"Formal {image_class} image security-report entry is invalid."
)
role = raw_image.get("role")
if not isinstance(role, str) or role not in expected_images:
raise ReleasePackageError(
f"Formal {image_class} image security-report role is invalid."
)
if role in formal_reports:
raise ReleasePackageError(
f"Formal image security-report inventory repeats role: {role}"
)
expected_manifest_digest, expected_config_digest = expected_images[role]
if (
raw_image.get("manifest_digest") != expected_manifest_digest
or raw_image.get("config_digest") != expected_config_digest
):
raise ReleasePackageError(
f"Formal image security reports bind a different image: {role}"
)
formal_reports[role] = {
"sbom": report_fingerprint(
raw_image.get("sbom"), label=f"formal {role} SBOM"
),
"trivy_license": report_fingerprint(
raw_image.get("license_scan"),
label=f"formal {role} Trivy license report",
),
}
if set(formal_reports) != set(expected_images):
raise ReleasePackageError(
"Formal image security reports do not cover exactly all eleven release images."
)
observed_images: dict[str, tuple[object, object]] = {}
for item in raw_images:
if not isinstance(item, Mapping) or not isinstance(item.get("role"), str):
raise ReleasePackageError("Third-party image identity is invalid.")
observed_images[item["role"]] = (
role = item["role"]
if role in observed_images:
raise ReleasePackageError(f"Third-party image role is repeated: {role}")
observed_images[role] = (
item.get("manifest_digest"),
item.get("config_digest"),
)
expected_reports = formal_reports.get(role)
if expected_reports is None:
raise ReleasePackageError(
f"Third-party image role has no formal security reports: {role}"
)
for report_kind in ("sbom", "trivy_license"):
observed_report = report_fingerprint(
item.get(report_kind),
label=f"third-party {role} {report_kind}",
)
if observed_report != expected_reports[report_kind]:
raise ReleasePackageError(
"Third-party materials use a different raw image report than the "
f"formal build gate: {role} {report_kind}."
)
if observed_images != expected_images:
raise ReleasePackageError("Third-party image identities differ from the release plan.")
return materials_input, materials_manifest, included_receipt, profile
@@ -1788,6 +1885,8 @@ def validate_release_package(
candidate_tag=candidate_tag,
source_manifest_sha256=release_sha,
release_import_plan=docker_import_plan,
installable_manifest=installable_manifest,
infrastructure_bundle=infrastructure_bundle,
)
backup_proof: dict[str, Any] | None = None
@@ -2360,6 +2459,24 @@ def _builtin_post_apply_checker(
up_index = command_argv.index("up")
compose_prefix = command_argv[:up_index]
ps_command = [*compose_prefix, "ps", "--all", "--format", "json"]
expected_services = list(plan["deployed_service_set"])
service_image_ids: dict[str, str] = {}
for item in plan["docker_import_plan"]:
for service in item["services"]:
if service not in expected_services:
continue
if service in service_image_ids:
raise ReleasePackageError(
f"Post-apply image plan repeats service binding: {service}."
)
service_image_ids[service] = item["expected_loaded_image_id"]
if set(service_image_ids) != set(expected_services):
raise ReleasePackageError("Post-apply image plan does not cover every deployed service.")
deadline = time.monotonic() + POST_APPLY_WAIT_TIMEOUT_SECONDS
records: list[dict[str, Any]] = []
release_binding_verified = False
while True:
completed = _fixed_subprocess_runner(
ps_command,
check=False,
@@ -2379,25 +2496,25 @@ def _builtin_post_apply_checker(
rows = parsed
else:
raise ReleasePackageError("Compose post-apply inventory has an invalid shape.")
expected_services = list(plan["deployed_service_set"])
by_service: dict[str, Mapping[str, Any]] = {}
for row in rows:
service = row.get("Service")
if not isinstance(service, str) or service in by_service:
raise ReleasePackageError("Compose post-apply inventory repeats or omits a service.")
raise ReleasePackageError(
"Compose post-apply inventory repeats or has an invalid service."
)
by_service[service] = row
if set(by_service) != set(expected_services):
raise ReleasePackageError("Compose post-apply service inventory differs from the plan.")
service_image_ids = {
service: item["expected_loaded_image_id"]
for item in plan["docker_import_plan"]
for service in item["services"]
if service in expected_services
}
records: list[dict[str, Any]] = []
release_binding_verified = False
unexpected = set(by_service).difference(expected_services)
if unexpected:
raise ReleasePackageError("Compose post-apply service inventory exceeds the plan.")
pending_services = set(expected_services).difference(by_service)
current_records: list[dict[str, Any]] = []
current_release_binding_verified = False
for service in expected_services:
row = by_service[service]
row = by_service.get(service)
if row is None:
continue
container_id = row.get("ID")
if not isinstance(container_id, str) or not container_id:
raise ReleasePackageError(f"Post-apply container ID is missing for {service}.")
@@ -2431,18 +2548,43 @@ def _builtin_post_apply_checker(
or not isinstance(labels, Mapping)
or labels.get("com.docker.compose.project") != plan["compose_project_name"]
or labels.get("com.docker.compose.service") != service
or detail.get("Image") != service_image_ids.get(service)
or detail.get("Image") != service_image_ids[service]
):
raise ReleasePackageError(f"Post-apply project or image binding failed for {service}.")
raise ReleasePackageError(
f"Post-apply project or image binding failed for {service}."
)
status = state.get("Status")
health_payload = state.get("Health")
health = health_payload.get("Status") if isinstance(health_payload, Mapping) else None
health = (
health_payload.get("Status")
if isinstance(health_payload, Mapping)
else None
)
exit_code = state.get("ExitCode")
if service in ONE_SHOT_INSTALL_SERVICES:
if status != "exited" or exit_code != 0:
raise ReleasePackageError(f"Post-apply migration/init failed for {service}.")
elif status != "running" or health != "healthy":
raise ReleasePackageError(f"Post-apply service is not healthy: {service}.")
if status == "exited":
if exit_code != 0:
raise ReleasePackageError(
f"Post-apply migration/init failed for {service}."
)
elif status in {"created", "running", "restarting"}:
pending_services.add(service)
else:
raise ReleasePackageError(
f"Post-apply migration/init entered an invalid state: {service}."
)
elif status == "running" and health == "healthy":
pass
elif status in {"created", "running", "restarting"} and health in {
None,
"starting",
"unhealthy",
}:
pending_services.add(service)
else:
raise ReleasePackageError(
f"Post-apply service entered an invalid state: {service}."
)
env_items = config.get("Env")
if service == "ai-platform-api":
if (
@@ -2450,17 +2592,27 @@ def _builtin_post_apply_checker(
or f"AI_PLATFORM_RELEASE_ID={plan['target_release_id']}" not in env_items
):
raise ReleasePackageError("AI runtime release identity is not bound.")
release_binding_verified = True
records.append(
current_release_binding_verified = True
current_records.append(
{
"service": service,
"container_id_sha256": hashlib.sha256(container_id.encode("utf-8")).hexdigest(),
"container_id_sha256": hashlib.sha256(
container_id.encode("utf-8")
).hexdigest(),
"status": status,
"health": health,
"exit_code": exit_code,
"image_id": detail.get("Image"),
}
)
if not pending_services:
records = current_records
release_binding_verified = current_release_binding_verified
break
if time.monotonic() >= deadline:
raise ReleasePackageError("Post-apply services did not become ready before timeout.")
time.sleep(POST_APPLY_POLL_INTERVAL_SECONDS)
if "ai-platform-api" in expected_services and not release_binding_verified:
raise ReleasePackageError("Post-apply release identity check did not run.")
payload: dict[str, Any] = {
@@ -4079,26 +4231,46 @@ def _validate_first_release_install_readiness(
def _validate_readiness_runtime_binding(
*,
readiness: Mapping[str, Any],
evidence: RuntimeEvidence,
env_values: Mapping[str, str],
env_sha256: str,
daemon_identity: Mapping[str, str],
tls_binding: Mapping[str, str],
project_name: str,
) -> None:
bindings = readiness.get("release_environment_and_tls_bindings")
target = readiness.get("target")
expected_bindings = {
"production_env_sha256": evidence.env_sha256,
"production_env_sha256": env_sha256,
"tls_certificate_sha256": tls_binding.get("certificate_sha256"),
"tls_private_key_sha256": tls_binding.get("private_key_sha256"),
"tls_trusted_ca_sha256": tls_binding.get("trusted_ca_sha256"),
}
planner = _load_snapshot_restore_planner_module()
host_identity = getattr(planner, "runtime_host_identity_sha256", None)
if not callable(host_identity):
raise ReleasePackageError(
"The frozen snapshot planner has no runtime host identity algorithm."
)
expected_target = {
"environment_id": env_values.get("YUQEI_RELEASE_ENVIRONMENT_ID"),
"environment_class": env_values.get("YUQEI_RELEASE_ENVIRONMENT_CLASS"),
"host_id": env_values.get("YUQEI_RELEASE_HOST_ID"),
"host_identity_sha256": host_identity(
env_values.get("YUQEI_RELEASE_HOST_ID"),
daemon_identity,
),
"docker_daemon_identity_sha256": daemon_identity.get("identity_sha256"),
"compose_project_name": project_name,
}
if (
not isinstance(bindings, Mapping)
or dict(bindings) != expected_bindings
or not isinstance(target, Mapping)
or target.get("compose_project_name") != project_name
or dict(target) != expected_target
):
raise ReleasePackageError(
"First-release readiness receipt does not match the exact runtime env/TLS/project."
"First-release readiness receipt does not match the exact runtime "
"environment/host/daemon/TLS/project."
)
@@ -4219,14 +4391,9 @@ def execute(
if readiness is not None:
_validate_readiness_runtime_binding(
readiness=readiness,
evidence=RuntimeEvidence(
env_file=Path(plan["env_file"]),
env_values={},
env_values=parse_env_file(Path(plan["env_file"])),
env_sha256=plan["env_file_sha256"],
preflight_summary=plan["production_preflight"],
compose_gate_summary=plan["compose_merge_gate"],
daemon_identity=plan["docker_daemon_identity"],
),
tls_binding=plan["tls_material_binding"],
project_name=plan["compose_project_name"],
)
@@ -4285,8 +4452,16 @@ def execute(
return ExecutionOutcome(pending, exit_code=1, replayed=True)
if receipt_path.exists() or receipt_path.is_symlink():
existing = _read_existing_receipt(receipt_path)
if existing.get("idempotency_key") != safe_key or existing.get("plan_sha256") != plan["plan_sha256"]:
raise ReleasePackageError("Idempotency key is already bound to a different release operation.")
if existing.get("idempotency_key") != safe_key:
raise ReleasePackageError("Idempotency receipt is bound to another key.")
if existing.get("execution_mode") != execution_mode:
raise ReleasePackageError(
"Idempotency receipt execution mode does not match the current apply mode."
)
if existing.get("plan_sha256") != plan["plan_sha256"]:
raise ReleasePackageError(
"Idempotency key is already bound to a different release operation."
)
return ExecutionOutcome(existing, exit_code=0, replayed=True)
snapshot = _snapshot_inputs(
@@ -19,12 +19,28 @@ import re
import stat
import sys
import tarfile
from datetime import datetime
from pathlib import Path, PurePosixPath
from typing import Any, Mapping
SOURCE_SET_SCHEMA = "yuqei.infrastructure-image-source-set.v1"
BUNDLE_SCHEMA = "yuqei.infrastructure-image-bundle.v1"
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
from linux_release_common import ( # noqa: E402
REQUIRED_TOOL_VERSIONS,
TRIVY_DB_REPOSITORY_TRANSPORT,
ReleaseValidationError,
load_policy,
parse_rfc3339,
validate_trivy_db_metadata,
validate_trivy_license_scan,
validate_trivy_scan,
)
SOURCE_SET_SCHEMA = "yuqei.infrastructure-image-source-set.v2"
BUNDLE_SCHEMA = "yuqei.infrastructure-image-bundle.v2"
APPLICATION_MANIFEST_SCHEMA = "yuqei.installable-release-manifest.v1"
IMPORT_PLAN_SCHEMA = "yuqei.infrastructure-docker-image-import-plan.v1"
TARGET = {"os": "linux", "architecture": "amd64", "platform": "linux/amd64"}
@@ -238,6 +254,138 @@ def _file_record(root: Path, value: object, *, label: str) -> tuple[Path, dict[s
return target, {"path": relative, "sha256": digest, "size_bytes": size}
def _extended_file_record(
root: Path,
value: object,
*,
extra_fields: set[str],
label: str,
) -> tuple[Path, dict[str, Any], Mapping[str, Any]]:
if not isinstance(value, dict) or set(value) != {
"path",
"sha256",
"size_bytes",
*extra_fields,
}:
raise BundleValidationError(f"{label} record fields are missing or unknown")
base = {key: value[key] for key in ("path", "sha256", "size_bytes")}
target, normalized = _file_record(root, base, label=label)
return target, normalized, value
def _validate_database_transport(value: object) -> dict[str, Any]:
if not isinstance(value, dict):
raise BundleValidationError("Trivy database repository transport must be an object")
expected_fields = {
"reference",
"transport",
"schema_version",
"resolved_digest",
"media_type",
"stable_during_download",
"before_download_inspect_sha256",
"after_download_inspect_sha256",
}
if set(value) != expected_fields:
raise BundleValidationError("Trivy database repository transport fields drifted")
if (
value.get("reference") != TRIVY_DB_REPOSITORY_TRANSPORT["reference"]
or value.get("transport") != TRIVY_DB_REPOSITORY_TRANSPORT["transport"]
or value.get("schema_version")
!= TRIVY_DB_REPOSITORY_TRANSPORT["schema_version"]
or value.get("media_type")
not in TRIVY_DB_REPOSITORY_TRANSPORT["allowed_media_types"]
or value.get("stable_during_download") is not True
or not DIGEST_RE.fullmatch(str(value.get("resolved_digest", "")))
or any(
not SHA256_RE.fullmatch(str(value.get(field, "")))
for field in (
"before_download_inspect_sha256",
"after_download_inspect_sha256",
)
)
):
raise BundleValidationError("Trivy database repository transport evidence drifted")
return dict(value)
def _validate_security_gate(
root: Path,
value: object,
*,
application_manifest: Mapping[str, Any],
) -> tuple[dict[str, Any], dict[str, Any], Mapping[str, Any]]:
if not isinstance(value, dict) or set(value) != {
"policy",
"database",
"scanner",
"scanner_version",
}:
raise BundleValidationError("infrastructure security gate fields are missing or unknown")
if (
value.get("scanner") != "trivy"
or value.get("scanner_version") != REQUIRED_TOOL_VERSIONS["trivy"]
):
raise BundleValidationError("infrastructure scanner identity is not pinned")
policy_path, policy_record = _file_record(
root,
value.get("policy"),
label="infrastructure vulnerability policy",
)
application_policy = application_manifest.get("policy")
if (
not isinstance(application_policy, Mapping)
or application_policy.get("sha256") != policy_record["sha256"]
):
raise BundleValidationError(
"infrastructure security policy differs from the application build policy"
)
try:
policy = load_policy(policy_path)
except (ReleaseValidationError, OSError) as exc:
raise BundleValidationError(
f"infrastructure vulnerability policy is invalid: {exc}"
) from exc
database_path, database_record, raw_database_record = _extended_file_record(
root,
value.get("database"),
extra_fields={"validated_at", "repository_transport"},
label="infrastructure Trivy database metadata",
)
try:
validated_at = parse_rfc3339(
raw_database_record.get("validated_at"),
label="infrastructure database.validated_at",
)
database_summary = validate_trivy_db_metadata(
load_json(database_path, label="infrastructure Trivy database metadata"),
policy,
at=validated_at,
)
except (ReleaseValidationError, BundleValidationError) as exc:
raise BundleValidationError(
f"infrastructure Trivy database evidence is invalid: {exc}"
) from exc
database_transport = _validate_database_transport(
raw_database_record.get("repository_transport")
)
return (
policy,
{
"scanner": "trivy",
"scanner_version": REQUIRED_TOOL_VERSIONS["trivy"],
"policy": policy_record,
"database": {
**database_record,
"validated_at": raw_database_record["validated_at"],
"repository_transport": database_transport,
"summary": database_summary,
},
},
load_json(database_path, label="infrastructure Trivy database metadata"),
)
def _safe_tar_name(value: str, *, label: str) -> str:
if "\\" in value or "\x00" in value:
raise BundleValidationError(f"{label} contains an unsafe member path")
@@ -727,39 +875,49 @@ def _validate_sbom(
}
def _validate_trivy_scan(
def _validate_vulnerability_scan(
path: Path,
*,
role: str,
scan_kind: str,
expected_config_digest: str,
expected_artifact_basename: str,
policy: dict[str, Any],
evaluated_at: datetime,
) -> dict[str, Any]:
payload = load_json(path, label=f"{role} {scan_kind}")
if payload.get("SchemaVersion") != 2 or payload.get("ArtifactType") != "container_image":
raise BundleValidationError(f"{role} {scan_kind} must be a Trivy container-image report")
metadata = payload.get("Metadata")
if not isinstance(metadata, dict) or metadata.get("ImageID") != expected_config_digest:
raise BundleValidationError(f"{role} {scan_kind} config image ID mismatch")
results = payload.get("Results")
if not isinstance(results, list) or not results:
raise BundleValidationError(f"{role} {scan_kind} result inventory is missing")
finding_key = "Vulnerabilities" if scan_kind == "vulnerability scan" else "Licenses"
finding_count = 0
for result in results:
if not isinstance(result, dict):
raise BundleValidationError(f"{role} {scan_kind} result is invalid")
findings = result.get(finding_key, [])
if findings is None:
findings = []
if not isinstance(findings, list) or any(not isinstance(item, dict) for item in findings):
raise BundleValidationError(f"{role} {scan_kind} findings are invalid")
finding_count += len(findings)
return {
"scanner": "trivy",
"config_digest": expected_config_digest,
"result_count": len(results),
"finding_count": finding_count,
}
payload = load_json(path, label=f"{role} vulnerability scan")
try:
summary = validate_trivy_scan(
payload,
policy,
expected_config_digest=expected_config_digest,
expected_artifact_basename=expected_artifact_basename,
role=role,
at=evaluated_at,
)
return {**summary, "config_digest": expected_config_digest}
except ReleaseValidationError as exc:
raise BundleValidationError(
f"{role} vulnerability gate failed: {exc}"
) from exc
def _validate_license_scan(
path: Path,
*,
role: str,
expected_config_digest: str,
expected_artifact_basename: str,
) -> dict[str, Any]:
payload = load_json(path, label=f"{role} license scan")
try:
summary = validate_trivy_license_scan(
payload,
expected_config_digest=expected_config_digest,
expected_artifact_basename=expected_artifact_basename,
)
except ReleaseValidationError as exc:
raise BundleValidationError(f"{role} license gate failed: {exc}") from exc
return {**summary, "config_digest": expected_config_digest}
def _validate_application_manifest(root: Path, payload: Mapping[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]:
@@ -816,6 +974,15 @@ def build_bundle(
if source_set.get("schema_version") != SOURCE_SET_SCHEMA:
raise BundleValidationError("unsupported infrastructure source-set schema")
_embedded_hash(source_set, "source_set_payload_sha256", label="infrastructure source set")
if set(source_set) != {
"schema_version",
"candidate",
"target",
"security_gate",
"images",
"source_set_payload_sha256",
}:
raise BundleValidationError("infrastructure source-set fields are missing or unknown")
if source_set.get("target") != TARGET:
raise BundleValidationError("infrastructure source target must be linux/amd64")
source_candidate = source_set.get("candidate")
@@ -826,16 +993,32 @@ def build_bundle(
)
if app_candidate.get("commit") != source_candidate.get("commit") or app_candidate.get("tag") != source_candidate.get("tag"):
raise BundleValidationError("application and infrastructure candidate bindings differ")
policy, security_gate, database_payload = _validate_security_gate(
root,
source_set.get("security_gate"),
application_manifest=application_manifest,
)
raw_images = source_set.get("images")
if not isinstance(raw_images, list) or len(raw_images) != len(INFRASTRUCTURE_SPECS):
raise BundleValidationError("infrastructure source set must contain exactly seven images")
seen_archives: set[str] = set()
seen_configs: set[str] = set()
seen_security_artifacts: set[str] = set()
seen_security_artifacts: set[str] = {
security_gate["policy"]["path"],
security_gate["database"]["path"],
}
seen_registry_blobs: set[str] = set()
infrastructure_images: list[dict[str, Any]] = []
import_images: list[dict[str, Any]] = []
aggregate_vulnerability_counts = {
bucket: {
severity: 0
for severity in policy["vulnerability_scan"]["thresholds"]
}
for bucket in ("total", "exempted", "blocking")
}
applied_waiver_ids: set[str] = set()
overrides = {item["environment_key"]: item["config_digest"] for item in application_images}
for sequence, (raw, (role, environment_key, services)) in enumerate(
zip(raw_images, INFRASTRUCTURE_SPECS, strict=True), start=1
@@ -937,13 +1120,26 @@ def build_bundle(
sbom_path, sbom_record = _file_record(
root, raw.get("sbom"), label=f"{role} SBOM"
)
vulnerability_path, vulnerability_record = _file_record(
(
vulnerability_path,
vulnerability_record,
raw_vulnerability_record,
) = _extended_file_record(
root,
raw.get("vulnerability_scan"),
extra_fields={
"scanner",
"scanner_version",
"waiver_evaluated_at",
"passed",
},
label=f"{role} vulnerability scan",
)
license_path, license_record = _file_record(
root, raw.get("license_scan"), label=f"{role} license scan"
license_path, license_record, raw_license_record = _extended_file_record(
root,
raw.get("license_scan"),
extra_fields={"scanner", "scanner_version"},
label=f"{role} license scan",
)
security_paths = {
sbom_record["path"],
@@ -958,18 +1154,50 @@ def build_bundle(
role=role,
expected_manifest_digest=manifest_digest,
)
vulnerability_summary = _validate_trivy_scan(
if (
raw_vulnerability_record.get("scanner") != "trivy"
or raw_vulnerability_record.get("scanner_version")
!= REQUIRED_TOOL_VERSIONS["trivy"]
or raw_vulnerability_record.get("passed") is not True
or raw_license_record.get("scanner") != "trivy"
or raw_license_record.get("scanner_version")
!= REQUIRED_TOOL_VERSIONS["trivy"]
):
raise BundleValidationError(f"{role} scanner identity or pass state drifted")
try:
scan_evaluated_at = parse_rfc3339(
raw_vulnerability_record.get("waiver_evaluated_at"),
label=f"{role} vulnerability waiver_evaluated_at",
)
validate_trivy_db_metadata(
database_payload,
policy,
at=scan_evaluated_at,
)
except ReleaseValidationError as exc:
raise BundleValidationError(
f"{role} vulnerability scan database/freshness evidence is invalid: {exc}"
) from exc
vulnerability_summary = _validate_vulnerability_scan(
vulnerability_path,
role=role,
scan_kind="vulnerability scan",
expected_config_digest=config_digest,
expected_artifact_basename=archive_path.name,
policy=policy,
evaluated_at=scan_evaluated_at,
)
license_summary = _validate_trivy_scan(
license_summary = _validate_license_scan(
license_path,
role=role,
scan_kind="license scan",
expected_config_digest=config_digest,
expected_artifact_basename=archive_path.name,
)
for bucket in aggregate_vulnerability_counts:
for severity in aggregate_vulnerability_counts[bucket]:
aggregate_vulnerability_counts[bucket][severity] += (
vulnerability_summary["counts"][bucket][severity]
)
applied_waiver_ids.update(vulnerability_summary["waiver_ids"])
image = {
"sequence": sequence,
"role": role,
@@ -990,9 +1218,21 @@ def build_bundle(
"archive_import_validation": "requires_controlled_docker_load_and_inspect_receipt",
"sbom": sbom_record,
"sbom_summary": sbom_summary,
"vulnerability_scan": vulnerability_record,
"vulnerability_scan": {
**vulnerability_record,
"scanner": "trivy",
"scanner_version": REQUIRED_TOOL_VERSIONS["trivy"],
"waiver_evaluated_at": raw_vulnerability_record[
"waiver_evaluated_at"
],
"passed": True,
},
"vulnerability_scan_summary": vulnerability_summary,
"license_scan": license_record,
"license_scan": {
**license_record,
"scanner": "trivy",
"scanner_version": REQUIRED_TOOL_VERSIONS["trivy"],
},
"license_scan_summary": license_summary,
}
infrastructure_images.append(image)
@@ -1043,12 +1283,23 @@ def build_bundle(
if tuple(overrides) != ALL_IMAGE_KEYS or set(overrides) != set(ALL_IMAGE_KEYS):
raise BundleValidationError("offline override map does not cover exactly all eleven images")
security_gate.update(
{
"thresholds": policy["vulnerability_scan"]["thresholds"],
"exemptions": policy["vulnerability_scan"]["exemptions"],
"vulnerability_counts": aggregate_vulnerability_counts,
"waiver_ids": sorted(applied_waiver_ids),
"passed": True,
"fail_closed": True,
}
)
unsigned: dict[str, Any] = {
"schema_version": BUNDLE_SCHEMA,
"candidate": {"commit": source_candidate["commit"], "tag": source_candidate["tag"]},
"target": TARGET,
"application_images": application_images,
"infrastructure_images": infrastructure_images,
"security_gate": security_gate,
"offline_compose_overrides": overrides,
"all_release_image_keys": list(ALL_IMAGE_KEYS),
"docker_import_plan": {
@@ -49,6 +49,8 @@ _WINDOWS_GIT_EXECUTABLE = (
)
)
RELEASE_CONTROL_PATHS = (
".github/workflows/build-test-quality-gate.yml",
".github/workflows/security-quality-gate.yml",
"yuqei-infra/scripts/build-release-candidate-manifest.py",
"yuqei-infra/scripts/build-release-worktree-ownership.py",
"yuqei-infra/compose/docker-compose.production.offline.yml",
@@ -80,6 +80,8 @@ RUNTIME_COMPONENTS = {
"yuqei-ai-platform/services/ai-platform-api/migrations/",
),
"deployment": (
".github/workflows/build-test-quality-gate.yml",
".github/workflows/security-quality-gate.yml",
"yuqei-infra/docker/",
"yuqei-infra/compose/",
"yuqei-infra/nginx/",
@@ -107,6 +109,8 @@ RUNTIME_COMPONENTS = {
),
}
REQUIRED_WORKTREE_ANCHORS = (
".github/workflows/build-test-quality-gate.yml",
".github/workflows/security-quality-gate.yml",
"package.json",
"package-lock.json",
"yuqei-ai-sdk-python/pyproject.toml",
@@ -485,6 +485,7 @@ def _case(
"environment_class": "production",
"host_id": host_id,
"host_identity_sha256": "c" * 64,
"docker_daemon_identity_sha256": "d" * 64,
"compose_project_name": "yuqei-contract",
},
"consistency_set_id": consistency_set_id,
@@ -7,6 +7,7 @@ import io
import json
import sys
import tarfile
from datetime import datetime, timezone
from pathlib import Path
import pytest
@@ -22,6 +23,11 @@ assert SPEC is not None and SPEC.loader is not None
bundle = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = bundle
SPEC.loader.exec_module(bundle)
POLICY_PATH = (
MODULE_PATH.parents[1]
/ "release"
/ "linux-release-security-policy.v1.json"
)
def _sha(path: Path) -> str:
@@ -177,7 +183,7 @@ def _make_security_artifacts(
license_path = root / "scans" / f"{role}.license.json"
common = {
"SchemaVersion": 2,
"ArtifactName": role,
"ArtifactName": f"{role}.docker.tar",
"ArtifactType": "container_image",
"Metadata": {"ImageID": config_digest},
}
@@ -204,21 +210,82 @@ def _make_security_artifacts(
"Target": role,
"Class": "license",
"Type": "image",
"Licenses": [{"Name": "Apache-2.0", "Category": "notice"}],
"Licenses": [
{
"Name": "Apache-2.0",
"Category": "notice",
"Severity": "LOW",
"PkgName": f"fixture-{role}",
"FilePath": f"usr/share/doc/{role}/copyright",
}
],
}
],
},
)
return (
_record(sbom_path, root),
_record(vulnerability_path, root),
_record(license_path, root),
{
**_record(vulnerability_path, root),
"scanner": "trivy",
"scanner_version": bundle.REQUIRED_TOOL_VERSIONS["trivy"],
"waiver_evaluated_at": "2026-09-27T00:05:00Z",
"passed": True,
},
{
**_record(license_path, root),
"scanner": "trivy",
"scanner_version": bundle.REQUIRED_TOOL_VERSIONS["trivy"],
},
)
def _make_security_gate(root: Path) -> dict[str, object]:
policy_path = root / "policy/linux-release-security-policy.v1.json"
policy_path.parent.mkdir(parents=True, exist_ok=True)
policy_path.write_bytes(POLICY_PATH.read_bytes())
database_path = root / "security/trivy-db-metadata.json"
_write_canonical(
database_path,
{
"Version": bundle.REQUIRED_TOOL_VERSIONS["trivy"],
"VulnerabilityDB": {
"Version": 2,
"UpdatedAt": "2026-09-27T00:00:00Z",
"NextUpdate": "2026-09-27T06:00:00Z",
"DownloadedAt": "2026-09-27T00:01:00Z",
},
},
)
return {
"policy": _record(policy_path, root),
"database": {
**_record(database_path, root),
"validated_at": "2026-09-27T00:02:00Z",
"repository_transport": {
"reference": bundle.TRIVY_DB_REPOSITORY_TRANSPORT["reference"],
"transport": bundle.TRIVY_DB_REPOSITORY_TRANSPORT["transport"],
"schema_version": bundle.TRIVY_DB_REPOSITORY_TRANSPORT[
"schema_version"
],
"resolved_digest": "sha256:" + "a" * 64,
"media_type": bundle.TRIVY_DB_REPOSITORY_TRANSPORT[
"allowed_media_types"
][0],
"stable_during_download": True,
"before_download_inspect_sha256": "b" * 64,
"after_download_inspect_sha256": "c" * 64,
},
},
"scanner": "trivy",
"scanner_version": bundle.REQUIRED_TOOL_VERSIONS["trivy"],
}
def _fixture(root: Path) -> tuple[dict[str, object], dict[str, object]]:
commit = "1" * 40
tag = "v2-test-1"
security_gate = _make_security_gate(root)
app_images: list[dict[str, object]] = []
for index, (role, environment_key) in enumerate(bundle.APPLICATION_SPECS, start=1):
repo_tag = f"yuqei/{role}:{commit}"
@@ -240,6 +307,7 @@ def _fixture(root: Path) -> tuple[dict[str, object], dict[str, object]]:
{
"schema_version": bundle.APPLICATION_MANIFEST_SCHEMA,
"candidate": {"commit": commit, "tag": tag},
"policy": security_gate["policy"],
"images": app_images,
},
"manifest_payload_sha256",
@@ -303,6 +371,7 @@ def _fixture(root: Path) -> tuple[dict[str, object], dict[str, object]]:
"schema_version": bundle.SOURCE_SET_SCHEMA,
"candidate": {"commit": commit, "tag": tag},
"target": bundle.TARGET,
"security_gate": security_gate,
"images": infra_images,
},
"source_set_payload_sha256",
@@ -605,8 +674,8 @@ def test_every_infrastructure_security_artifact_is_required(
("field", "message"),
(
("sbom", "SBOM root does not bind"),
("vulnerability_scan", "vulnerability scan config image ID mismatch"),
("license_scan", "license scan config image ID mismatch"),
("vulnerability_scan", "vulnerability gate failed"),
("license_scan", "license gate failed"),
),
)
def test_security_artifact_image_identity_drift_is_rejected(
@@ -623,7 +692,162 @@ def test_security_artifact_image_identity_drift_is_rejected(
else:
payload["Metadata"]["ImageID"] = "sha256:" + "f" * 64
_write_canonical(path, payload)
source["images"][0][field] = _record(path, tmp_path)
refreshed = _record(path, tmp_path)
source["images"][0][field] = {
**record,
**refreshed,
}
source = _rehash_source(source)
with pytest.raises(bundle.BundleValidationError, match=message):
bundle.build_bundle(
artifact_root=tmp_path,
source_set=source,
application_manifest=application,
)
def test_infrastructure_vulnerability_gate_rejects_high_finding_even_when_claimed_pass(
tmp_path: Path,
) -> None:
source, application = _fixture(tmp_path)
record = source["images"][0]["vulnerability_scan"]
path = tmp_path / record["path"]
payload = json.loads(path.read_text(encoding="utf-8"))
payload["Results"][0]["Vulnerabilities"] = [
{
"VulnerabilityID": "CVE-2099-0001",
"PkgName": "unwaived-package",
"InstalledVersion": "1.0.0",
"FixedVersion": "1.0.1",
"Severity": "HIGH",
}
]
_write_canonical(path, payload)
source["images"][0]["vulnerability_scan"] = {
**record,
**_record(path, tmp_path),
}
source = _rehash_source(source)
with pytest.raises(bundle.BundleValidationError, match="vulnerability gate failed"):
bundle.build_bundle(
artifact_root=tmp_path,
source_set=source,
application_manifest=application,
)
@pytest.mark.parametrize(
("mutation", "message"),
(
(
lambda source: source["security_gate"].update(
{"scanner_version": "0.0.0"}
),
"scanner identity is not pinned",
),
(
lambda source: source["images"][0]["vulnerability_scan"].update(
{"waiver_evaluated_at": "2026-09-29T00:05:00Z"}
),
"database/freshness evidence is invalid",
),
),
)
def test_infrastructure_security_gate_rejects_scanner_or_database_drift(
tmp_path: Path,
mutation,
message: str,
) -> None:
source, application = _fixture(tmp_path)
mutation(source)
source = _rehash_source(source)
with pytest.raises(bundle.BundleValidationError, match=message):
bundle.build_bundle(
artifact_root=tmp_path,
source_set=source,
application_manifest=application,
)
def test_infrastructure_license_inventory_cannot_be_empty(tmp_path: Path) -> None:
source, application = _fixture(tmp_path)
record = source["images"][0]["license_scan"]
path = tmp_path / record["path"]
payload = json.loads(path.read_text(encoding="utf-8"))
payload["Results"][0]["Licenses"] = []
_write_canonical(path, payload)
source["images"][0]["license_scan"] = {
**record,
**_record(path, tmp_path),
}
source = _rehash_source(source)
with pytest.raises(bundle.BundleValidationError, match="license inventory is empty"):
bundle.build_bundle(
artifact_root=tmp_path,
source_set=source,
application_manifest=application,
)
@pytest.mark.parametrize(
("mutation", "message"),
(
(
lambda payload: payload.update({"SchemaVersion": 3}),
"SchemaVersion must be exactly 2",
),
(
lambda payload: payload["Results"][0].pop("Target"),
"Target is missing",
),
(
lambda payload: payload["Results"][0].pop("Class"),
"Class is missing",
),
(
lambda payload: payload["Results"][0].pop("Licenses"),
"lacks an explicit Licenses inventory",
),
(
lambda payload: payload["Results"][0]["Licenses"][0].pop(
"Category"
),
"has no category",
),
(
lambda payload: payload["Results"][0]["Licenses"][0].pop(
"Severity"
),
"has no severity",
),
(
lambda payload: (
payload["Results"][0]["Licenses"][0].pop("PkgName"),
payload["Results"][0]["Licenses"][0].pop("FilePath"),
),
"has no package or file identity",
),
),
)
def test_infrastructure_license_report_shape_fails_closed(
tmp_path: Path,
mutation,
message: str,
) -> None:
source, application = _fixture(tmp_path)
record = source["images"][0]["license_scan"]
path = tmp_path / record["path"]
payload = json.loads(path.read_text(encoding="utf-8"))
mutation(payload)
_write_canonical(path, payload)
source["images"][0]["license_scan"] = {
**record,
**_record(path, tmp_path),
}
source = _rehash_source(source)
with pytest.raises(bundle.BundleValidationError, match=message):
@@ -316,6 +316,15 @@ def test_worktree_classifier_has_explicit_release_ownership(path: str, category:
def test_manifest_is_deterministic_and_summarizes_complete_committed_graph(tmp_path: Path) -> None:
release_control_workflows = (
".github/workflows/build-test-quality-gate.yml",
".github/workflows/security-quality-gate.yml",
)
for release_control_workflow in release_control_workflows:
assert release_control_workflow in manifest.RELEASE_CONTROL_PATHS
assert release_control_workflow in ownership.REQUIRED_WORKTREE_ANCHORS
assert release_control_workflow in ownership.RUNTIME_COMPONENTS["deployment"]
assert ownership.classify_path(release_control_workflow)[0] == "release_source_include"
release_control_tests = (
"yuqei-infra/tests/test_production_compose_preflight.py",
"yuqei-infra/tests/test_production_compose_merge_gate.py",
@@ -90,6 +90,7 @@ _STRICT_BUILD_TEMPLATE: dict[str, Any] | None = None
_REAL_INFRA_TOOL: Any | None = None
_INFRA_SYNTHETIC_APPLICATION: dict[str, Any] | None = None
_FIXTURE_MIT_TEXT = b"MIT fixture license text\n"
_FIXTURE_APACHE_TEXT = b"Apache-2.0 fixture license text\n"
_FIXTURE_GPL_TEXT = b"GPL-2.0-only fixture license text\n"
_FIXTURE_SOURCE_BYTES = b"def copyleft_fixture() -> str:\n return 'source'\n"
@@ -151,6 +152,13 @@ def _fixture_third_party_trust_baseline(third_tool: Any) -> dict[str, Any]:
"aliases": [],
**record(_FIXTURE_GPL_TEXT),
},
{
"id": "Apache-2.0",
"kind": "license",
"spdx_expression": "Apache-2.0",
"aliases": [],
**record(_FIXTURE_APACHE_TEXT),
},
],
"source_materials": [
{
@@ -359,10 +367,6 @@ def _add_v2_release_artifacts(
"release_package_infra_fixture_helpers",
"yuqei-infra/tests/test_infrastructure_image_bundle.py",
)
third_helper = _load_test_helper(
"release_package_third_party_fixture_helpers",
"yuqei-infra/tests/test_third_party_materials.py",
)
real_infra_tool = release_package._load_infrastructure_bundle_module()
third_tool = release_package._load_third_party_materials_module()
third_tool.REPOSITORY_TRUST_BASELINE_PATH = (
@@ -373,6 +377,7 @@ def _add_v2_release_artifacts(
commit = installable["candidate"]["commit"]
tag = installable["candidate"]["tag"]
v2_root.mkdir(parents=True)
infrastructure_security_gate = infra_helper._make_security_gate(v2_root)
for image in installable["images"]:
relative = Path(image["docker_archive"]["path"])
destination = v2_root / relative
@@ -412,6 +417,7 @@ def _add_v2_release_artifacts(
_INFRA_SYNTHETIC_APPLICATION = {
"schema_version": real_infra_tool.APPLICATION_MANIFEST_SCHEMA,
"candidate": {"commit": commit, "tag": tag},
"policy": infrastructure_security_gate["policy"],
"images": synthetic_images,
}
_INFRA_SYNTHETIC_APPLICATION["manifest_payload_sha256"] = (
@@ -492,6 +498,7 @@ def _add_v2_release_artifacts(
"schema_version": infra_tool.SOURCE_SET_SCHEMA,
"candidate": {"commit": commit, "tag": tag},
"target": infra_tool.TARGET,
"security_gate": infrastructure_security_gate,
"images": infra_images,
}
source_set["source_set_payload_sha256"] = infra_tool.canonical_sha256(source_set)
@@ -510,8 +517,10 @@ def _add_v2_release_artifacts(
text_dir = materials_root / "texts"
text_dir.mkdir()
mit = text_dir / "MIT.txt"
apache = text_dir / "Apache-2.0.txt"
gpl = text_dir / "GPL-2.0-only.txt"
mit.write_bytes(_FIXTURE_MIT_TEXT)
apache.write_bytes(_FIXTURE_APACHE_TEXT)
gpl.write_bytes(_FIXTURE_GPL_TEXT)
spdx_licenses = materials_root / "spdx/licenses.json"
spdx_exceptions = materials_root / "spdx/exceptions.json"
@@ -535,6 +544,8 @@ def _add_v2_release_artifacts(
"role": item["role"],
"manifest_digest": item["manifest_digest"],
"config_digest": item["config_digest"],
"formal_sbom_path": bundle_root / item["sbom"]["path"],
"formal_license_path": bundle_root / item["license_scan"]["path"],
}
for item in installable["images"]
] + [
@@ -542,8 +553,10 @@ def _add_v2_release_artifacts(
"role": item["role"],
"manifest_digest": item["manifest_digest"],
"config_digest": item["config_digest"],
"formal_sbom_path": v2_root / item["sbom"]["path"],
"formal_license_path": v2_root / item["license_scan"]["path"],
}
for item in infra_images
for item in infrastructure_bundle["infrastructure_images"]
]
candidate_images = []
occurrences = []
@@ -551,9 +564,10 @@ def _add_v2_release_artifacts(
role = item["role"]
sbom_path = materials_root / f"reports/{role}.spdx.json"
trivy_path = materials_root / f"reports/{role}.trivy-license.json"
_write_json(sbom_path, third_helper._sbom(role, item["manifest_digest"]))
trivy_payload = third_helper._trivy(role, item["config_digest"])
_write_json(trivy_path, trivy_payload)
sbom_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(item["formal_sbom_path"], sbom_path)
shutil.copy2(item["formal_license_path"], trivy_path)
trivy_payload = json.loads(trivy_path.read_text(encoding="utf-8"))
trivy_record = _record_for(trivy_path, materials_root)
parsed, _summary = third_tool._parse_trivy(
trivy_payload,
@@ -564,19 +578,23 @@ def _add_v2_release_artifacts(
occurrences.extend(parsed)
candidate_images.append(
{
**item,
"role": role,
"manifest_digest": item["manifest_digest"],
"config_digest": item["config_digest"],
"sbom": _record_for(sbom_path, materials_root),
"trivy_license": trivy_record,
}
)
coverage = []
for occurrence in occurrences:
license_name = occurrence["license_name"]
assert license_name in {"Apache-2.0", "GPL-2.0-only", "MIT"}
restricted = occurrence["category"] == "restricted"
coverage.append(
{
"occurrence_id": occurrence["occurrence_id"],
"spdx_expression": occurrence["license_name"],
"license_text_ids": ["GPL-2.0-only" if restricted else "MIT"],
"spdx_expression": license_name,
"license_text_ids": [license_name],
"source_material_ids": ["fixture-copyleft-source"] if restricted else [],
}
)
@@ -610,6 +628,10 @@ def _add_v2_release_artifacts(
"files": [source_file_record],
}
source_archive = materials_root / "sources/fixture-copyleft.tar.gz"
third_helper = _load_test_helper(
"release_package_third_party_fixture_helpers",
"yuqei-infra/tests/test_third_party_materials.py",
)
third_helper._source_tar(
source_archive,
source_manifest=source_archive_manifest,
@@ -637,6 +659,13 @@ def _add_v2_release_artifacts(
"aliases": [],
"file": _record_for(mit, materials_root),
},
{
"id": "Apache-2.0",
"kind": "license",
"spdx_expression": "Apache-2.0",
"aliases": [],
"file": _record_for(apache, materials_root),
},
{
"id": "GPL-2.0-only",
"kind": "license",
@@ -767,9 +796,65 @@ def _strict_build_template(tmp_path: Path) -> dict[str, Any]:
return _STRICT_BUILD_TEMPLATE
helpers = _load_linux_build_test_helpers()
original_source_fixture = helpers._fixture_source
original_license_scan = helpers._license_scan
original_spdx = helpers._spdx
def release_package_license_scan(
artifact_name: str,
*,
image_id: str,
licenses: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
payload = original_license_scan(
artifact_name,
image_id=image_id,
licenses=licenses,
)
if artifact_name == "contract-api.docker.tar" and licenses is None:
payload["Results"][0]["Licenses"].append(
{
"Severity": "HIGH",
"Category": "restricted",
"PkgName": "fixture-copyleft",
"FilePath": "usr/share/doc/fixture-copyleft/copyright",
"Name": "GPL-2.0-only",
"Text": "",
"Confidence": 1,
"Link": "https://spdx.org/licenses/GPL-2.0-only.html",
}
)
return payload
def release_package_spdx(
role: str,
manifest_digest: str | None = None,
) -> dict[str, Any]:
payload = original_spdx(role, manifest_digest)
if role == "contract-api":
payload["packages"].append(
{
"SPDXID": "SPDXRef-Package-fixture-copyleft",
"name": "fixture-copyleft",
"versionInfo": "1.0.0",
"licenseDeclared": "GPL-2.0-only",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": (
"pkg:generic/fixture-copyleft@1.0.0"
),
}
],
}
)
return payload
helpers._fixture_source = lambda path: _extend_source_fixture_with_release_validators(
helpers, original_source_fixture, path
)
helpers._license_scan = release_package_license_scan
helpers._spdx = release_package_spdx
try:
template_source = tmp_path.parent / "release-package-strict-template-source"
# A failed first construction must not poison every later case in the
@@ -777,8 +862,19 @@ def _strict_build_template(tmp_path: Path) -> dict[str, Any]:
if template_source.exists():
shutil.rmtree(template_source)
bundle = helpers._bundle(template_source)
receipt_unsigned = dict(bundle["receipt"])
receipt_unsigned.pop("receipt_payload_sha256")
receipt_unsigned["security"]["license_inventory"]["license_count"] = 5
bundle["receipt"] = helpers.common.with_payload_digest(
receipt_unsigned, "receipt_payload_sha256"
)
bundle["receipt_path"].write_bytes(
helpers.common.canonical_json_bytes(bundle["receipt"])
)
finally:
helpers._fixture_source = original_source_fixture
helpers._license_scan = original_license_scan
helpers._spdx = original_spdx
receipt_sha256 = _sha(bundle["receipt_path"])
installable = bundle["root"] / "installable-release-manifest.json"
assembler = release_package._load_installable_validator_module()
@@ -919,6 +1015,9 @@ def _write_strict_production_env(
{
"COMPOSE_PROJECT_NAME": "yuqei-prod",
"RELEASE_ID": "release-2",
"YUQEI_RELEASE_ENVIRONMENT_ID": "production-cn-1",
"YUQEI_RELEASE_ENVIRONMENT_CLASS": "production",
"YUQEI_RELEASE_HOST_ID": "production-host-1",
**{key: value["reference"] for key, value in application_images.items()},
**{key: value["reference"] for key, value in infrastructure_images.items()},
"AI_PLATFORM_RELEASE_IMAGE_DIGEST": application_images[
@@ -1622,6 +1721,50 @@ def test_v2_source_bound_third_party_trust_baseline_drift_is_rejected(
)
def test_v2_self_consistent_third_party_report_drift_from_formal_build_is_rejected(
tmp_path: Path,
) -> None:
fixture = _make_package(tmp_path)
materials_input = json.loads(
fixture.third_party_input_file.read_text(encoding="utf-8")
)
first_image = materials_input["candidate"]["images"][0]
sbom_record = first_image["sbom"]
sbom_path = fixture.third_party_input_file.parent / sbom_record["path"]
sbom_payload = json.loads(sbom_path.read_text(encoding="utf-8"))
sbom_payload["comment"] = "self-consistent alternate report"
_write_json(sbom_path, sbom_payload)
sbom_record.update(
{
"sha256": _sha(sbom_path),
"size_bytes": sbom_path.stat().st_size,
}
)
_write_json(fixture.third_party_input_file, materials_input)
materials_module = release_package._load_third_party_materials_module()
output_root = fixture.third_party_manifest_file.parent
shutil.rmtree(output_root)
materials_module.build(fixture.third_party_input_file, output_root)
verification = materials_module.verify(
fixture.third_party_input_file,
output_root,
"internal-hosted",
)
_write_json(fixture.third_party_receipt_file, verification)
_refresh_outer_manifests(fixture)
with pytest.raises(
release_package.ReleasePackageError,
match="different raw image report than the formal build gate",
):
release_package.validate_release_package(
fixture.root,
expected_package_sha256=fixture.expected_sha256,
repo_root=fixture.repo,
)
def test_v2_predecessor_recovery_preserves_legacy_plan_semantics(tmp_path: Path) -> None:
fixture = _make_package(tmp_path)
_use_v2_predecessor_recovery(fixture)
@@ -1813,6 +1956,8 @@ def test_v2_first_release_readiness_is_revalidated_before_injected_apply(
raw_inputs["trusted_root_public_key"].write_bytes(b"r" * 32)
identity = json.loads(fixture.identity_file.read_text(encoding="utf-8"))
tls = preplan["tls_material_binding"]
snapshot_planner = release_package._load_snapshot_restore_planner_module()
daemon_identity = preplan["docker_daemon_identity"]
readiness = {
"schema_version": "yuqei.first-release-recovery-readiness-receipt.v1",
"result": release_package.RECOVERY_READINESS_RESULT,
@@ -1825,7 +1970,17 @@ def test_v2_first_release_readiness_is_revalidated_before_injected_apply(
"source_commit": identity["source_commit"],
"image_identity_sha256": _sha(fixture.identity_file),
},
"target": {"compose_project_name": "yuqei-prod"},
"target": {
"environment_id": "production-cn-1",
"environment_class": "production",
"host_id": "production-host-1",
"host_identity_sha256": snapshot_planner.runtime_host_identity_sha256(
"production-host-1",
daemon_identity,
),
"docker_daemon_identity_sha256": daemon_identity["identity_sha256"],
"compose_project_name": "yuqei-prod",
},
"release_environment_and_tls_bindings": {
"production_env_sha256": preplan["env_file_sha256"],
"tls_certificate_sha256": tls["certificate_sha256"],
@@ -1836,9 +1991,7 @@ def test_v2_first_release_readiness_is_revalidated_before_injected_apply(
receipt_file = tmp_path / "first-release-readiness.json"
_write_json(receipt_file, readiness)
receipt_sha256 = _sha(receipt_file)
planner_file = Path(
release_package._load_snapshot_restore_planner_module().__file__
)
planner_file = Path(snapshot_planner.__file__)
validator_calls: list[dict[str, Any]] = []
def validate_install_readiness(**kwargs: Any) -> dict[str, Any]:
@@ -1861,6 +2014,9 @@ def test_v2_first_release_readiness_is_revalidated_before_injected_apply(
SimpleNamespace(
__file__=str(planner_file),
validate_install_readiness=validate_install_readiness,
runtime_host_identity_sha256=(
snapshot_planner.runtime_host_identity_sha256
),
),
)
compose_calls: list[list[str]] = []
@@ -1898,6 +2054,76 @@ def test_v2_first_release_readiness_is_revalidated_before_injected_apply(
assert list((state / "receipts").iterdir())
@pytest.mark.parametrize(
"case",
("uat_to_production", "host_a_to_b", "daemon_a_to_b"),
)
def test_first_release_readiness_rejects_runtime_target_replay(case: str) -> None:
daemon_identity = {
"docker_cli": release_package.DOCKER_CLI,
"docker_host": release_package.LOCAL_DOCKER_HOST,
"daemon_id": "daemon-a",
"daemon_name": "host-a",
"docker_root_dir": "/var/lib/docker",
"server_version": "26.1.3",
}
daemon_identity["identity_sha256"] = release_package.canonical_sha256(
daemon_identity
)
planner = release_package._load_snapshot_restore_planner_module()
runtime_env = {
"YUQEI_RELEASE_ENVIRONMENT_ID": "production-cn-1",
"YUQEI_RELEASE_ENVIRONMENT_CLASS": "production",
"YUQEI_RELEASE_HOST_ID": "host-a",
}
target = {
"environment_id": runtime_env["YUQEI_RELEASE_ENVIRONMENT_ID"],
"environment_class": runtime_env["YUQEI_RELEASE_ENVIRONMENT_CLASS"],
"host_id": runtime_env["YUQEI_RELEASE_HOST_ID"],
"host_identity_sha256": planner.runtime_host_identity_sha256(
runtime_env["YUQEI_RELEASE_HOST_ID"],
daemon_identity,
),
"docker_daemon_identity_sha256": daemon_identity["identity_sha256"],
"compose_project_name": "yuqei-prod",
}
if case == "uat_to_production":
target["environment_class"] = "isolated_uat"
elif case == "host_a_to_b":
target["host_id"] = "host-b"
target["host_identity_sha256"] = planner.runtime_host_identity_sha256(
"host-b",
daemon_identity,
)
else:
target["docker_daemon_identity_sha256"] = "f" * 64
with pytest.raises(
release_package.ReleasePackageError,
match="environment/host/daemon",
):
release_package._validate_readiness_runtime_binding(
readiness={
"target": target,
"release_environment_and_tls_bindings": {
"production_env_sha256": "1" * 64,
"tls_certificate_sha256": "2" * 64,
"tls_private_key_sha256": "3" * 64,
"tls_trusted_ca_sha256": "4" * 64,
},
},
env_values=runtime_env,
env_sha256="1" * 64,
daemon_identity=daemon_identity,
tls_binding={
"certificate_sha256": "2" * 64,
"private_key_sha256": "3" * 64,
"trusted_ca_sha256": "4" * 64,
},
project_name="yuqei-prod",
)
@pytest.mark.parametrize(
"case",
(
@@ -2268,6 +2494,58 @@ def test_apply_is_idempotent_and_conflicting_reuse_fails_closed(tmp_path: Path)
assert len(calls) == 1
def test_injected_success_receipt_cannot_be_replayed_as_builtin_apply(
tmp_path: Path,
monkeypatch,
) -> None:
fixture = _make_package(tmp_path)
state = tmp_path / "state"
key = "mode-bound-operation-001"
injected = _execute(
fixture,
operation="install",
env_file=fixture.env_file,
apply=True,
confirmation=release_package.CONFIRMATIONS["install"],
idempotency_key=key,
state_dir=state,
runner=lambda command, **_kwargs: subprocess.CompletedProcess(
command,
0,
stdout=b"injected",
stderr=b"",
),
now=datetime(2026, 9, 20, 4, 0, tzinfo=UTC),
)
assert injected.payload["execution_mode"] == "test_injected_runner"
builtin_runner = InspectionRunner()
monkeypatch.setattr(release_package, "_fixed_subprocess_runner", builtin_runner)
monkeypatch.setattr(release_package, "_real_apply_blockers", lambda *_args, **_kwargs: [])
with pytest.raises(release_package.ReleasePackageError, match="execution mode"):
release_package.execute(
package_dir=fixture.root,
expected_package_sha256=fixture.expected_sha256,
operation="install",
env_file=fixture.env_file,
apply=True,
confirmation=release_package.CONFIRMATIONS["install"],
idempotency_key=key,
state_dir=state,
repo_root=fixture.repo,
runner=builtin_runner,
inspection_runner=builtin_runner,
post_apply_checker=release_package._builtin_post_apply_checker,
now=datetime(2026, 9, 20, 4, 0, tzinfo=UTC),
)
assert not any(
command[3:6] == ["image", "load", "--input"]
for command, _kwargs in builtin_runner.calls
)
def test_exclusive_lock_blocks_concurrent_apply(tmp_path: Path) -> None:
fixture = _make_package(tmp_path)
state = tmp_path / "state"
@@ -2435,6 +2713,124 @@ def test_real_apply_entrypoint_stays_closed_while_external_release_proofs_are_mi
assert not state.exists()
def test_builtin_post_apply_checker_binds_every_install_service_image(
tmp_path: Path,
monkeypatch,
) -> None:
image_ids_by_service: dict[str, str] = {}
docker_import_plan: list[dict[str, Any]] = []
for index, (role, _env_key, services) in enumerate(
release_package.RELEASE_IMPORT_ROLE_ENV_SERVICES,
start=1,
):
image_id = f"sha256:{index:064x}"
docker_import_plan.append(
{
"role": role,
"expected_loaded_image_id": image_id,
"services": list(services),
}
)
for service in services:
image_ids_by_service[service] = image_id
expected_services = list(release_package.INSTALL_SERVICES)
assert "ai-platform-prompt-assets-import" in expected_services
assert set(expected_services).issubset(image_ids_by_service)
container_services = {
f"container-{service}": service for service in expected_services
}
poll_count = {"ps": 0}
sleeps: list[int] = []
def runner(command: list[str], **_kwargs: Any):
if "ps" in command:
poll_count["ps"] += 1
inventory = [
{"Service": service, "ID": f"container-{service}"}
for service in expected_services
]
return subprocess.CompletedProcess(
command,
0,
stdout=json.dumps(inventory),
stderr="",
)
container_id = command[-1]
service = container_services[container_id]
one_shot = service in release_package.ONE_SHOT_INSTALL_SERVICES
detail = {
"Image": image_ids_by_service[service],
"Config": {
"Labels": {
"com.docker.compose.project": "yuqei-release-test",
"com.docker.compose.service": service,
},
"Env": (
["AI_PLATFORM_RELEASE_ID=release-test"]
if service == "ai-platform-api"
else []
),
},
"State": {
"Status": "exited" if one_shot else "running",
"ExitCode": 0,
"Health": (
None
if one_shot
else {
"Status": (
"starting"
if service == "ai-platform-api" and poll_count["ps"] == 1
else "healthy"
)
}
),
},
}
return subprocess.CompletedProcess(
command,
0,
stdout=json.dumps(detail),
stderr="",
)
monkeypatch.setattr(release_package, "_fixed_subprocess_runner", runner)
monkeypatch.setattr(release_package.time, "sleep", sleeps.append)
compose_file = tmp_path / "docker-compose.yml"
result = release_package._builtin_post_apply_checker(
plan={
"operation": "install",
"target_release_id": "release-test",
"compose_project_name": "yuqei-release-test",
"deployed_service_set": expected_services,
"docker_import_plan": docker_import_plan,
},
command_argv=[
release_package.DOCKER_CLI,
"--host",
release_package.LOCAL_DOCKER_HOST,
"compose",
"--project-name",
"yuqei-release-test",
"-f",
str(compose_file),
"up",
"--detach",
],
runtime_environment={},
daemon_identity={"identity_sha256": "a" * 64},
)
assert result["status"] == "verified"
assert poll_count["ps"] == 2
assert sleeps == [release_package.POST_APPLY_POLL_INTERVAL_SECONDS]
assert result["container_count"] == len(expected_services)
assert {
item["service"]: item["image_id"] for item in result["services"]
} == {service: image_ids_by_service[service] for service in expected_services}
def test_ambient_docker_and_compose_controls_cannot_redirect_apply(
tmp_path: Path,
monkeypatch,
@@ -1223,3 +1223,76 @@ def test_plan_entrypoint_rejects_relative_release_paths_before_validation(
trusted_root_public_key_file=(tmp_path / "root.pub").resolve(),
env_file=(tmp_path / "production.env").resolve(),
)
def test_plan_entrypoint_uses_each_package_own_verified_validator(
tmp_path: Path,
monkeypatch,
) -> None:
current_package = (tmp_path / "current-package").resolve()
predecessor_package = (tmp_path / "predecessor-package").resolve()
current_repo = (tmp_path / "current-repo").resolve()
predecessor_repo = (tmp_path / "predecessor-repo").resolve()
bootstrap_calls: list[tuple[Path, str, Path]] = []
load_calls: list[tuple[object, Path]] = []
validation_calls: list[tuple[Path, Path, str]] = []
def bootstrap(*, current_package_dir, current_package_sha256, repo_root):
bootstrap_calls.append(
(current_package_dir, current_package_sha256, repo_root)
)
marker = repo_root.name
return {"marker": marker.encode()}, {"marker": marker}
def load(verified_sources, *, verified_repo_root=planner.REPO_ROOT):
marker = verified_sources["marker"].decode()
load_calls.append((verified_sources, verified_repo_root))
def validate(package_dir, *, expected_package_sha256, repo_root):
validation_calls.append(
(package_dir, repo_root, expected_package_sha256)
)
return SimpleNamespace(marker=marker)
return SimpleNamespace(validate_release_package=validate)
class StopAfterPackageValidation(RuntimeError):
pass
monkeypatch.setattr(planner, "_bootstrap_verified_tool_sources", bootstrap)
monkeypatch.setattr(planner, "_load_release_package_module", load)
monkeypatch.setattr(
planner,
"_read_pinned_json",
lambda *_args, **_kwargs: (_ for _ in ()).throw(
StopAfterPackageValidation()
),
)
with pytest.raises(StopAfterPackageValidation):
planner.plan_from_files(
current_package_dir=current_package,
current_package_sha256="a" * 64,
predecessor_package_dir=predecessor_package,
predecessor_package_sha256="b" * 64,
predecessor_repo_root=predecessor_repo,
evidence_file=(tmp_path / "evidence.json").resolve(),
evidence_sha256="c" * 64,
keyring_file=(tmp_path / "keyring.json").resolve(),
keyring_sha256="d" * 64,
compose_gate_evidence_file=(tmp_path / "compose-gate.json").resolve(),
compose_gate_evidence_sha256="e" * 64,
trusted_root_public_key_file=(tmp_path / "root.pub").resolve(),
env_file=(tmp_path / "production.env").resolve(),
repo_root=current_repo,
)
assert bootstrap_calls == [
(current_package, "a" * 64, current_repo),
(predecessor_package, "b" * 64, predecessor_repo),
]
assert [item[1] for item in load_calls] == [current_repo, predecessor_repo]
assert validation_calls == [
(current_package, current_repo, "a" * 64),
(predecessor_package, predecessor_repo, "b" * 64),
]