From f7078468db064084756461982348ebe3812e89b7 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 18 Jul 2026 02:35:43 +0200 Subject: [PATCH] Make release image tags truly immutable --- CHANGELOG.md | 3 ++ backend/tests/test_rc5_release_deployment.py | 9 +++- deploy/unraid/Dockerfile.all-in-one | 3 +- deploy/unraid/README.md | 11 +++-- deploy/unraid/deploy-release.sh | 50 +++++++++++++------- docs/CODEX_EXECUTION_LOG.md | 4 ++ 6 files changed, 55 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec63fa04..da48d3f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,9 @@ - Hardened the all-in-one release path with commit-SHA image tags, OCI build labels, previous-image preservation, automatic/manual rollback, an isolated fresh-install smoke and dependency-cache-safe build metadata. +- Bound immutable release tags to both commit and dependency profile + (`-ai` or `-gis`) and reuse matching images instead of rebuilding + or overwriting an existing release tag. - Made production startup reject known-default PostGIS passwords and apply the configured upload limit consistently to nginx and FastAPI. diff --git a/backend/tests/test_rc5_release_deployment.py b/backend/tests/test_rc5_release_deployment.py index e9f555ff..cb8e7d40 100644 --- a/backend/tests/test_rc5_release_deployment.py +++ b/backend/tests/test_rc5_release_deployment.py @@ -15,15 +15,20 @@ def test_build_identity_does_not_invalidate_dependency_layers() -> None: 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 + assert 'io.geointel.ai.enabled="${GEOINTEL_INSTALL_AI}"' 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_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 'docker tag "$current_image_id" "${GEOINTEL_IMAGE_REPOSITORY}:${current_revision}"' in script assert 'docker tag "$current_image_id" "$GEOINTEL_PREVIOUS_IMAGE"' 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 assert "rollback_previous()" in script assert "Deployed immutable image" in script diff --git a/deploy/unraid/Dockerfile.all-in-one b/deploy/unraid/Dockerfile.all-in-one index 86e1dc81..e2ffd44b 100644 --- a/deploy/unraid/Dockerfile.all-in-one +++ b/deploy/unraid/Dockerfile.all-in-one @@ -159,7 +159,8 @@ ENV GEOINTEL_BUILD_SHA="${GEOINTEL_BUILD_SHA}" \ LABEL org.opencontainers.image.title="GeoIntel" \ org.opencontainers.image.description="GeoIntel workbench for Belgium and the Belgian North Sea" \ org.opencontainers.image.revision="${GEOINTEL_BUILD_SHA}" \ - org.opencontainers.image.created="${GEOINTEL_BUILD_TIME}" + org.opencontainers.image.created="${GEOINTEL_BUILD_TIME}" \ + io.geointel.ai.enabled="${GEOINTEL_INSTALL_AI}" VOLUME ["/var/lib/postgresql/data", "/app/storage"] diff --git a/deploy/unraid/README.md b/deploy/unraid/README.md index a8f6a5d6..99012814 100644 --- a/deploy/unraid/README.md +++ b/deploy/unraid/README.md @@ -81,10 +81,11 @@ password is empty or one of the documented defaults. The repository deploy scripts run the same flow automatically. They validate the Compose reference, preserve the current image as -`geointel-all-in-one:previous`, build an immutable commit-SHA tag plus `latest`, -install the DockerMan metadata and start the SHA-tagged image. A failed start, -live migration smoke or browser/API smoke automatically attempts the previous -image without changing the configured PostGIS or storage paths. +`geointel-all-in-one:previous`, build an immutable `-ai` or +`-gis` tag plus `latest`, install the DockerMan metadata and start +the immutable image. An existing matching tag is reused, never rebuilt. A +failed start, live migration smoke or browser/API smoke automatically attempts +the previous image without changing the configured PostGIS or storage paths. `scripts/deploy_tower.sh` and `scripts/deploy_tower.ps1` source the remote `.env` before building the image. That means `GEOINTEL_INSTALL_AI=true` in @@ -270,7 +271,7 @@ bash deploy/unraid/rollback-dockerman-container.sh For an older retained commit, select its immutable tag explicitly: ```bash -GEOINTEL_ROLLBACK_IMAGE=geointel-all-in-one: \ +GEOINTEL_ROLLBACK_IMAGE=geointel-all-in-one:-ai \ bash deploy/unraid/rollback-dockerman-container.sh ``` diff --git a/deploy/unraid/deploy-release.sh b/deploy/unraid/deploy-release.sh index 4c836a75..bc6da251 100644 --- a/deploy/unraid/deploy-release.sh +++ b/deploy/unraid/deploy-release.sh @@ -19,7 +19,12 @@ GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-false}" GEOINTEL_BUILD_SHA="$(git rev-parse HEAD)" GEOINTEL_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" GEOINTEL_IMAGE_REPOSITORY="${GEOINTEL_IMAGE_REPOSITORY:-geointel-all-in-one}" -GEOINTEL_RELEASE_IMAGE="${GEOINTEL_IMAGE_REPOSITORY}:${GEOINTEL_BUILD_SHA}" +if [ "$GEOINTEL_INSTALL_AI" = "true" ]; then + GEOINTEL_RELEASE_VARIANT="ai" +else + GEOINTEL_RELEASE_VARIANT="gis" +fi +GEOINTEL_RELEASE_IMAGE="${GEOINTEL_IMAGE_REPOSITORY}:${GEOINTEL_BUILD_SHA}-${GEOINTEL_RELEASE_VARIANT}" GEOINTEL_PREVIOUS_IMAGE="${GEOINTEL_IMAGE_REPOSITORY}:previous" FRONTEND_URL="${FRONTEND_URL:-http://127.0.0.1:${GEOINTEL_FRONTEND_PORT:-1202}}" @@ -62,25 +67,36 @@ docker compose -f docker-compose.unraid.yml config >/dev/null current_image_id="$(docker inspect --format '{{.Image}}' geointel 2>/dev/null || true)" if [ -n "$current_image_id" ] && docker image inspect "$current_image_id" >/dev/null 2>&1; then - current_revision="$( - docker image inspect \ - --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' \ - "$current_image_id" 2>/dev/null || true - )" - if [ -n "$current_revision" ] && [ "$current_revision" != "" ] && [ "$current_revision" != "unknown" ]; then - docker tag "$current_image_id" "${GEOINTEL_IMAGE_REPOSITORY}:${current_revision}" - fi docker tag "$current_image_id" "$GEOINTEL_PREVIOUS_IMAGE" fi -docker build \ - --build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI" \ - --build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA" \ - --build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME" \ - -f deploy/unraid/Dockerfile.all-in-one \ - -t "$GEOINTEL_RELEASE_IMAGE" \ - -t "${GEOINTEL_IMAGE_REPOSITORY}:latest" \ - . +if docker image inspect "$GEOINTEL_RELEASE_IMAGE" >/dev/null 2>&1; then + stored_revision="$( + docker image inspect \ + --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' \ + "$GEOINTEL_RELEASE_IMAGE" + )" + stored_ai="$( + docker image inspect \ + --format '{{index .Config.Labels "io.geointel.ai.enabled"}}' \ + "$GEOINTEL_RELEASE_IMAGE" + )" + if [ "$stored_revision" != "$GEOINTEL_BUILD_SHA" ] || [ "$stored_ai" != "$GEOINTEL_INSTALL_AI" ]; then + echo "Immutable release tag has conflicting metadata: ${GEOINTEL_RELEASE_IMAGE}" >&2 + exit 2 + fi + echo "Reusing existing immutable image ${GEOINTEL_RELEASE_IMAGE}." + docker tag "$GEOINTEL_RELEASE_IMAGE" "${GEOINTEL_IMAGE_REPOSITORY}:latest" +else + docker build \ + --build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI" \ + --build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA" \ + --build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME" \ + -f deploy/unraid/Dockerfile.all-in-one \ + -t "$GEOINTEL_RELEASE_IMAGE" \ + -t "${GEOINTEL_IMAGE_REPOSITORY}:latest" \ + . +fi if ! start_image "$GEOINTEL_RELEASE_IMAGE"; then rollback_previous || true diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 3c40dbc1..bd132286 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -45,6 +45,10 @@ immutable commit images plus a `previous` rollback target, adding automatic rollback and an isolated fresh-install smoke, and failing closed on default database secrets or inconsistent upload limits. +- Tower proved cached apt/PyTorch layers, a fresh install on isolated paths and + a healthy rollback against the retained database/storage. The release tag + contract was then tightened to immutable commit-plus-profile tags so repeated + deploys reuse rather than overwrite the same build identity. - Froze the RC geography as all Belgian land plus the separately labelled territorial sea, EEZ and continental shelf.