fix(deploy): refuse to flatten a multi-arch tag to one architecture

`bin/deploy` builds with plain `docker build`, which produces an image for
the architecture of the machine running it. Pushing that to a tag whose
current manifest is a multi-arch list replaces the list outright, and every
host on the other architecture fails the next pull with `exec format error`.

This is not hypothetical — it happened to `:latest` on 2026-08-18 and was
reported within hours as #427. The release workflow builds amd64 and arm64
and joins them into a manifest; a homelab deploy from an x86 workstation
then silently flattened that to amd64-only. The registry has been repaired
by re-pointing `:latest` at the `:1.28.1` manifest list.

Two changes so it cannot recur:

- `bin/deploy` inspects the target tag first and exits 65 if it already
  resolves to a manifest list, naming the private-tag fix and the
  pull-only alternative for deploying a released image.
- `bin/deploy.env.example` shipped `IMAGE="akitaonrails/ai-memory:latest"`,
  so the documented setup pointed the operator's build at the public
  release tag by default. It now defaults to `:homelab`.

Refs #427

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
AkitaOnRails
2026-08-19 12:16:30 -03:00
co-authored by Claude Opus 5
parent acd9c0b369
commit e40b9964b1
3 changed files with 62 additions and 3 deletions
+11
View File
@@ -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
+38
View File
@@ -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 <<EOF
==> 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 .
+13 -3
View File
@@ -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`.