Make release image tags truly immutable
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:35:43 +02:00
parent 5699006a5e
commit f7078468db
6 changed files with 55 additions and 25 deletions
+3
View File
@@ -72,6 +72,9 @@
- Hardened the all-in-one release path with commit-SHA image tags, OCI build - Hardened the all-in-one release path with commit-SHA image tags, OCI build
labels, previous-image preservation, automatic/manual rollback, an isolated labels, previous-image preservation, automatic/manual rollback, an isolated
fresh-install smoke and dependency-cache-safe build metadata. fresh-install smoke and dependency-cache-safe build metadata.
- Bound immutable release tags to both commit and dependency profile
(`<sha>-ai` or `<sha>-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 - Made production startup reject known-default PostGIS passwords and apply the
configured upload limit consistently to nginx and FastAPI. configured upload limit consistently to nginx and FastAPI.
+7 -2
View File
@@ -15,15 +15,20 @@ def test_build_identity_does_not_invalidate_dependency_layers() -> None:
assert build_identity > source_copy assert build_identity > source_copy
assert 'org.opencontainers.image.revision="${GEOINTEL_BUILD_SHA}"' in dockerfile assert 'org.opencontainers.image.revision="${GEOINTEL_BUILD_SHA}"' in dockerfile
assert 'org.opencontainers.image.created="${GEOINTEL_BUILD_TIME}"' 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: def test_release_deploy_preserves_immutable_and_previous_images() -> None:
script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8") 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 '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 '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 "rollback_previous()" in script
assert "Deployed immutable image" in script assert "Deployed immutable image" in script
+2 -1
View File
@@ -159,7 +159,8 @@ ENV GEOINTEL_BUILD_SHA="${GEOINTEL_BUILD_SHA}" \
LABEL org.opencontainers.image.title="GeoIntel" \ LABEL org.opencontainers.image.title="GeoIntel" \
org.opencontainers.image.description="GeoIntel workbench for Belgium and the Belgian North Sea" \ org.opencontainers.image.description="GeoIntel workbench for Belgium and the Belgian North Sea" \
org.opencontainers.image.revision="${GEOINTEL_BUILD_SHA}" \ 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"] VOLUME ["/var/lib/postgresql/data", "/app/storage"]
+6 -5
View File
@@ -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 repository deploy scripts run the same flow automatically. They validate
the Compose reference, preserve the current image as the Compose reference, preserve the current image as
`geointel-all-in-one:previous`, build an immutable commit-SHA tag plus `latest`, `geointel-all-in-one:previous`, build an immutable `<commit-sha>-ai` or
install the DockerMan metadata and start the SHA-tagged image. A failed start, `<commit-sha>-gis` tag plus `latest`, install the DockerMan metadata and start
live migration smoke or browser/API smoke automatically attempts the previous the immutable image. An existing matching tag is reused, never rebuilt. A
image without changing the configured PostGIS or storage paths. 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 `scripts/deploy_tower.sh` and `scripts/deploy_tower.ps1` source the remote
`.env` before building the image. That means `GEOINTEL_INSTALL_AI=true` in `.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: For an older retained commit, select its immutable tag explicitly:
```bash ```bash
GEOINTEL_ROLLBACK_IMAGE=geointel-all-in-one:<commit-sha> \ GEOINTEL_ROLLBACK_IMAGE=geointel-all-in-one:<commit-sha>-ai \
bash deploy/unraid/rollback-dockerman-container.sh bash deploy/unraid/rollback-dockerman-container.sh
``` ```
+33 -17
View File
@@ -19,7 +19,12 @@ GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-false}"
GEOINTEL_BUILD_SHA="$(git rev-parse HEAD)" GEOINTEL_BUILD_SHA="$(git rev-parse HEAD)"
GEOINTEL_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" GEOINTEL_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
GEOINTEL_IMAGE_REPOSITORY="${GEOINTEL_IMAGE_REPOSITORY:-geointel-all-in-one}" 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" GEOINTEL_PREVIOUS_IMAGE="${GEOINTEL_IMAGE_REPOSITORY}:previous"
FRONTEND_URL="${FRONTEND_URL:-http://127.0.0.1:${GEOINTEL_FRONTEND_PORT:-1202}}" 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)" 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 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" != "<no value>" ] && [ "$current_revision" != "unknown" ]; then
docker tag "$current_image_id" "${GEOINTEL_IMAGE_REPOSITORY}:${current_revision}"
fi
docker tag "$current_image_id" "$GEOINTEL_PREVIOUS_IMAGE" docker tag "$current_image_id" "$GEOINTEL_PREVIOUS_IMAGE"
fi fi
docker build \ if docker image inspect "$GEOINTEL_RELEASE_IMAGE" >/dev/null 2>&1; then
--build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI" \ stored_revision="$(
--build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA" \ docker image inspect \
--build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME" \ --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' \
-f deploy/unraid/Dockerfile.all-in-one \ "$GEOINTEL_RELEASE_IMAGE"
-t "$GEOINTEL_RELEASE_IMAGE" \ )"
-t "${GEOINTEL_IMAGE_REPOSITORY}:latest" \ 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 if ! start_image "$GEOINTEL_RELEASE_IMAGE"; then
rollback_previous || true rollback_previous || true
+4
View File
@@ -45,6 +45,10 @@
immutable commit images plus a `previous` rollback target, adding automatic immutable commit images plus a `previous` rollback target, adding automatic
rollback and an isolated fresh-install smoke, and failing closed on default rollback and an isolated fresh-install smoke, and failing closed on default
database secrets or inconsistent upload limits. 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 - Froze the RC geography as all Belgian land plus the separately labelled
territorial sea, EEZ and continental shelf. territorial sea, EEZ and continental shelf.