328 lines
13 KiB
Python
328 lines
13 KiB
Python
"""Release packaging and provenance guarantees added in v1.2.1.
|
|
|
|
Two defects reached production before these existed, and both were invisible to every gate the
|
|
project had. Neither was a code bug: the source was correct in both cases and the *artifact* was
|
|
wrong, which is precisely the class of failure a unit test looking at source cannot see.
|
|
|
|
**The console could not reach its own API.** Vite inlines ``VITE_*`` at build time, but
|
|
``release_build.py`` never passed ``VITE_API_BASE_URL``, so every release image compiled the
|
|
Dockerfile's development default — ``http://localhost:8000`` — into an immutable bundle, and the
|
|
nginx CSP, derived from the same argument, hardcoded the same wrong origin. The frontend test suite
|
|
passed because jsdom never performs a cross-origin fetch; the release gate passed because it
|
|
inspects labels and checksums, not bundle contents. It surfaced only when a human opened the
|
|
production console.
|
|
|
|
**The Node Agent's identity drifted from its tag.** ``docker-compose.node-agent.yml`` declared a
|
|
``build:`` block with no ``args:``, so a Compose-built agent got version ``0.0.0`` and empty
|
|
revision/created labels while still being tagged from ``MODELFORGE_VERSION``. Production ran an
|
|
image tagged ``1.1.1`` whose contents were ``1.2.0`` and whose OCI labels were blank.
|
|
|
|
These tests read the actual release inputs — the Dockerfiles, the Compose projections and the
|
|
release builder — so the same two failures cannot ship again without failing here first.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
from modelforge_api.domain.release import PRODUCT_VERSION
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
RELEASE_BUILD = ROOT / "scripts" / "release_build.py"
|
|
FRONTEND_DOCKERFILE = ROOT / "frontend" / "Dockerfile"
|
|
NODE_AGENT_DOCKERFILE = ROOT / "node-agent" / "Dockerfile"
|
|
|
|
#: Every image the release builder publishes. The Runtime Worker is a first-class private runtime
|
|
#: artifact even though it exposes no public listener.
|
|
RELEASE_IMAGES = (
|
|
"modelforge-api",
|
|
"modelforge-web",
|
|
"modelforge-node-agent",
|
|
"modelforge-runtime-worker",
|
|
)
|
|
|
|
#: The OCI fields a release image must carry for an operator to trace it back to a commit.
|
|
REQUIRED_OCI_LABELS = (
|
|
"org.opencontainers.image.title",
|
|
"org.opencontainers.image.version",
|
|
"org.opencontainers.image.revision",
|
|
"org.opencontainers.image.created",
|
|
"org.opencontainers.image.source",
|
|
)
|
|
|
|
|
|
def _release_build_source() -> str:
|
|
return RELEASE_BUILD.read_text(encoding="utf-8")
|
|
|
|
|
|
def _compose(name: str) -> dict:
|
|
return yaml.safe_load((ROOT / name).read_text(encoding="utf-8"))
|
|
|
|
|
|
# --------------------------------------------------------------- console API origin (defect A)
|
|
|
|
|
|
def test_the_release_builder_requires_an_explicit_public_api_origin() -> None:
|
|
"""No origin, no release. The default that shipped v1.2.0 was a development convenience."""
|
|
|
|
source = _release_build_source()
|
|
assert "--public-api-origin" in source
|
|
assert "MODELFORGE_PUBLIC_API_ORIGIN" in source
|
|
assert "def normalise_public_api_origin" in source
|
|
|
|
|
|
def test_a_release_build_without_an_api_origin_fails_closed() -> None:
|
|
import importlib.util
|
|
|
|
spec = importlib.util.spec_from_file_location("modelforge_release_build", RELEASE_BUILD)
|
|
assert spec and spec.loader
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
|
|
for empty in ("", " "):
|
|
with pytest.raises(SystemExit) as raised:
|
|
module.normalise_public_api_origin(empty)
|
|
assert "public API origin" in str(raised.value)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"value",
|
|
[
|
|
"localhost:8000", # no scheme: ambiguous, and how the original default looked
|
|
"ftp://example.com",
|
|
"https://",
|
|
"https://example.com/api", # a path would be appended twice
|
|
"https://example.com?x=1",
|
|
],
|
|
)
|
|
def test_a_malformed_api_origin_is_refused(value: str) -> None:
|
|
import importlib.util
|
|
|
|
spec = importlib.util.spec_from_file_location("modelforge_release_build", RELEASE_BUILD)
|
|
assert spec and spec.loader
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
with pytest.raises(SystemExit):
|
|
module.normalise_public_api_origin(value)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("given", "expected"),
|
|
[
|
|
("https://modelforge.example.com", "https://modelforge.example.com"),
|
|
("http://192.0.2.10:18000/", "http://192.0.2.10:18000"),
|
|
(" https://example.com ", "https://example.com"),
|
|
],
|
|
)
|
|
def test_a_valid_api_origin_is_normalised(given: str, expected: str) -> None:
|
|
import importlib.util
|
|
|
|
spec = importlib.util.spec_from_file_location("modelforge_release_build", RELEASE_BUILD)
|
|
assert spec and spec.loader
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
assert module.normalise_public_api_origin(given) == expected
|
|
|
|
|
|
def test_the_api_origin_reaches_the_console_image_build() -> None:
|
|
"""The validated origin must actually be passed to the build that inlines it."""
|
|
|
|
source = _release_build_source()
|
|
assert "VITE_API_BASE_URL={public_api_origin}" in source
|
|
assert "ORIGIN_DEPENDENT_IMAGES" in source
|
|
assert "modelforge-web" in source
|
|
|
|
|
|
def test_the_release_manifest_records_the_console_api_origin() -> None:
|
|
"""An operator must be able to see the compiled-in origin without unpacking the image."""
|
|
|
|
assert '"public_api_origin"' in _release_build_source()
|
|
|
|
|
|
def test_the_console_csp_is_derived_from_the_same_origin_argument() -> None:
|
|
"""The bundle and the policy that protects it cannot be allowed to disagree."""
|
|
|
|
dockerfile = FRONTEND_DOCKERFILE.read_text(encoding="utf-8")
|
|
assert 'API_ORIGIN=$(printf \'%s\' "${VITE_API_BASE_URL}" | cut -d/ -f1-3)' in dockerfile
|
|
# The build fails rather than shipping a policy with an unsubstituted placeholder.
|
|
assert 'grep -q "connect-src \'self\' ${API_ORIGIN};"' in dockerfile
|
|
|
|
|
|
def test_the_console_csp_is_never_widened_to_a_wildcard() -> None:
|
|
template = (ROOT / "frontend" / "security-headers.inc.template").read_text(encoding="utf-8")
|
|
connect = [line for line in template.splitlines() if "connect-src" in line]
|
|
assert connect, "the template must define connect-src"
|
|
for line in connect:
|
|
assert "connect-src 'self' __API_ORIGIN__" in line
|
|
assert "*" not in line.split("connect-src", 1)[1].split(";")[0]
|
|
|
|
|
|
def test_local_development_keeps_its_convenient_default() -> None:
|
|
"""Fail-closed is a property of the release path, not of `docker compose up` on a laptop."""
|
|
|
|
dockerfile = FRONTEND_DOCKERFILE.read_text(encoding="utf-8")
|
|
assert "ARG VITE_API_BASE_URL=http://localhost:8000" in dockerfile
|
|
development = _compose("docker-compose.yml")["services"]["web"]["build"]["args"]
|
|
assert development["VITE_API_BASE_URL"] == "${VITE_API_BASE_URL:-http://localhost:8000}"
|
|
|
|
|
|
def test_the_production_overlay_still_demands_an_api_base_url() -> None:
|
|
production = _compose("docker-compose.production.yml")["services"]["web"]["build"]["args"]
|
|
assert production["VITE_API_BASE_URL"].startswith("${VITE_API_BASE_URL:?")
|
|
|
|
|
|
# ------------------------------------------------------------- image provenance (defect B)
|
|
|
|
|
|
def test_every_release_dockerfile_declares_the_release_identity_arguments() -> None:
|
|
for relative in (
|
|
"backend/Dockerfile",
|
|
"frontend/Dockerfile",
|
|
"node-agent/Dockerfile",
|
|
"runtime-worker/Dockerfile",
|
|
):
|
|
text = (ROOT / relative).read_text(encoding="utf-8")
|
|
for argument in ("MODELFORGE_VERSION", "MODELFORGE_COMMIT", "MODELFORGE_BUILT_AT"):
|
|
assert f"ARG {argument}" in text, f"{relative} does not accept {argument}"
|
|
|
|
|
|
def test_every_release_dockerfile_emits_the_required_oci_labels() -> None:
|
|
for relative in (
|
|
"backend/Dockerfile",
|
|
"frontend/Dockerfile",
|
|
"node-agent/Dockerfile",
|
|
"runtime-worker/Dockerfile",
|
|
):
|
|
text = (ROOT / relative).read_text(encoding="utf-8")
|
|
for label in REQUIRED_OCI_LABELS:
|
|
assert f'LABEL {label}=' in text, f"{relative} does not set {label}"
|
|
|
|
|
|
def test_the_oci_version_and_revision_come_from_the_release_arguments() -> None:
|
|
"""A label typed in by hand is a label that drifts."""
|
|
|
|
for relative in (
|
|
"backend/Dockerfile",
|
|
"frontend/Dockerfile",
|
|
"node-agent/Dockerfile",
|
|
"runtime-worker/Dockerfile",
|
|
):
|
|
text = (ROOT / relative).read_text(encoding="utf-8")
|
|
assert 'org.opencontainers.image.version="${MODELFORGE_VERSION}"' in text
|
|
assert 'org.opencontainers.image.revision="${MODELFORGE_COMMIT}"' in text
|
|
assert 'org.opencontainers.image.created="${MODELFORGE_BUILT_AT}"' in text
|
|
|
|
|
|
def test_the_node_agent_compose_build_passes_the_release_identity() -> None:
|
|
"""The exact gap that let production run a 1.1.1 tag containing 1.2.0."""
|
|
|
|
build = _compose("docker-compose.node-agent.yml")["services"]["node-agent"]["build"]
|
|
args = build.get("args")
|
|
assert args, "the node-agent build must pass release identity arguments"
|
|
assert args["MODELFORGE_VERSION"] == "${MODELFORGE_VERSION:-0.0.0}"
|
|
assert args["MODELFORGE_COMMIT"] == "${MODELFORGE_COMMIT:-}"
|
|
assert args["MODELFORGE_BUILT_AT"] == "${MODELFORGE_BUILT_AT:-}"
|
|
|
|
|
|
def test_every_composed_release_image_passes_the_release_identity() -> None:
|
|
"""Whatever builds a published image must stamp it; otherwise the tag is the only identity."""
|
|
|
|
for compose_file, service in (
|
|
("docker-compose.yml", "api"),
|
|
("docker-compose.yml", "web"),
|
|
("docker-compose.node-agent.yml", "node-agent"),
|
|
("docker-compose.runtime-worker.yml", "runtime-worker"),
|
|
):
|
|
build = _compose(compose_file)["services"][service].get("build")
|
|
assert build, f"{compose_file}:{service} has no build section"
|
|
args = build.get("args") or {}
|
|
assert "MODELFORGE_VERSION" in args, f"{compose_file}:{service} omits MODELFORGE_VERSION"
|
|
assert "MODELFORGE_COMMIT" in args, f"{compose_file}:{service} omits MODELFORGE_COMMIT"
|
|
|
|
|
|
def test_the_node_agent_image_reference_is_versioned_and_never_floating() -> None:
|
|
service = _compose("docker-compose.node-agent.yml")["services"]["node-agent"]
|
|
reference = service["image"]
|
|
assert "${MODELFORGE_NODE_AGENT_IMAGE:-" in reference
|
|
assert "${MODELFORGE_VERSION:-local}" in reference
|
|
assert ":latest" not in reference
|
|
|
|
|
|
def test_the_runtime_worker_image_reference_is_versioned_and_never_floating() -> None:
|
|
service = _compose("docker-compose.runtime-worker.yml")["services"]["runtime-worker"]
|
|
reference = service["image"]
|
|
assert "${MODELFORGE_RUNTIME_WORKER_IMAGE:-" in reference
|
|
assert "${MODELFORGE_VERSION:-local}" in reference
|
|
assert ":latest" not in reference
|
|
|
|
|
|
def test_the_agent_version_derives_from_the_repository_version_file() -> None:
|
|
"""Single-source versioning: VERSION drives every packaged manifest.
|
|
|
|
Read from the file rather than imported: the Node Agent is a separate distribution and is not
|
|
installed into the backend's environment, and a version test that silently skips when the
|
|
import fails would be worthless.
|
|
"""
|
|
|
|
text = (ROOT / "node-agent" / "src" / "modelforge_node_agent" / "__init__.py").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
match = re.search(r'__version__\s*=\s*"([^"]+)"', text)
|
|
assert match, "the Node Agent must declare __version__"
|
|
assert match.group(1) == PRODUCT_VERSION
|
|
assert (ROOT / "VERSION").read_text(encoding="utf-8").strip() == PRODUCT_VERSION
|
|
|
|
|
|
def test_the_runtime_worker_version_matches_the_release() -> None:
|
|
text = (ROOT / "runtime-worker" / "src" / "modelforge_runtime_worker" / "__init__.py").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
match = re.search(r'__version__\s*=\s*"([^"]+)"', text)
|
|
assert match and match.group(1) == PRODUCT_VERSION
|
|
|
|
|
|
def test_the_release_builder_publishes_exactly_the_expected_images() -> None:
|
|
source = _release_build_source()
|
|
for image in RELEASE_IMAGES:
|
|
assert f'"{image}"' in source
|
|
|
|
|
|
# ---------------------------------------------------------------- artifact reproducibility
|
|
|
|
|
|
def test_release_artifacts_are_written_with_unix_line_endings() -> None:
|
|
"""`sha256sum -c` treats a trailing CR as part of the filename and fails on every entry.
|
|
|
|
Found while verifying v1.2.0 on Windows: all four digests were correct and not one filename
|
|
could be read.
|
|
"""
|
|
|
|
source = _release_build_source()
|
|
assert source.count('newline="\\n"') >= 2
|
|
assert 'checksums.write_text("\\n".join(lines) + "\\n", encoding="utf-8", newline="\\n")' in (
|
|
source
|
|
)
|
|
|
|
|
|
def test_the_build_timestamp_feeds_the_reproducible_build_contract() -> None:
|
|
source = _release_build_source()
|
|
assert "SOURCE_DATE_EPOCH" in source
|
|
assert "def source_date_epoch" in source
|
|
|
|
|
|
def test_the_release_build_refuses_a_dirty_tree_by_default() -> None:
|
|
source = _release_build_source()
|
|
assert "refusing to build a release from a dirty working tree" in source
|
|
|
|
|
|
def test_the_release_build_records_no_host_specific_paths() -> None:
|
|
"""A manifest that names the machine that built it is not a portable release record."""
|
|
|
|
source = _release_build_source()
|
|
for accidental in ("C:\\\\", "/home/", "/Users/", "os.getcwd()"):
|
|
assert accidental not in source, f"release build references {accidental!r}"
|