CaiWuXiTong/deploy/activate-incremental-release.sh

123 lines
4.0 KiB
Bash

#!/bin/sh
set -eu
REGISTRY_IMAGE="${1:?REGISTRY_IMAGE is required}"
IMAGE_TAG="${2:?IMAGE_TAG is required}"
REMOTE_DIR="${3:-/opt/caiwuxitong}"
DEPLOY_SHA="${4:?DEPLOY_SHA is required}"
RUN_MIGRATIONS="${5:-0}"
KEEP_IMAGES="${6:-2}"
IMAGE_NAME="caiwu_akaunting"
LOCK_FILE="$REMOTE_DIR/.deploy.lock"
REGISTRY_API="http://127.0.0.1:5000/v2/caiwuxitong/app"
TAG_HISTORY="$REMOTE_DIR/.registry-tags"
if [ "$(id -u)" -ne 0 ]; then
echo "activate-incremental-release.sh must run as root" >&2
exit 1
fi
if [ ! -f "$REMOTE_DIR/runtime/.env" ]; then
echo "Missing $REMOTE_DIR/runtime/.env; run bootstrap-server.sh first" >&2
exit 1
fi
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
echo "Another CaiWuXiTong deployment is already running" >&2
exit 1
fi
PREVIOUS_IMAGE="$(docker inspect caiwu_app --format '{{.Config.Image}}' 2>/dev/null || true)"
PREVIOUS_SHA="$(cat "$REMOTE_DIR/.deployed-sha" 2>/dev/null || true)"
docker pull "$REGISTRY_IMAGE"
docker tag "$REGISTRY_IMAGE" "$IMAGE_NAME:$IMAGE_TAG"
LOADED_SHA="$(docker image inspect "$IMAGE_NAME:$IMAGE_TAG" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')"
if [ "$LOADED_SHA" != "$DEPLOY_SHA" ]; then
echo "Image revision mismatch: expected $DEPLOY_SHA, got $LOADED_SHA" >&2
exit 1
fi
cd "$REMOTE_DIR"
export IMAGE_TAG RUN_MIGRATIONS
docker compose -f compose.jiangling.yml up -d --remove-orphans
healthy=0
attempt=1
while [ "$attempt" -le 30 ]; do
if curl -fsS -o /dev/null http://127.0.0.1:18088/; then
healthy=1
break
fi
sleep 5
attempt=$((attempt + 1))
done
if [ "$healthy" -ne 1 ]; then
docker compose -f compose.jiangling.yml logs --tail 120 app >&2 || true
case "$PREVIOUS_IMAGE" in
"$IMAGE_NAME":*)
IMAGE_TAG="${PREVIOUS_IMAGE#*:}"
RUN_MIGRATIONS=0
export IMAGE_TAG RUN_MIGRATIONS
docker compose -f compose.jiangling.yml up -d app || true
;;
esac
echo "Release failed health check; previous image was restored when available" >&2
exit 1
fi
printf '%s\n' "$DEPLOY_SHA" > "$REMOTE_DIR/.deployed-sha.tmp"
mv "$REMOTE_DIR/.deployed-sha.tmp" "$REMOTE_DIR/.deployed-sha"
docker image ls "$IMAGE_NAME" --format '{{.Repository}}:{{.Tag}}' \
| awk '!seen[$0]++' \
| tail -n "+$((KEEP_IMAGES + 1))" \
| while IFS= read -r old_image; do
[ -n "$old_image" ] && docker image rm "$old_image" >/dev/null 2>&1 || true
done
docker image prune -f --filter 'label=com.caiwu.project=CaiWuXiTong' >/dev/null || true
# Track tags by successful deployment order, retain only current + previous,
# delete older manifests, and reclaim their unreferenced blobs.
touch "$TAG_HISTORY"
awk -v tag="$IMAGE_TAG" '$0 != tag && NF { print }' "$TAG_HISTORY" > "$TAG_HISTORY.tmp"
printf '%s\n' "$IMAGE_TAG" >> "$TAG_HISTORY.tmp"
mv "$TAG_HISTORY.tmp" "$TAG_HISTORY"
deleted_registry_tag=0
while [ "$(wc -l < "$TAG_HISTORY")" -gt "$KEEP_IMAGES" ]; do
OLD_TAG="$(sed -n '1p' "$TAG_HISTORY")"
DIGEST="$(curl -fsSI \
-H 'Accept: application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' \
"$REGISTRY_API/manifests/$OLD_TAG" \
| awk -F ': ' 'tolower($1) == "docker-content-digest" { gsub("\r", "", $2); print $2 }')"
if [ -n "$DIGEST" ]; then
curl -fsS -X DELETE "$REGISTRY_API/manifests/$DIGEST" >/dev/null
deleted_registry_tag=1
fi
docker image rm "127.0.0.1:5000/caiwuxitong/app:$OLD_TAG" >/dev/null 2>&1 || true
sed '1d' "$TAG_HISTORY" > "$TAG_HISTORY.tmp"
mv "$TAG_HISTORY.tmp" "$TAG_HISTORY"
done
if [ "$deleted_registry_tag" -eq 1 ]; then
systemctl stop docker-registry
if ! docker-registry garbage-collect --delete-untagged /etc/docker/registry/config.yml; then
echo "Registry garbage collection failed; the service will still be restarted" >&2
fi
systemctl start docker-registry
fi
docker compose -f compose.jiangling.yml ps
echo "DEPLOYED_SHA=$DEPLOY_SHA"
: "$PREVIOUS_SHA"