fix(release): make deployment backup and rollback immutable

This commit is contained in:
Jens
2026-08-30 06:00:43 +02:00
parent a0884d64c9
commit c272220277
47 changed files with 3035 additions and 430 deletions
+79 -7
View File
@@ -18,17 +18,16 @@ def test_build_identity_does_not_invalidate_dependency_layers() -> None:
assert 'io.geointel.ai.enabled="${GEOINTEL_INSTALL_AI}"' in dockerfile
def test_release_deploy_preserves_immutable_and_previous_images() -> None:
def test_release_deploy_preserves_immutable_and_backup_specific_rollback_images() -> None:
script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
assert 'GEOINTEL_RELEASE_VARIANT="ai"' in script
assert 'GEOINTEL_RELEASE_VARIANT="gis"' in script
assert 'GEOINTEL_RELEASE_IMAGE="${GEOINTEL_IMAGE_REPOSITORY}:${GEOINTEL_BUILD_SHA}-${GEOINTEL_RELEASE_VARIANT}"' in script
assert 'GEOINTEL_PREVIOUS_IMAGE="${GEOINTEL_IMAGE_REPOSITORY}:previous"' in script
assert 'release_image_id="$(' in script
assert '[ "$current_image_id" != "$release_image_id" ]' in script
assert 'docker tag "$current_image_id" "$GEOINTEL_PREVIOUS_IMAGE"' in script
assert "preserving the existing previous image" in script
assert 'GEOINTEL_PREDEPLOY_ROLLBACK_TAG="${GEOINTEL_IMAGE_REPOSITORY}:rollback-${release_id}"' in script
assert 'docker tag "$current_image_id" "$GEOINTEL_PREDEPLOY_ROLLBACK_TAG"' in script
assert '--rollback-image-tag "$GEOINTEL_PREDEPLOY_ROLLBACK_TAG"' in script
assert "GEOINTEL_PREVIOUS_IMAGE" not in script
assert 'if docker image inspect "$GEOINTEL_RELEASE_IMAGE"' in script
assert "Immutable release tag has conflicting metadata" in script
assert "Reusing existing immutable image" in script
@@ -36,12 +35,62 @@ def test_release_deploy_preserves_immutable_and_previous_images() -> None:
assert "Deployed immutable image" in script
def test_release_creates_verified_backup_before_candidate_migrations() -> None:
script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
backup_index = script.index("create_predeploy_backup\n")
scan_index = script.index("scan_release_image\n")
candidate_start_index = script.index('if ! start_image "$GEOINTEL_RELEASE_IMAGE_ID"')
assert scan_index < backup_index
assert backup_index < candidate_start_index
assert script.index("docker build") < backup_index
assert "/mnt/user/appdata/geointel/backups" in script
assert "--inventory-mode sha256" in script
assert "scripts/verify_release_backup.sh" in script
assert "refusing an unbacked migration" in script
assert "Quiescing the current backend" in script
assert "restarting the unchanged current release" in script
assert "preflight_backup_capacity" in script
assert "select_verified_link_dest" in script
assert "release_backup_snapshot.py" in script
def test_release_starts_only_the_locally_attested_ai_image() -> None:
script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
assert 'GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-true}"' in script
assert "Production release deployment requires the gated AI image" in script
assert 'bash scripts/generate_container_sbom.sh "$GEOINTEL_RELEASE_IMAGE"' in script
assert 'bash scripts/scan_container_image.sh "$GEOINTEL_RELEASE_IMAGE"' in script
assert 'running_image_id="$(docker inspect --format \'{{.Image}}\' geointel)"' in script
assert 'if [ "$running_image_id" != "$image" ]' in script
assert "deployment-attestation.json" in script
assert "artifacts/release-evidence/deploy" in script
assert "GITEA_COMMIT_SHA" in script
assert "GITHUB_SHA" in script
assert "must contain one full 40-character Git commit SHA" in script
assert 'running_revision" != "$GEOINTEL_BUILD_SHA"' in script
assert 'running_ai" != "true"' in script
assert '"revision": revision' in script
def test_release_and_container_replacement_are_serialized() -> None:
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")
rollback_script = (ROOT / "deploy" / "unraid" / "rollback-dockerman-container.sh").read_text(
encoding="utf-8"
)
restore_script = (ROOT / "deploy" / "unraid" / "restore-predeploy-database.sh").read_text(
encoding="utf-8"
)
assert "GEOINTEL_DEPLOY_LOCK_FILE" in release_script
assert "flock -n 9" in release_script
assert "GEOINTEL_DEPLOY_LOCK_FILE" in rollback_script
assert "flock -n 9" in rollback_script
assert "GEOINTEL_DEPLOY_LOCK_FILE" in restore_script
assert "flock -n 9" in restore_script
assert "GEOINTEL_DEPLOY_LOCK_HELD=true" in release_script
assert "GEOINTEL_CONTAINER_LOCK_FILE" in run_script
assert "flock -w 300 8" in run_script
assert "GeoIntel container removal did not complete within 60 seconds" in run_script
@@ -89,13 +138,35 @@ def test_manual_rollback_reuses_persistent_paths_and_requires_existing_image() -
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 "Backup manifest lacks an immutable rollback image ID" in rollback
assert 'get("image_id", "")' in rollback
assert 'docker image inspect "$GEOINTEL_ROLLBACK_IMAGE"' in rollback
assert 'GEOINTEL_IMAGE="$GEOINTEL_ROLLBACK_IMAGE"' in rollback
assert "restore-predeploy-database.sh" in rollback
assert "--confirm-production-database-restore" in rollback
assert "Image-only rollback" 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_same_revision_redeploy_rolls_back_by_backup_bound_image_id() -> None:
release = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
rollback = (ROOT / "deploy" / "unraid" / "rollback-dockerman-container.sh").read_text(
encoding="utf-8"
)
restore = (ROOT / "deploy" / "unraid" / "restore-predeploy-database.sh").read_text(
encoding="utf-8"
)
backup = (ROOT / "scripts" / "backup_release_state.sh").read_text(encoding="utf-8")
assert "current_image_id" in release
assert "release_image_id" not in release
assert '"rollback_image_tag": ${ROLLBACK_IMAGE_TAG@Q} or None' in backup
assert 'if [ -z "$RESTORE_IMAGE" ]; then\n RESTORE_IMAGE="$BACKUP_IMAGE_ID"' in restore
assert 'GEOINTEL_ROLLBACK_IMAGE="$(python3 - "$BACKUP_DIR/manifest.json"' in rollback
assert "geointel-all-in-one:previous" not in release + rollback + restore
def test_readiness_checks_all_release_shell_entrypoints() -> None:
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
@@ -107,6 +178,7 @@ def test_readiness_checks_all_release_shell_entrypoints() -> None:
"deploy/unraid/run-dockerman-container.sh",
"deploy/unraid/deploy-release.sh",
"deploy/unraid/rollback-dockerman-container.sh",
"deploy/unraid/restore-predeploy-database.sh",
):
assert f"bash -n {path}" in readiness