Files
trex-core/tests/release/test_image_contracts.py
T
Hermes Agent 46904621ff
Publish verified container image / verify (push) Canceled after 0s
Publish verified container image / publish (push) Canceled after 0s
fix(ci): use preserved helper artifact path
2026-08-10 15:30:15 +09:00

167 lines
6.2 KiB
Python

from __future__ import annotations
from pathlib import Path
import yaml
REPO = Path(__file__).resolve().parents[2]
WORKFLOW_PATH = REPO / ".github" / "workflows" / "publish-image.yml"
def _workflow():
return yaml.load(WORKFLOW_PATH.read_text(), Loader=yaml.BaseLoader)
def _step_index(steps, name):
return next(index for index, step in enumerate(steps) if step.get("name") == name)
def test_workflow_triggers_only_main_and_v_tags():
workflow = _workflow()
assert set(workflow["on"]) == {"push"}
assert workflow["on"]["push"] == {"branches": ["main"], "tags": ["v*"]}
def test_publish_is_gated_and_has_the_only_package_write_permission():
workflow = _workflow()
verify = workflow["jobs"]["verify"]
publish = workflow["jobs"]["publish"]
assert publish["needs"] == "verify"
assert publish["permissions"]["packages"] == "write"
assert "packages" not in workflow["permissions"]
assert "packages" not in verify["permissions"]
def test_verify_canary_build_smoke_export_scan_checksum_upload_contract():
steps = _workflow()["jobs"]["verify"]["steps"]
names = [step["name"] for step in steps]
assert names.count("Create excluded synthetic canary") == 1
assert names.count("Build source image once") == 1
assert names.count("Scan all canary surfaces") == 1
canary = steps[_step_index(steps, "Create excluded synthetic canary")]["run"]
assert 'Path(".env.ci-canary")' in canary
assert "TREX_SECRET_CANARY_DO_NOT_SHIP" in canary
scan = steps[_step_index(steps, "Scan all canary surfaces")]["run"]
assert "image_release.py scan" in scan
status_contract = '''>canary-scan.stdout 2>canary-scan.stderr
scan_status=$?
set -e
python scripts/image_release.py assert-clean-output \\
canary-scan.stdout canary-scan.stderr
exit "${scan_status}"'''
assert status_contract in scan
assert scan.count("scan_status=$?") == 1
assert scan.rstrip().endswith('exit "${scan_status}"')
assert _step_index(steps, "Run repository lint") < _step_index(steps, "Build source image once")
assert _step_index(steps, "Run repository tests") < _step_index(steps, "Build source image once")
ordered = [
"Create excluded synthetic canary",
"Build source image once",
"Smoke built image",
"Export tested image",
"Scan all canary surfaces",
"Record tested archive checksum",
"Upload immutable tested image artifact",
]
assert [_step_index(steps, name) for name in ordered] == sorted(
_step_index(steps, name) for name in ordered
)
def test_repository_checks_use_the_locked_uv_environment():
steps = _workflow()["jobs"]["verify"]["steps"]
install = steps[_step_index(steps, "Install repository checks")]["run"]
lint = steps[_step_index(steps, "Run repository lint")]["run"]
tests = steps[_step_index(steps, "Run repository tests")]["run"]
assert 'python -m pip install "uv==0.11.14"' in install
assert "uv sync --extra dev --locked" in install
assert lint == "uv run ruff check ."
assert tests == "uv run pytest"
assert 'pip install ".[dev]"' not in install
def test_tested_image_id_is_recorded_after_smoke_and_transferred_to_publish():
workflow = _workflow()
verify_steps = workflow["jobs"]["verify"]["steps"]
publish_steps = workflow["jobs"]["publish"]["steps"]
smoke_index = _step_index(verify_steps, "Smoke built image")
identity_index = _step_index(verify_steps, "Record tested image ID")
export_index = _step_index(verify_steps, "Export tested image")
assert smoke_index < identity_index < export_index
identity = verify_steps[identity_index]["run"]
assert "image_release.py record-image-id" in identity
assert "--output trex-core-image.id" in identity
upload = verify_steps[_step_index(verify_steps, "Upload immutable tested image artifact")]["with"]
assert "trex-core-image.id" in upload["path"].splitlines()
publish = publish_steps[_step_index(publish_steps, "Verify, load, and publish without rebuilding")][
"run"
]
assert "--image-id tested-image/trex-core-image.id" in publish
def test_publish_job_has_only_download_then_verified_publish_helper():
steps = _workflow()["jobs"]["publish"]["steps"]
assert len(steps) == 2
assert "run" not in steps[0]
assert steps[0]["uses"].startswith("actions/download-artifact@")
run_steps = [step for step in steps if "run" in step]
assert run_steps == [steps[1]]
expected_run = '''python tested-image/scripts/image_release.py publish \\
--archive tested-image/trex-core-image.tar \\
--checksum tested-image/trex-core-image.tar.sha256 \\
--image-id tested-image/trex-core-image.id \\
--local-image "trex-core-ci:${GITHUB_SHA}" \\
--commit "${GITHUB_SHA}" \\
--ref "${GITHUB_REF}"'''
assert steps[1]["run"].strip() == expected_run
rendered = "\n".join(str(step) for step in steps).lower()
for forbidden in (
"actions/checkout",
"docker login",
"docker push",
"docker build",
"buildx",
):
assert forbidden not in rendered
def test_workflow_uses_full_sha_and_contains_no_latest_tag():
text = WORKFLOW_PATH.read_text()
assert "${GITHUB_SHA}" in text
assert "github.sha" in text
assert "--provenance=false" in text
assert ":latest" not in text.lower()
def test_docker_context_contract():
dockerfile = (REPO / "Dockerfile").read_text()
copy_lines = [line.strip() for line in dockerfile.splitlines() if line.strip().startswith("COPY ")]
assert copy_lines == ["COPY pyproject.toml ./", "COPY trex_core/ ./trex_core/"]
assert "org.opencontainers.image.revision" in dockerfile
assert "python:3.12" in dockerfile
ignored = set((REPO / ".dockerignore").read_text().splitlines())
assert {".git", ".env", ".env.*", "config/", ".proofline/"} <= ignored
def test_operator_documentation_preserves_rollback_boundaries():
operations = (REPO / "docs" / "operations.md").read_text()
assert "read-only volume" in operations
assert "runtime environment" in operations
assert "sha256:<64-hex>" in operations
assert "이전" in operations and "digest" in operations
assert "`trex-core-reset`" in operations
assert "자동 rollback 절차에 절대로 포함하지 않는다" in operations