visionA/scripts/deploy-stage-v2.sh
jim800121chen fad17ddde9 fix(deploy): scripts/deploy-stage-v2.sh — 修兩個全形右括號 typo 卡 shell parsing
兩處 `\$STAGE_VERSION)` / `\$status)` 含 UTF-8 U+FF09 全形右括號(mojibake / 中文輸入法殘留);shell 把全形 `)` 當 var name 一部分、解析成 `$STAGE_VERSION)` / `$status)` 變數、unbound variable 中斷 script。

為什麼今天才暴露:5/9 第一次 v2 trial 卡在 build context 上傳階段(VPN 下 docker daemon 大 POST hang)、從沒跑到 line 120 / 196。今天 VPN 順、第一次 build + deploy 整段跑通才撞兩個 typo。

修法:兩處全形 `()` 改半形 `()`、不影響日誌可讀性。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:49:30 +08:00

214 lines
8.1 KiB
Bash
Executable File
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.

#!/usr/bin/env bash
#
# visionA — Stage Deploy v2remote build 模式,仿 edge-ai-platform/scripts/deploy-docker.sh
#
# 為什麼有 v2
# v1deploy-stage.sh走 docker save | gzip | docker load 模式,需要把 81MB
# tarball 一次性 POST 到 stage docker daemon `/images/load`。經驗證 VPN 下
# docker daemon 對單一大 POST 有 read timeout / hang 問題5/4 卡 30+ 分、
# 5/9 i/o timeout / 也卡 30 分),公司網段直連才可靠。
#
# v2 改用 `DOCKER_HOST=stage docker buildx build .` — multi-stage build
# 完全在 stage daemon 上執行:
# - 跨網路只傳 build context~44 MBstreaming 上傳VPN 友善)
# - alpine base / nodejs apk / go mod download / pnpm install 都是 stage
# daemon 自己 pull / 抓(公司內網 → docker hub / npm registry
# - layer cache 留在 stage daemon、後續 deploy 只重 build 改動的 layer
#
# 流程(仿 edge-ai-platform deploy-docker.sh
# 0. Pre-flightdocker buildx、git status
# 1. DOCKER_HOST=stage docker buildx buildmulti-stage 全在 stage 跑)
# 2. DOCKER_HOST=stage docker compose up -d同 v1
# 3. Verifycontainer status + healthz
#
# 跟 v1 (deploy-stage.sh) 的差別:
# - v1本機 buildx --load → save | gzip | load → compose up
# - v2直接 DOCKER_HOST=stage buildx build → compose up沒 save/load 步驟)
#
# 用法:
# bash scripts/deploy-stage-v2.sh # full deploy
# bash scripts/deploy-stage-v2.sh --skip-build # 不重 build只 compose up
# bash scripts/deploy-stage-v2.sh --no-deploy # build 完不 compose up驗證 build
# bash scripts/deploy-stage-v2.sh --help
set -euo pipefail
# ──────────── 路徑 ────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
DOCKERFILE="$PROJECT_ROOT/docker/Dockerfile.stage"
COMPOSE_FILE="$PROJECT_ROOT/docker-compose.stage.yml"
# ──────────── stage 設定 ────────────
DOCKER_REMOTE="${DOCKER_HOST:-tcp://192.168.0.130:2375}"
IMAGE_NAME="visiona"
IMAGE_TAG="stage"
IMAGE_REF="${IMAGE_NAME}:${IMAGE_TAG}"
STAGE_DOMAIN="stage-9527.innovedus.com"
STAGE_PORT="9527"
HEALTHZ_URL="https://${STAGE_DOMAIN}:${STAGE_PORT}/healthz"
# ──────────── colors ────────────
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
CYAN='\033[0;36m'
BLUE='\033[0;34m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
step() { echo -e "\n${CYAN}=== $* ===${NC}\n"; }
hint() { echo -e "${BLUE}[HINT]${NC} $*"; }
show_help() {
cat <<'HELP'
visionA — Stage Deploy v2 (remote build 模式)
Usage:
bash scripts/deploy-stage-v2.sh [OPTIONS]
Options:
--skip-build Skip build, just compose upimage 已 build 過)
--no-deploy Build only, do not compose up
--help Show this help
Environment:
DOCKER_HOST Remote Docker daemon (default: tcp://192.168.0.130:2375)
Examples:
bash scripts/deploy-stage-v2.sh
bash scripts/deploy-stage-v2.sh --skip-build
bash scripts/deploy-stage-v2.sh --no-deploy
After deployment:
https://stage-9527.innovedus.com:9527/
HELP
}
# ──────────── parse args ────────────
SKIP_BUILD=false
NO_DEPLOY=false
while [ $# -gt 0 ]; do
case "$1" in
--skip-build) SKIP_BUILD=true; shift ;;
--no-deploy) NO_DEPLOY=true; shift ;;
--help|-h) show_help; exit 0 ;;
*) error "Unknown option: $1 (use --help)" ;;
esac
done
# ──────────── pre-flight ────────────
step "0/3 Pre-flight checks"
command -v docker >/dev/null 2>&1 || error "docker 未安裝"
[ -f "$DOCKERFILE" ] || error "找不到 Dockerfile.stage$DOCKERFILE"
[ -f "$COMPOSE_FILE" ] || error "找不到 docker-compose.stage.yml$COMPOSE_FILE"
# 確認 stage daemon 連得上v2 必須能連、v1 也是)
info "確認 stage docker daemon 連線:$DOCKER_REMOTE"
if ! DOCKER_HOST="$DOCKER_REMOTE" docker version --format '{{.Server.Version}}' >/dev/null 2>&1; then
error "無法連到 stage docker daemon @ $DOCKER_REMOTE
提示:
- 公司內網需直連 192.168.0.130:2375
- VPN 下小流量 OKversion / images但大流量會卡
- v2 模式跨網路傳 ~44 MB build context比 v1 友善但仍非 100% 保證"
fi
STAGE_VERSION=$(DOCKER_HOST="$DOCKER_REMOTE" docker version --format '{{.Server.Version}}' 2>/dev/null || echo unknown)
info "stage daemon 連線 OK (server version: $STAGE_VERSION)"
# git 狀態warn only
cd "$PROJECT_ROOT"
GIT_SHA="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
TIMESTAMPED_TAG="${IMAGE_TAG}-${TIMESTAMP}-${GIT_SHA}"
if ! git diff-index --quiet HEAD -- 2>/dev/null; then
warn "git working tree 有未 commit 的變更stage build 仍會繼續)"
fi
info "Project root : $PROJECT_ROOT"
info "Docker remote : $DOCKER_REMOTE"
info "Image : $IMAGE_REF"
info "Timestamp tag : ${IMAGE_NAME}:${TIMESTAMPED_TAG}"
info "Git SHA : $GIT_SHA"
# ──────────── build (在 stage daemon 上) ────────────
if [ "$SKIP_BUILD" = false ]; then
step "1/3 Remote build (DOCKER_HOST=stage docker buildx build)"
info "build 完全在 stage daemon 上執行;只跨網路傳 build context (~44 MB streaming)"
# 用 buildxbuildx 預設 builder 是 default = remote daemon driver
# 不需要 --load / --pushbuild 完 image 直接留在 stage daemon
# 不需要 --platformDOCKER_HOST 指向 stage daemon、stage 是 linux/amd64、native build
cd "$PROJECT_ROOT"
DOCKER_HOST="$DOCKER_REMOTE" docker build \
-f "$DOCKERFILE" \
-t "${IMAGE_NAME}:${IMAGE_TAG}" \
-t "${IMAGE_NAME}:${TIMESTAMPED_TAG}" \
.
info "build 完成 — stage daemon 上已有 ${IMAGE_NAME}:${IMAGE_TAG} + ${IMAGE_NAME}:${TIMESTAMPED_TAG}"
else
info "skip 1/3--skip-build— 假設 ${IMAGE_NAME}:${IMAGE_TAG} 已存在於 stage daemon"
fi
# ──────────── deploy ────────────
if [ "$NO_DEPLOY" = true ]; then
info "skip 2/3, 3/3--no-deploy"
info "下一步:執行 bash scripts/deploy-stage-v2.sh --skip-build 完成 deploy"
exit 0
fi
step "2/3 Deploy via docker compose"
DOCKER_HOST="$DOCKER_REMOTE" docker compose \
-f "$COMPOSE_FILE" \
--project-directory "$PROJECT_ROOT" \
-p visiona-stage \
up -d --remove-orphans
info "container 已啟動,等 healthcheck最多 60s..."
# ──────────── verify ────────────
step "3/3 Verify"
DOCKER_HOST="$DOCKER_REMOTE" docker ps --filter name=visiona --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
deadline=$(( $(date +%s) + 60 ))
healthy=false
status=unknown
while [ "$(date +%s)" -lt "$deadline" ]; do
status=$(DOCKER_HOST="$DOCKER_REMOTE" docker inspect --format='{{.State.Health.Status}}' visiona 2>/dev/null || echo "unknown")
if [ "$status" = "healthy" ]; then
healthy=true
break
fi
sleep 3
done
if [ "$healthy" = true ]; then
info "container healthy ✓"
else
warn "container healthcheck 超時 (status=$status) — 注意 5/1 已知 healthcheck 從 container 內被 nginx server_name 擋成 444 是 false negative外部 healthz 才是真實狀態"
fi
if curl -fsS --max-time 10 "$HEALTHZ_URL" >/dev/null 2>&1; then
info "對外 healthz ✓ ($HEALTHZ_URL)"
else
warn "對外 healthz 失敗:$HEALTHZ_URL"
warn "查看 log: DOCKER_HOST=$DOCKER_REMOTE docker logs visiona --tail 100"
fi
echo ""
echo -e "${GREEN}=== Deploy v2 完成 ===${NC}"
echo ""
info "URL : https://${STAGE_DOMAIN}:${STAGE_PORT}/"
info "Logs: DOCKER_HOST=$DOCKER_REMOTE docker logs -f visiona"
echo ""
hint "Rollback hintDOCKER_HOST=$DOCKER_REMOTE docker tag visiona:stage-<old-tag> visiona:stage && bash scripts/deploy-stage-v2.sh --skip-build"
echo ""