diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c8020b6..3ba00381 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- `bin/deploy` now refuses to push a single-architecture build over a tag that + already resolves to a multi-architecture manifest (#427). It builds for the + architecture of the machine it runs on, so deploying a homelab from an x86 + workstation to `IMAGE=akitaonrails/ai-memory:latest` replaced the + amd64+arm64 manifest CI had published with an amd64-only image, and every + arm64 host pulling `:latest` failed with `exec format error`. The shipped + `bin/deploy.env.example` also defaulted `IMAGE` to `:latest`, so following + the documented setup led straight into it; it now defaults to a private + `:homelab` tag and explains that release tags are published by CI only. + ### Added - New `strip_root_combinators` config flag (env `AI_MEMORY_STRIP_ROOT_COMBINATORS`, or `strip_root_combinators = true` in config.toml) strips root-level diff --git a/bin/deploy b/bin/deploy index 9f71d513..1e8a45f3 100755 --- a/bin/deploy +++ b/bin/deploy @@ -58,6 +58,44 @@ CONTAINER_NAME="${CONTAINER_NAME:-ai-memory}" cd "$REPO_ROOT" +# Fail closed if $IMAGE already carries a multi-architecture manifest. +# +# `docker build` below produces an image for THIS machine's architecture +# only. Pushing that to a tag whose current manifest is a multi-arch list +# replaces the list, and every host on the other architecture starts +# failing with `exec format error` on the next pull. +# +# That is not hypothetical: it happened to `:latest` on 2026-08-18 (issue +# #427). `bin/release` + the release workflow publish amd64 and arm64 and +# join them into a manifest; a homelab deploy from an x86 workstation then +# silently flattened it to amd64-only. Release tags are published by CI — +# a deploy has no business overwriting one. +if docker manifest inspect "$IMAGE" >/dev/null 2>&1 && + docker manifest inspect "$IMAGE" 2>/dev/null | grep -q '"manifests"'; then + cat >&2 < REFUSING to overwrite a multi-architecture tag. + + $IMAGE currently resolves to a multi-arch manifest (published by + the release workflow). This script builds for $(uname -m) only, so + pushing would drop every other architecture and break those hosts + with "exec format error". + + Deploy a private tag instead — set IMAGE in bin/deploy.env to + something like: + + IMAGE="akitaonrails/ai-memory:homelab" + + and point docker-compose.yml on the server at that tag. Release + tags (:latest, :X.Y.Z) are published by CI only. + + To deploy the released image without rebuilding, skip this script + and just pull it on the server: + + ssh \$SERVER "cd \$DEPLOY_DIR && docker compose pull && docker compose up -d" +EOF + exit 65 +fi + echo "==> Building Docker image: $IMAGE" docker build -t "$IMAGE" -f docker/Dockerfile . diff --git a/bin/deploy.env.example b/bin/deploy.env.example index 3c41c0fc..e0a68396 100644 --- a/bin/deploy.env.example +++ b/bin/deploy.env.example @@ -12,9 +12,19 @@ DEPLOY_DIR="/var/opt/docker/utils/ai-memory" # Image tag to build + push + pull. Make sure the Docker host can pull # from wherever you push to (Docker Hub, GHCR, your own registry). -# Default: the official published image. Change to your own fork if -# you're building from source. -IMAGE="akitaonrails/ai-memory:latest" +# +# Use a tag of your OWN, not a release tag. `bin/deploy` builds for the +# architecture of the machine you run it on; pushing that over `:latest` +# or `:X.Y.Z` — which CI publishes as multi-arch manifests — drops every +# other architecture and breaks those hosts with "exec format error" +# (issue #427). bin/deploy refuses to do it, but pick a private tag here +# and there is nothing to refuse. +IMAGE="akitaonrails/ai-memory:homelab" + +# If you only want to run the RELEASED image rather than build your own, +# you do not need bin/deploy at all — point docker-compose.yml at +# `akitaonrails/ai-memory:latest` on the server and run: +# docker compose pull && docker compose up -d # Optional: override the container name if your docker-compose.yml # uses something other than the default `ai-memory`.