Harden immutable release deployment
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-18 02:31:28 +02:00
parent 611ad0cd57
commit 5699006a5e
20 changed files with 477 additions and 151 deletions
+5 -3
View File
@@ -167,9 +167,11 @@ def test_frontend_uses_same_origin_api_proxy_by_default() -> None:
def test_nginx_runtime_allows_real_gis_upload_payloads() -> None:
frontend_nginx = (ROOT / "frontend" / "nginx.conf").read_text(encoding="utf-8")
all_in_one_nginx = (ROOT / "deploy" / "unraid" / "nginx-all-in-one.conf").read_text(encoding="utf-8")
start_script = (ROOT / "deploy" / "unraid" / "all-in-one-start.sh").read_text(encoding="utf-8")
assert "client_max_body_size 250m;" in frontend_nginx
assert "client_max_body_size 250m;" in all_in_one_nginx
assert "client_max_body_size __GEOINTEL_MAX_UPLOAD_MB__m;" in all_in_one_nginx
assert 'sed -i "s/__GEOINTEL_MAX_UPLOAD_MB__/${MAX_UPLOAD_MB}/g"' in start_script
def test_nginx_runtime_allows_long_ai_and_qa_requests() -> None:
@@ -314,11 +316,11 @@ def test_all_in_one_dockerfile_caches_dependencies_and_uses_cpu_torch_for_ai_run
def test_unraid_deploy_passes_ai_build_arg_and_yolo_runtime_env() -> None:
deploy_ps1 = (ROOT / "scripts" / "deploy_tower.ps1").read_text(encoding="utf-8")
deploy_sh = (ROOT / "scripts" / "deploy_tower.sh").read_text(encoding="utf-8")
release_script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
run_script = (ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(encoding="utf-8")
assert 'DEPLOY_GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-}"' in deploy_sh
assert 'docker build --build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI"' in deploy_sh
assert 'docker build --build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI"' in deploy_ps1
assert "--build-arg GEOINTEL_INSTALL_AI=" in release_script
assert "DEPLOY_GEOINTEL_INSTALL_AI" in deploy_ps1
assert "[string]$InstallAi" in deploy_ps1
@@ -0,0 +1,78 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_build_identity_does_not_invalidate_dependency_layers() -> None:
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
dependency_install = dockerfile.index("/usr/bin/python3.11 -m venv /opt/geointel/venv")
source_copy = dockerfile.index("COPY backend/ /app/")
build_identity = dockerfile.index("ARG GEOINTEL_BUILD_SHA=unknown")
assert build_identity > dependency_install
assert build_identity > source_copy
assert 'org.opencontainers.image.revision="${GEOINTEL_BUILD_SHA}"' in dockerfile
assert 'org.opencontainers.image.created="${GEOINTEL_BUILD_TIME}"' in dockerfile
def test_release_deploy_preserves_immutable_and_previous_images() -> None:
script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
assert 'GEOINTEL_RELEASE_IMAGE="${GEOINTEL_IMAGE_REPOSITORY}:${GEOINTEL_BUILD_SHA}"' in script
assert 'GEOINTEL_PREVIOUS_IMAGE="${GEOINTEL_IMAGE_REPOSITORY}:previous"' in script
assert 'docker tag "$current_image_id" "${GEOINTEL_IMAGE_REPOSITORY}:${current_revision}"' in script
assert 'docker tag "$current_image_id" "$GEOINTEL_PREVIOUS_IMAGE"' in script
assert "rollback_previous()" in script
assert "Deployed immutable image" in script
def test_runtime_configuration_is_validated_before_container_replacement() -> None:
run_script = (ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(encoding="utf-8")
validation_index = run_script.index("validate_runtime_config")
replacement_index = run_script.index("docker compose down")
assert validation_index < replacement_index
assert "known-default PostGIS password" in run_script
assert "GEOINTEL_MAX_UPLOAD_MB must be between 1 and 2048" in run_script
assert 'docker image inspect "$GEOINTEL_IMAGE"' in run_script
def test_fresh_install_smoke_is_isolated_and_cleans_only_its_temp_path() -> None:
script = (ROOT / "scripts" / "verify_release_fresh_install.sh").read_text(encoding="utf-8")
assert "mktemp -d" in script
assert "geointel-fresh-smoke.*" in script
assert "-p 127.0.0.1::80" in script
assert "GEOINTEL_POSTGRES_PASSWORD=" in script
assert "/health/ready" in script
assert "/api/v1/system/capabilities" in script
assert "docker exec" in script
assert "python -m alembic heads" in script
def test_manual_rollback_reuses_persistent_paths_and_requires_existing_image() -> None:
rollback = (ROOT / "deploy" / "unraid" / "rollback-dockerman-container.sh").read_text(encoding="utf-8")
run_script = (ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(encoding="utf-8")
assert "geointel-all-in-one:previous" in rollback
assert 'docker image inspect "$GEOINTEL_ROLLBACK_IMAGE"' in rollback
assert 'GEOINTEL_IMAGE="$GEOINTEL_ROLLBACK_IMAGE"' in rollback
assert '-v "${GEOINTEL_POSTGIS_DATA_PATH}:/var/lib/postgresql/data"' in run_script
assert '-v "${GEOINTEL_STORAGE_PATH}:/app/storage"' in run_script
def test_readiness_checks_all_release_shell_entrypoints() -> None:
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
for path in (
"scripts/deploy_tower.sh",
"scripts/verify_release_fresh_install.sh",
"deploy/unraid/all-in-one-start.sh",
"deploy/unraid/run-dockerman-container.sh",
"deploy/unraid/deploy-release.sh",
"deploy/unraid/rollback-dockerman-container.sh",
):
assert f"bash -n {path}" in readiness
@@ -36,13 +36,16 @@ def test_runtime_report_is_read_only_by_default_and_requires_confirmation() -> N
def test_all_in_one_deploy_embeds_immutable_build_identity() -> None:
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
release_script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
deploy_powershell = (ROOT / "scripts" / "deploy_tower.ps1").read_text(encoding="utf-8")
deploy_shell = (ROOT / "scripts" / "deploy_tower.sh").read_text(encoding="utf-8")
assert "ARG GEOINTEL_BUILD_SHA=unknown" in dockerfile
assert 'GEOINTEL_BUILD_SHA="${GEOINTEL_BUILD_SHA}"' in dockerfile
assert 'GEOINTEL_BUILD_TIME="${GEOINTEL_BUILD_TIME}"' in dockerfile
assert 'org.opencontainers.image.revision="${GEOINTEL_BUILD_SHA}"' in dockerfile
assert 'GEOINTEL_BUILD_SHA="$(git rev-parse HEAD)"' in release_script
assert "--build-arg GEOINTEL_BUILD_SHA=" in release_script
assert "--build-arg GEOINTEL_BUILD_TIME=" in release_script
for deploy_source in (deploy_powershell, deploy_shell):
assert 'GEOINTEL_BUILD_SHA="$(git rev-parse HEAD)"' in deploy_source
assert "--build-arg GEOINTEL_BUILD_SHA=" in deploy_source
assert "--build-arg GEOINTEL_BUILD_TIME=" in deploy_source
assert "bash deploy/unraid/deploy-release.sh" in deploy_source
@@ -524,8 +524,11 @@ def test_temporal_frontend_and_official_operator_contracts_exist() -> None:
def test_tower_deploy_waits_for_startup_migration_before_live_smoke() -> None:
for relative_path in ("scripts/deploy_tower.ps1", "scripts/deploy_tower.sh"):
script = (ROOT / relative_path).read_text(encoding="utf-8")
wait_position = script.index("wait_for_geointel_health")
invocation_position = script.index("\nwait_for_geointel_health", wait_position)
smoke_position = script.index("LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh")
assert "docker inspect --format" in script
assert invocation_position < smoke_position
assert "bash deploy/unraid/deploy-release.sh" in script
release_script = (ROOT / "deploy/unraid/deploy-release.sh").read_text(encoding="utf-8")
wait_position = release_script.index("wait_for_geointel_health")
invocation_position = release_script.index("\n wait_for_geointel_health", wait_position)
smoke_position = release_script.index("LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh")
assert "docker inspect --format" in release_script
assert invocation_position < smoke_position
+20 -14
View File
@@ -10,7 +10,7 @@ def test_unraid_template_documents_editable_runtime_settings() -> None:
template = (ROOT / "deploy" / "unraid" / "geointel-unraid-template.xml").read_text(encoding="utf-8")
assert "<Name>geointel</Name>" in template
assert "GeoIntel all-in-one runs the complete GeoIntel Kempen V1 stack in one Docker container" in template
assert "Belgium and Belgian North Sea workbench" in template
assert "<Repository>geointel-all-in-one:latest</Repository>" in template
assert "<WebUI>http://[IP]:[PORT:80]/</WebUI>" in template
assert "<Icon>http://192.168.10.150:1202/geointel-icon.png</Icon>" in template
@@ -109,29 +109,35 @@ def test_unraid_all_in_one_runtime_starts_embedded_postgis_backend_and_nginx() -
def test_tower_deploy_uses_single_container_unraid_compose() -> None:
powershell = (ROOT / "scripts" / "deploy_tower.ps1").read_text(encoding="utf-8")
bash = (ROOT / "scripts" / "deploy_tower.sh").read_text(encoding="utf-8")
release_script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
for script in (powershell, bash):
assert "docker compose -f docker-compose.unraid.yml config" in script
assert "--build-arg GEOINTEL_INSTALL_AI=" in script
assert '--build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA"' in script
assert '--build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME"' in script
assert "-f deploy/unraid/Dockerfile.all-in-one" in script
assert "-t geointel-all-in-one:latest" in script
assert "docker compose -f docker-compose.unraid.yml build geointel" not in script
assert "bash deploy/unraid/run-dockerman-container.sh" in script
assert "LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh" in script
assert "bash deploy/unraid/deploy-release.sh" in script
assert "docker compose -f docker-compose.unraid.yml config" in release_script
assert "--build-arg GEOINTEL_INSTALL_AI=" in release_script
assert '--build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA"' in release_script
assert '--build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME"' in release_script
assert "-f deploy/unraid/Dockerfile.all-in-one" in release_script
assert '-t "$GEOINTEL_RELEASE_IMAGE"' in release_script
assert '-t "${GEOINTEL_IMAGE_REPOSITORY}:latest"' in release_script
assert 'GEOINTEL_IMAGE="$image" bash deploy/unraid/run-dockerman-container.sh' in release_script
assert "LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh" in release_script
def test_tower_deploy_build_uses_remote_env_ai_setting_by_default() -> None:
powershell = (ROOT / "scripts" / "deploy_tower.ps1").read_text(encoding="utf-8")
bash = (ROOT / "scripts" / "deploy_tower.sh").read_text(encoding="utf-8")
release_script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
for script in (powershell, bash):
assert "if [ -f .env ]; then" in script
assert ". ./.env" in script
assert "DEPLOY_GEOINTEL_INSTALL_AI" in script
assert 'GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-false}"' in script
assert 'docker build --build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI"' in script
assert "bash deploy/unraid/deploy-release.sh" in script
assert "if [ -f .env ]; then" in release_script
assert ". ./.env" in release_script
assert 'GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-false}"' in release_script
assert "--build-arg GEOINTEL_INSTALL_AI=" in release_script
def test_powershell_tower_deploy_streams_remote_script_to_bash() -> None: