netanalyzer/docker/remote-agent/deploy/install.sh
Joseph McQueen 0506f39bf3 fix(deploy): install.sh always docker-loads so stale wrong-arch images can't hide the shipped tarball
Previously install.sh short-circuited with "Image ... already present -
skipping load" whenever a tag with the same version already existed in
Docker. That optimization was actively harmful: if an earlier deploy
attempt had loaded a wrong-arch (e.g. arm64) image at the same tag, we'd
never load the corrected tarball in the current ZIP and the arch check
would keep failing against the stale image forever.

docker load reassigns the tag atomically to whatever is in the tarball and
is a fast no-op when the layers are already present, so unconditional load
is both safe and self-healing.

install.sh now also prints the loaded image id + arch and, on mismatch,
tells the operator to `git pull` on the build host before re-running
package.sh so they pick up the buildx/binfmt fixes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 10:12:08 -04:00

138 lines
5.3 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# StoreHealthAnalyzer Remote Agent — install / (re)start on a remote host.
#
# Run this after extracting the deploy ZIP:
# unzip sha-remote-agent-<version>.zip
# cd sha-remote-agent-<version>
# ./install.sh
#
# On first run: verifies the image tarball, loads it into Docker, and drops
# a starter .env so you can fill in WS_URL / WS_TOKEN. Re-runs are safe —
# the script is idempotent.
set -euo pipefail
# Move to the script's own directory so relative paths work regardless of
# where the user invoked it from.
cd "$(dirname "$0")"
RED=$'\033[0;31m'
GRN=$'\033[0;32m'
YLW=$'\033[1;33m'
RST=$'\033[0m'
log() { printf '%s[install]%s %s\n' "$GRN" "$RST" "$*"; }
warn() { printf '%s[install]%s %s\n' "$YLW" "$RST" "$*"; }
die() { printf '%s[install]%s %s\n' "$RED" "$RST" "$*" >&2; exit 1; }
# --- 1. Preflight ----------------------------------------------------------
command -v docker >/dev/null 2>&1 || die "Docker not found on PATH."
docker info >/dev/null 2>&1 \
|| die "Cannot talk to the Docker daemon. Is it running / do you have permission?"
# Detect either `docker compose` (v2 plugin) or the legacy `docker-compose`.
if docker compose version >/dev/null 2>&1; then
COMPOSE=(docker compose)
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE=(docker-compose)
else
die "Neither 'docker compose' nor 'docker-compose' is available. Install Docker Compose."
fi
[[ -f VERSION ]] || die "VERSION file missing from bundle — is this a valid deploy ZIP?"
VERSION="$(cat VERSION)"
IMAGE_TARBALL="sha-remote-agent-${VERSION}.tar.gz"
[[ -f "$IMAGE_TARBALL" ]] || die "Image tarball not found: $IMAGE_TARBALL"
# --- 2. Verify checksum (optional; skip gracefully if no shasum tool) -----
if [[ -f SHA256SUMS ]]; then
if command -v sha256sum >/dev/null 2>&1; then
log "Verifying SHA256 checksum..."
sha256sum -c SHA256SUMS >/dev/null || die "Checksum verification FAILED."
elif command -v shasum >/dev/null 2>&1; then
log "Verifying SHA256 checksum (macOS shasum)..."
shasum -a 256 -c SHA256SUMS >/dev/null || die "Checksum verification FAILED."
else
warn "No sha256sum/shasum tool found — skipping integrity check."
fi
log "Checksum OK."
else
warn "No SHA256SUMS file in bundle — skipping integrity check."
fi
# --- 3. Load the image -----------------------------------------------------
# ALWAYS docker load, unconditionally. `docker load` reassigns the tag to
# whatever's in the tarball and is a fast no-op when the layers are already
# present, so this is safe to re-run. We intentionally do NOT "skip if the
# tag already exists" — a stale image left over from a previous wrong-arch
# attempt has this exact tag, and skipping the load would hide the correct
# image in the tarball we're actually holding.
IMAGE_TAG="sha-remote-agent:${VERSION}"
log "Loading Docker image from ${IMAGE_TARBALL}..."
docker load -i "$IMAGE_TARBALL"
# --- 3a. Platform sanity check --------------------------------------------
# Inspect the image we just (re)loaded — this is the ground truth for what
# was actually shipped in this ZIP, independent of anything that was on the
# host before. If the tarball was built for a different CPU architecture
# than this host, Docker will let it "run" but tini (and node) fail with
# cryptic errors like "exec format error". Catch that up front with a
# clear message.
IMAGE_ARCH="$(docker image inspect --format '{{.Architecture}}' "$IMAGE_TAG" 2>/dev/null || true)"
IMAGE_ID_SHORT="$(docker image inspect --format '{{.Id}}' "$IMAGE_TAG" 2>/dev/null | sed 's|^sha256:||' | cut -c1-12)"
HOST_ARCH_RAW="$(uname -m)"
case "$HOST_ARCH_RAW" in
x86_64|amd64) HOST_ARCH="amd64" ;;
aarch64|arm64) HOST_ARCH="arm64" ;;
armv7l) HOST_ARCH="arm" ;;
*) HOST_ARCH="$HOST_ARCH_RAW" ;;
esac
log "Loaded ${IMAGE_TAG} (id: ${IMAGE_ID_SHORT:-unknown}, arch: ${IMAGE_ARCH:-unknown}); host arch: ${HOST_ARCH}."
if [[ -z "$IMAGE_ARCH" ]]; then
die "Could not read image architecture from Docker — is the image really loaded?"
fi
if [[ "$IMAGE_ARCH" != "$HOST_ARCH" ]]; then
warn "Image architecture (${IMAGE_ARCH}) does not match this host (${HOST_ARCH})."
warn "The container would fail to start with 'exec /sbin/tini: exec format error'."
warn ""
warn "This means the ZIP was built for the wrong CPU. On the build host:"
warn " 1. git pull # make sure you have the fixed package.sh"
warn " 2. ./docker/remote-agent/package.sh --platform linux/${HOST_ARCH}"
warn "The updated package.sh verifies the architecture during build and"
warn "refuses to produce a ZIP that would fail this check."
die "Aborting install. Ship a linux/${HOST_ARCH} bundle and re-run this script."
fi
# --- 4. Bootstrap .env -----------------------------------------------------
if [[ ! -f .env ]]; then
if [[ -f .env.example ]]; then
cp .env.example .env
warn ".env did not exist — copied .env.example into place."
warn "EDIT .env now to set WS_URL and WS_TOKEN, then re-run this script."
exit 0
else
die ".env is missing and no .env.example is bundled. Cannot proceed."
fi
fi
# --- 5. Start the container -----------------------------------------------
log "Starting sha-remote-agent (version ${VERSION})..."
"${COMPOSE[@]}" up -d
log "Done. Tail logs with:"
log " ${COMPOSE[*]} logs -f"
log "Stop with:"
log " ${COMPOSE[*]} down"