101 lines
4.9 KiB
Python
101 lines
4.9 KiB
Python
from __future__ import annotations
|
|
|
|
import subprocess
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
REPO = Path(__file__).resolve().parents[2]
|
|
PUBLISH = REPO / ".github/workflows/publish-image.yml"
|
|
VERIFY = REPO / ".github/workflows/verify.yml"
|
|
|
|
def test_lock_exists_and_is_fresh():
|
|
assert (REPO/"uv.lock").is_file()
|
|
subprocess.run(["uv","lock","--check"],cwd=REPO,check=True,capture_output=True,text=True)
|
|
|
|
def test_branch_verification_has_no_registry_write_authority_or_path():
|
|
text=VERIFY.read_text(); assert "branches: [main]" in text and "pull_request:" in text
|
|
for forbidden in ("packages: write","docker login","docker push","REGISTRY_TOKEN","image_release.py publish"):
|
|
assert forbidden not in text
|
|
assert "uv sync --extra dev --locked" in text and "uv run pytest -q" in text
|
|
|
|
def test_tag_publication_is_strict_single_build_handoff_without_latest():
|
|
text=PUBLISH.read_text(); verify,publish=text.split("\n publish:",1)
|
|
assert 'tags:\n - "v*.*.*"' in text and "validate-event" in text and "validate-checkout" in text
|
|
assert "ref: ${{ github.sha }}" in text and text.count("docker build ")==1
|
|
assert "docker build" not in publish and "packages: write" not in verify and "packages: write" in publish
|
|
assert "tested-image/scripts/image_release.py publish" in publish and ":latest" not in text
|
|
assert "--platform linux/amd64" in verify and '--build-arg "RELEASE_VERSION=${GITHUB_REF_NAME}"' in verify
|
|
for name in ("portfolio-hub-image.tar","portfolio-hub-image.tar.sha256","portfolio-hub-image.id","migration-inventory.txt"):
|
|
assert name in text
|
|
|
|
def test_dockerfile_python311_locked_non_root_explicit_uvicorn_and_resources():
|
|
text=(REPO/"Dockerfile").read_text()
|
|
assert text.count("FROM python:3.11-slim@sha256:")==2 and "uv sync --locked --no-dev --no-editable" in text
|
|
assert "FROM ghcr.io/astral-sh/uv:0.11.14@sha256:" in text
|
|
assert text.startswith("# syntax=docker/dockerfile:1@sha256:")
|
|
assert "USER 10001:10001" in text and 'CMD ["uvicorn", "app.main:app"' in text
|
|
assert "COPY app/ ./app/" in text and "COPY ." not in text
|
|
assert "org.opencontainers.image.source" in text and "org.opencontainers.image.revision" in text
|
|
assert "org.opencontainers.image.version" in text and "ARG RELEASE_VERSION" in text
|
|
|
|
|
|
def test_workflow_actions_are_commit_pinned_and_verify_version_is_project_derived():
|
|
for workflow in (PUBLISH, VERIFY):
|
|
for line in workflow.read_text().splitlines():
|
|
if "uses: actions/" in line:
|
|
revision = line.split("@", 1)[1].split()[0]
|
|
assert len(revision) == 40 and all(char in "0123456789abcdef" for char in revision)
|
|
verify = VERIFY.read_text()
|
|
assert "tomllib" in verify and 'RELEASE_VERSION=${RELEASE_VERSION}' in verify
|
|
assert 'RELEASE_VERSION=v0.1.3' not in verify
|
|
|
|
|
|
def test_release_version_and_migration_contract_has_current_head_0031():
|
|
project = (REPO / "pyproject.toml").read_text()
|
|
config = (REPO / "app/core/config.py").read_text()
|
|
migrations = sorted(
|
|
path.name
|
|
for path in (REPO / "app/db/alembic/versions").glob("*.py")
|
|
if path.name != "__init__.py"
|
|
)
|
|
assert 'version = "0.3.0"' in project
|
|
assert 'app_version: str = "0.3.0"' in config
|
|
assert len(migrations) == 31
|
|
assert migrations[-1].startswith("20260913_0031_")
|
|
|
|
def test_dockerignore_is_deny_by_default_required_inputs_only():
|
|
lines=(REPO/".dockerignore").read_text().splitlines(); assert lines[0]=="**"
|
|
assert [x for x in lines if x.startswith("!")] == ["!pyproject.toml","!uv.lock","!README.md","!app/","!app/**"]
|
|
|
|
def test_source_and_wheel_migration_inventory_are_exact_and_non_empty(tmp_path):
|
|
source=sorted(x.name for x in (REPO/"app/db/alembic/versions").glob("*.py") if x.name!="__init__.py")
|
|
assert source
|
|
subprocess.run(["uv","build","--wheel","--out-dir",str(tmp_path)],cwd=REPO,check=True,capture_output=True,text=True)
|
|
wheel=next(tmp_path.glob("*.whl"))
|
|
with zipfile.ZipFile(wheel) as archive:
|
|
installed=sorted(Path(x).name for x in archive.namelist() if "/db/alembic/versions/" in x and x.endswith(".py") and not x.endswith("/__init__.py"))
|
|
assert installed == source
|
|
|
|
def test_operations_separate_publication_deployment_migration_and_db_rollback():
|
|
text = (REPO / "docs/container-operations.md").read_text()
|
|
for value in (
|
|
"@sha256:<64-lowercase-hex-digest>",
|
|
"pfhub migrate-db",
|
|
"Publication is not deployment",
|
|
"Image rollback is **not** database rollback",
|
|
"read-only root filesystem",
|
|
"10001:10001",
|
|
"PortfolioHub v0.3.0 container contract",
|
|
"v0.2.1 includes migration `0030`",
|
|
"trex-web v0.2.3 displays `strategy_nick`",
|
|
):
|
|
assert value in text
|
|
for forbidden in (
|
|
"docker run -d",
|
|
"docker compose up",
|
|
"kubectl apply",
|
|
"helm install",
|
|
"systemctl restart",
|
|
):
|
|
assert forbidden not in text
|