GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
199 lines
8.2 KiB
Python
199 lines
8.2 KiB
Python
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
def read(path: str) -> str:
|
|
return (ROOT / path).read_text(encoding="utf-8")
|
|
|
|
|
|
def test_python_ci_lock_is_hashed_linux_311_and_excludes_ai() -> None:
|
|
for lock_path in (
|
|
"backend/requirements-runtime.lock",
|
|
"backend/requirements-ci.lock",
|
|
):
|
|
lock = read(lock_path)
|
|
assert "pip-compile with Python 3.11" in lock
|
|
assert "# geointel-input-sha256: " in lock
|
|
assert "--generate-hashes" in lock
|
|
assert "\ntorch==" not in lock
|
|
assert "\nultralytics==" not in lock
|
|
|
|
|
|
def test_lock_generator_uses_pinned_linux_runtime_and_verifies_policy() -> None:
|
|
generator = read("scripts/generate_python_lock.sh")
|
|
|
|
assert "python:3.11-bookworm@sha256:" in generator
|
|
assert 'PIP_TOOLS_VERSION="7.5.3"' in generator
|
|
assert "--extra gis" in generator
|
|
assert "--extra dev" in generator
|
|
assert "requirements-runtime.lock" in generator
|
|
assert "requirements-ci.lock" in generator
|
|
assert "--generate-hashes" in generator
|
|
assert "verify_python_lock.py --stamp" in generator
|
|
|
|
|
|
def test_ci_runs_complete_release_and_supply_chain_gates() -> None:
|
|
for workflow_path, context in (
|
|
(".github/workflows/release-gates.yml", "github.sha"),
|
|
(".gitea/workflows/release-gates.yml", "gitea.sha"),
|
|
):
|
|
workflow = read(workflow_path)
|
|
assert "backend/requirements-ci.lock" in workflow
|
|
assert "scripts/verify_python_lock.py" in workflow
|
|
assert "scripts/run_readiness_check.sh" in workflow
|
|
assert "python -m alembic upgrade head --sql" in workflow
|
|
assert "docker compose config" in workflow
|
|
assert "pip-audit==2.10.1" in workflow
|
|
assert "audit_python_dependencies.sh" in workflow
|
|
assert "npm audit --audit-level=high" in workflow
|
|
assert "GEOINTEL_INSTALL_AI=true" in workflow
|
|
assert "geointel-ci:$RELEASE_SHA-ai" in workflow
|
|
assert "artifacts/image-id.txt" in workflow
|
|
assert 'scan_container_image.sh "$IMAGE_ID"' in workflow
|
|
assert "generate_container_sbom.sh" in workflow
|
|
assert "scan_container_image.sh" in workflow
|
|
assert "GEOINTEL_IMAGE_ARCHIVE: artifacts/geointel-image.tar" in workflow
|
|
assert "Remove temporary image archive" in workflow
|
|
assert context in workflow
|
|
|
|
assert (
|
|
"actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02"
|
|
in read(".github/workflows/release-gates.yml")
|
|
)
|
|
assert (
|
|
"actions/upload-artifact@c6a3b2bd78b3985e4b2f15397fec357f0fd808de"
|
|
in read(".gitea/workflows/release-gates.yml")
|
|
)
|
|
|
|
|
|
def test_gitea_production_deploy_depends_on_every_release_gate() -> None:
|
|
release = read(".gitea/workflows/release-gates.yml")
|
|
legacy_deploy = ROOT / ".gitea" / "workflows" / "unraid-deploy.yml"
|
|
|
|
assert "pull_request:" in release
|
|
assert "needs: [quality, dependency-audit, container]" in release
|
|
assert "gitea.event_name == 'push'" in release
|
|
assert "gitea.ref == 'refs/heads/main'" in release
|
|
assert "/opt/gitea-deploy/deploy.py deploy" in release
|
|
assert not legacy_deploy.exists()
|
|
assert "workflow_dispatch:" in release
|
|
|
|
|
|
def test_release_workflows_pin_third_party_actions_to_reviewed_commits() -> None:
|
|
shared = (
|
|
"actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683",
|
|
"actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065",
|
|
"actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020",
|
|
)
|
|
for path in (".gitea/workflows/release-gates.yml", ".github/workflows/release-gates.yml"):
|
|
workflow = read(path)
|
|
for action in shared:
|
|
assert action in workflow
|
|
|
|
assert (
|
|
"actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02"
|
|
in read(".github/workflows/release-gates.yml")
|
|
)
|
|
assert (
|
|
"actions/upload-artifact@c6a3b2bd78b3985e4b2f15397fec357f0fd808de"
|
|
in read(".gitea/workflows/release-gates.yml")
|
|
)
|
|
|
|
|
|
def test_managed_validation_targets_the_actual_backend_and_frontend_projects() -> None:
|
|
workflow = read(".gitea/workflows/managed-validation.yml")
|
|
|
|
assert "backend/requirements-ci.lock" in workflow
|
|
assert "frontend/package-lock.json" in workflow
|
|
assert "python -m pytest -W error::DeprecationWarning" in workflow
|
|
assert "cd frontend && npm run test:unit" in workflow
|
|
assert "python -m ruff check backend scripts tests" in workflow
|
|
assert "python scripts/verify_repository_layout.py" in workflow
|
|
assert "[[ -f pyproject.toml" not in workflow
|
|
|
|
|
|
def test_scanner_images_are_versioned_and_digest_pinned() -> None:
|
|
sbom = read("scripts/generate_container_sbom.sh")
|
|
scan = read("scripts/scan_container_image.sh")
|
|
|
|
assert "anchore/syft:v1.44.0@sha256:" in sbom
|
|
assert "--user 0:0" in sbom
|
|
assert 'docker save "$IMAGE_ID"' in sbom
|
|
assert '"docker-archive:$WORKDIR/$IMAGE_ARCHIVE"' in sbom
|
|
assert 'SYFT_PARALLELISM=${SYFT_PARALLELISM:-1}' in sbom
|
|
assert 'GOMEMLIMIT=${SYFT_GOMEMLIMIT:-4GiB}' in sbom
|
|
assert 'GOGC=${SYFT_GOGC:-25}' in sbom
|
|
assert "--select-catalogers=-binary" in sbom
|
|
assert '--volumes-from "$HOSTNAME"' in sbom
|
|
assert 'ARCHIVE_ID_FILE="${IMAGE_ARCHIVE}.image-id"' in sbom
|
|
assert "/var/run/docker.sock" not in sbom
|
|
assert "aquasec/trivy:0.70.0@sha256:" in scan
|
|
assert "--severity HIGH,CRITICAL" in scan
|
|
assert "--ignore-unfixed" in scan
|
|
assert "--timeout 20m" in scan
|
|
assert "--scanners vuln" in scan
|
|
assert 'docker save "$IMAGE_ID"' in scan
|
|
assert '--input "$WORKDIR/$IMAGE_ARCHIVE"' in scan
|
|
assert '--volumes-from "$HOSTNAME"' in scan
|
|
assert '--ignorefile "$CONTAINER_IGNORE_FILE"' in scan
|
|
assert "/var/run/docker.sock" not in scan
|
|
assert "--skip-files /usr/local/bin/gosu" in scan
|
|
assert "final filesystem replaces it with the audited setpriv shell wrapper" in scan
|
|
assert "geointel-container-vulnerabilities.json" in scan
|
|
|
|
|
|
def test_readiness_guards_lock_and_supply_chain_entrypoints() -> None:
|
|
readiness = read("scripts/run_readiness_check.sh")
|
|
|
|
assert "scripts/verify_python_lock.py" in readiness
|
|
assert "scripts/verify_security_exceptions.py" in readiness
|
|
for path in (
|
|
"scripts/generate_python_lock.sh",
|
|
"scripts/generate_container_sbom.sh",
|
|
"scripts/scan_container_image.sh",
|
|
"scripts/audit_python_dependencies.sh",
|
|
):
|
|
assert f"bash -n {path}" in readiness
|
|
|
|
|
|
def test_python_audit_policy_has_no_active_exceptions_and_keeps_full_evidence() -> None:
|
|
policy = read("security/pip-audit-exceptions.json")
|
|
audit_script = read("scripts/audit_python_dependencies.sh")
|
|
|
|
assert '"schema_version": 1' in policy
|
|
assert '"advisories": []' in policy
|
|
assert "pip-audit-full.json" in audit_script
|
|
assert "pip-audit-policy.json" in audit_script
|
|
assert "--ignore-vuln" in audit_script
|
|
assert "verify_security_exceptions.py" in audit_script
|
|
|
|
|
|
def test_release_image_uses_locked_non_ai_dependencies_and_npm_ci() -> None:
|
|
dockerfile = read("deploy/unraid/Dockerfile.all-in-one")
|
|
|
|
assert "RUN npm ci" in dockerfile
|
|
assert "COPY backend/requirements-runtime.lock /app/" in dockerfile
|
|
assert "COPY backend/requirements-ai-linux.lock /app/" in dockerfile
|
|
assert "COPY backend/requirements-build-tools.lock /app/" in dockerfile
|
|
assert "pip install --no-cache-dir --require-hashes -r requirements-runtime.lock" in dockerfile
|
|
assert "-r requirements-ai-linux.lock" in dockerfile
|
|
assert "-r requirements-build-tools.lock" in dockerfile
|
|
assert "--require-hashes" in dockerfile
|
|
assert "ultralytics==8.4.99 --hash=sha256:" in read("backend/requirements-ai-linux.lock")
|
|
assert "torch==2.11.0+cu128 --hash=sha256:" in read("backend/requirements-ai-linux.lock")
|
|
assert "COPY deploy/unraid/gosu-setpriv /usr/local/bin/gosu" in dockerfile
|
|
assert "&& pip check" in dockerfile
|
|
|
|
|
|
def test_gosu_compatibility_wrapper_uses_exec_and_setpriv() -> None:
|
|
wrapper = read("deploy/unraid/gosu-setpriv")
|
|
readiness = read("scripts/run_readiness_check.sh")
|
|
|
|
assert "exec setpriv" in wrapper
|
|
assert '--reuid="$target_user"' in wrapper
|
|
assert '--regid="$target_user"' in wrapper
|
|
assert "--init-groups" in wrapper
|
|
assert "bash -n deploy/unraid/gosu-setpriv" in readiness
|