CaiWuXiTong/deploy/activate-release.sh
limeng 7146b19bd3 修复生产发布健康探针 (#188)
使用容器已声明的公开静态资源执行发布健康检查,避免根路径被 Laravel 防火墙拒绝后误回滚。
2026-09-15 11:53:17 +08:00

205 lines
8.1 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/sh
set -eu
IMAGE_ARCHIVE="${1:?IMAGE_ARCHIVE 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"
cleanup() {
rm -f "$IMAGE_ARCHIVE"
}
trap cleanup EXIT INT TERM
if [ "$(id -u)" -ne 0 ]; then
echo "activate-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
clickhouse_env="$REMOTE_DIR/runtime/clickhouse.env"
if [ ! -f "$clickhouse_env" ]; then
clickhouse_password="$(openssl rand -hex 24)"
umask 077
{
printf '%s\n' \
'CLICKHOUSE_DB=caiwu_business_analytics' \
'CLICKHOUSE_USER=caiwu_app' \
"CLICKHOUSE_PASSWORD=$clickhouse_password" \
'CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1' \
'BUSINESS_CLICKHOUSE_ENABLED=true' \
'BUSINESS_CLICKHOUSE_URL=http://clickhouse:8123' \
'BUSINESS_CLICKHOUSE_DATABASE=caiwu_business_analytics' \
'BUSINESS_CLICKHOUSE_USERNAME=caiwu_app' \
"BUSINESS_CLICKHOUSE_PASSWORD=$clickhouse_password"
} > "$clickhouse_env"
chmod 600 "$clickhouse_env"
fi
if ! grep -q '^CLICKHOUSE_PASSWORD=.' "$clickhouse_env" \
|| ! grep -q '^BUSINESS_CLICKHOUSE_PASSWORD=.' "$clickhouse_env"; then
echo "Incomplete $clickhouse_env; refusing to overwrite private ClickHouse credentials" >&2
exit 1
fi
if [ ! -f "$REMOTE_DIR/runtime/config-db.env" ]; then
echo "Missing $REMOTE_DIR/runtime/config-db.env; run bootstrap-server.sh first" >&2
exit 1
fi
if ! grep -q '^BUSINESS_STORAGE_DRIVER=qiniu$' "$REMOTE_DIR/runtime/qiniu-storage.env" 2>/dev/null \
|| ! grep -q '^QINIU_ACCESS_KEY=.' "$REMOTE_DIR/runtime/qiniu-storage.env" 2>/dev/null \
|| ! grep -q '^QINIU_SECRET_KEY=.' "$REMOTE_DIR/runtime/qiniu-storage.env" 2>/dev/null; then
echo "Private business storage runtime is incomplete" >&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)"
restore_previous_image() {
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
}
docker load --input "$IMAGE_ARCHIVE"
rm -f "$IMAGE_ARCHIVE"
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
# Pull the independent columnar runtime before migrations or app switching.
# A registry/network failure therefore leaves both the database and current app untouched.
docker compose -f compose.jiangling.yml pull clickhouse
if [ "$RUN_MIGRATIONS" = "1" ]; then
# 先创建独立业务镜像库并授权应用账号,再启动会执行迁移的新应用。
# 该脚本幂等;已有数据库只会补齐授权,不会覆盖业务数据。
docker compose -f compose.jiangling.yml up -d config_db
config_db_attempt=1
while [ "$config_db_attempt" -le 24 ]; do
if [ "$(docker inspect caiwu_config_db --format '{{.State.Health.Status}}' 2>/dev/null || true)" = "healthy" ]; then
break
fi
sleep 5
config_db_attempt=$((config_db_attempt + 1))
done
if [ "$(docker inspect caiwu_config_db --format '{{.State.Health.Status}}' 2>/dev/null || true)" != "healthy" ]; then
docker compose -f compose.jiangling.yml logs --tail 80 config_db >&2 || true
echo "Shared MariaDB did not become healthy before mirror database initialization" >&2
exit 1
fi
docker exec caiwu_config_db sh /docker-entrypoint-initdb.d/10-config-db-security.sh
fi
docker compose -f compose.jiangling.yml up -d --remove-orphans
clickhouse_healthy=0
clickhouse_attempt=1
while [ "$clickhouse_attempt" -le 24 ]; do
if [ "$(docker inspect caiwu_clickhouse --format '{{.State.Health.Status}}' 2>/dev/null || true)" = "healthy" ]; then
clickhouse_healthy=1
break
fi
sleep 5
clickhouse_attempt=$((clickhouse_attempt + 1))
done
if [ "$clickhouse_healthy" -ne 1 ]; then
docker compose -f compose.jiangling.yml logs --tail 120 clickhouse >&2 || true
restore_previous_image
echo "ClickHouse did not become healthy; previous application image was restored when available" >&2
exit 1
fi
healthy=0
attempt=1
while [ "$attempt" -le 30 ]; do
if curl -fsS -o /dev/null http://127.0.0.1:18088/serviceworker.js; 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
restore_previous_image
echo "Release failed health check; previous image was restored when available" >&2
exit 1
fi
if ! docker exec -u www-data caiwu_app php artisan storage:business-files:sync --require-qiniu; then
docker compose -f compose.jiangling.yml logs --tail 120 app >&2 || true
restore_previous_image
echo "Release failed private business storage verification; previous image was restored when available" >&2
exit 1
fi
if ! curl -fsS -o /dev/null http://127.0.0.1:18088/serviceworker.js; then
restore_previous_image
echo "Release failed after business storage activation; 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"
# 只运行已登记的上游后台读取和 E 签宝状态轮询,不启用整个 Laravel
# 调度器,避免意外启动账单提醒等既有任务。上游读取共用一把锁,E 签宝单独加锁。
# 普通商品优先高频增量,并在配置的每周低峰窗口自动继续全量校准。
cat > /etc/cron.d/caiwu-jiansun-snapshots <<'EOF'
CRON_TZ=Asia/Shanghai
* * * * * root /usr/bin/flock -n /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-catalog --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-purchase-prices --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-store-report --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan upstream:snapshots:refresh-all --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-prices --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-tables --quiet' >/dev/null 2>&1
0 0 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-third-party-warehouse-storage --max-batches=0 --quiet >/dev/null 2>&1
0 0 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-jushuitan-cost-price-history --quiet >/dev/null 2>&1
*/2 * * * * root /usr/bin/flock -n /run/caiwu-esign-polling.lock /usr/bin/docker exec -u www-data caiwu_app php artisan esign:poll-signatures --quiet >/dev/null 2>&1
EOF
chmod 644 /etc/cron.d/caiwu-jiansun-snapshots
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
docker compose -f compose.jiangling.yml ps
echo "DEPLOYED_SHA=$DEPLOY_SHA"
# Keep the variable for rollback diagnostics without printing any secret.
: "$PREVIOUS_SHA"