Files
photo-live/scripts/prod-smoke.sh
T
浅念 c26d6b9a73 fix(deploy): review fixes — smoke photoId field, web healthcheck wget, outbox table name, cdn key shape
Reviewer findings on p5-prep batch:
1. BLOCKING: prod-smoke.sh visibility grep used "id" but API items
   serialize photoId (cn public.service / ov public-ov.service) — every
   healthy deployment would false-FAIL release gate ④. 4 sites fixed.
2. web/web-ov healthchecks used curl on nginx:alpine (absent) → perpetual
   false-unhealthy; switched to busybox wget.
3. RUNBOOK rollback rule cited nonexistent sync_outbox → outbox_events.
4. cdn-checklist invalidation sample key shape aligned to actual
   variantsObjectKey layout (.../albums/{albumId}/variants/{photoId}/{version}/{variant}.jpg).

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-21 02:03:44 +08:00

187 lines
13 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.
#!/usr/bin/env bash
# scripts/prod-smoke.sh —— 生产四点冒烟(P5-prep Task 2;RUNBOOK.md §2.3 的判定口径)。
#
# 用法:
# scripts/prod-smoke.sh <BASE_CN> <BASE_OV> <ACCOUNT> <PASSWORD> [选项]
# BASE_CN/BASE_OV 两地域 API 基址(https://photo.example.cn 形态;末尾不带 /v1)
# --skip-ov ov 未部署时跳过 ov 段(--ov 为其别名,同义)
# --dry-run 只打印将执行的调用序列,不发任何请求(本地校验脚本逻辑用)
# --wait-seconds N 公开可见轮询上限秒(默认 60;worker 处理 + cn→ov 推送 2s
# 间隔通常几秒内完成,60s 覆盖慢盘/排队余量)
#
# 覆盖四点:①两地域 /v1/health ②登录(真实密码)③建时间戳相册 → presign 上传
# 真实 JPEG 字节(内置 800x600 测试图,刻意不用 1x1——避免变体流水线踩最小尺寸
# 边界)→ complete → ④公开读轮询至可见(cn)+ ov 侧同步后公开读确认。
# 结束打印 PASS/FAIL 摘要并清理(软删测试照片:公开端点即刻不可见;相册行保留
# 时间戳名供审计——相册无删除端点,照片软删后公开列表即空)。
# 依赖:bash + curl + openssl + base64(无 jq/node)。退出码:0=全过,1=有 FAIL,2=用法错。
set -u
# ── 参数 ──
BASE_CN="${1:-}"; BASE_OV="${2:-}"; ACCOUNT="${3:-}"; PASSWORD="${4:-}"
[ "$#" -ge 4 ] && shift 4
SKIP_OV=0; DRY_RUN=0; WAIT_SECONDS=60; PREV_ARG=""
for arg in "$@"; do
case "$arg" in
--skip-ov|--ov) SKIP_OV=1 ;;
--dry-run) DRY_RUN=1 ;;
--wait-seconds) : ;;
*) [ "$PREV_ARG" = "--wait-seconds" ] && WAIT_SECONDS="$arg" ;;
esac
PREV_ARG="$arg"
done
# 内置测试图:800x600 JPEG(quality 55,3118 字节)——base64 内嵌,跨机器无依赖。
JPEG_B64='/9j/2wBDAA4KCw0LCQ4NDA0QDw4RFiQXFhQUFiwgIRokNC43NjMuMjI6QVNGOj1OPjIySGJJTlZYXV5dOEVmbWVabFNbXVn/2wBDAQ8QEBYTFioXFypZOzI7WVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVn/wAARCAJYAyADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAX/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCsAkKIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//2Q=='
PASS=0; FAIL=0; FAILED_STEPS=""
TOKEN=""; ALBUM_ID=""; PUBLIC_TOKEN=""; PHOTO_ID=""; UPLOAD_ID=""; CRED_URL=""
HTTP_CODE="000"; BODY=""
JPG_FILE="$(mktemp "${TMPDIR:-/tmp}/photo-smoke-XXXXXX.jpg")"
trap cleanup EXIT
step_pass() { PASS=$((PASS + 1)); echo " [PASS] $1"; }
step_fail() { FAIL=$((FAIL + 1)); FAILED_STEPS="${FAILED_STEPS}${1}; "; echo " [FAIL] $1"; }
# json_escape <s>:处理反斜杠/双引号(密码与标题的常见特殊字符;控制字符不入参)。
json_escape() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'; }
# extract_str <json> <key>:取扁平响应里 "key":"value" 的 value(本脚本响应均此形态)。
extract_str() { printf '%s' "$1" | sed -n "s/.*\"$2\":\"\([^\"]*\)\".*/\1/p" | head -n 1; }
# http <method> <url> <expected-codes(逗号分隔)> [json-body]
# 成功时置 HTTP_CODE/BODY 并返回 0。dry-run 下只打印调用并返回 0。
http() {
local method="$1" url="$2" expect="$3" body="${4:-}"
if [ "$DRY_RUN" -eq 1 ]; then
echo " curl -sS -X $method '$url'${body:+ -d '$body'}${TOKEN:+ -H 'Authorization: Bearer <token>'}"
HTTP_CODE="dry"; BODY=""
return 0
fi
local tmp; tmp="$(mktemp "${TMPDIR:-/tmp}/photo-smoke-body-XXXXXX")"
local -a args=(-sS -X "$method" "$url" -H 'Content-Type: application/json' -o "$tmp" -w '%{http_code}')
[ -n "$TOKEN" ] && args+=(-H "Authorization: Bearer $TOKEN")
if [ -n "$body" ]; then
HTTP_CODE="$(curl "${args[@]}" -d "$body" 2>/dev/null || echo 000)"
else
HTTP_CODE="$(curl "${args[@]}" 2>/dev/null || echo 000)"
fi
BODY="$(cat "$tmp" 2>/dev/null || true)"
rm -f "$tmp"
case ",$expect," in
*",$HTTP_CODE,"*) return 0 ;;
*) return 1 ;;
esac
}
cleanup() {
if [ "$DRY_RUN" -eq 0 ] && [ -n "$PHOTO_ID" ] && [ -n "$TOKEN" ] && [ "$TOKEN" != "dry" ]; then
echo "-- 清理:软删测试照片 $PHOTO_ID(公开列表即刻为空)"
if http DELETE "$BASE_CN/v1/photos/$PHOTO_ID" 204; then step_pass "cleanup 软删照片"; else step_fail "cleanup 软删照片 (HTTP=$HTTP_CODE)"; fi
fi
rm -f "$JPG_FILE"
}
echo "== photo-live 生产冒烟 =="
echo " cn=$BASE_CN ov=$BASE_OV account=$ACCOUNT skip_ov=$SKIP_OV dry_run=$DRY_RUN"
if [ -z "$BASE_CN" ] || [ -z "$ACCOUNT" ] || [ -z "$PASSWORD" ] || { [ "$SKIP_OV" -eq 0 ] && [ -z "$BASE_OV" ]; }; then
echo "用法: $0 <BASE_CN> <BASE_OV> <ACCOUNT> <PASSWORD> [--skip-ov|--ov] [--dry-run] [--wait-seconds N]" >&2
exit 2
fi
printf '%s' "$JPEG_B64" | base64 -d >"$JPG_FILE" 2>/dev/null || { echo "base64 解码失败(需 GNU base64)" >&2; exit 2; }
JPEG_BYTES=$(wc -c <"$JPG_FILE" | tr -d ' ')
TS=$(date +%Y%m%d%H%M%S)
echo "-- ① 健康检查(两地域)"
if http GET "$BASE_CN/v1/health" 200; then step_pass "cn /v1/health"; else step_fail "cn /v1/health (HTTP=$HTTP_CODE)"; fi
if [ "$SKIP_OV" -eq 0 ]; then
if http GET "$BASE_OV/v1/health" 200; then step_pass "ov /v1/health"; else step_fail "ov /v1/health (HTTP=$HTTP_CODE)"; fi
fi
echo "-- ② 登录(真实密码,会话走 Bearer)"
if [ "$DRY_RUN" -eq 1 ]; then
http POST "$BASE_CN/v1/auth/password" 200 \
"{\"account\":\"$(json_escape "$ACCOUNT")\",\"password\":\"$(json_escape "$PASSWORD")\"}"
TOKEN="<dry-run-token>"
else
if http POST "$BASE_CN/v1/auth/password" 200 \
"{\"account\":\"$(json_escape "$ACCOUNT")\",\"password\":\"$(json_escape "$PASSWORD")\"}"; then
TOKEN=$(extract_str "$BODY" sessionToken)
fi
fi
if [ -n "$TOKEN" ]; then
step_pass "登录(sessionToken 获取)"
else
step_fail "登录 (HTTP=$HTTP_CODE BODY=$BODY)"
echo "== 摘要:PASS=$PASS FAIL=$FAIL 失败项: $FAILED_STEPS =="; echo "RESULT: FAIL"
exit 1
fi
echo "-- ③ 建时间戳相册 → presign 上传 → complete"
if http POST "$BASE_CN/v1/albums" 201 \
"{\"translations\":[{\"locale\":\"zh-CN\",\"title\":\"smoke-$TS\",\"description\":\"prod-smoke 自动冒烟\"}],\"defaultLocale\":\"zh-CN\",\"startAt\":\"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)\"}"; then
ALBUM_ID=$(extract_str "$BODY" id); PUBLIC_TOKEN=$(extract_str "$BODY" publicToken)
fi
if [ -n "$ALBUM_ID" ] && [ -n "$PUBLIC_TOKEN" ]; then
step_pass "建相册(publicToken=$PUBLIC_TOKEN)"
elif [ "$DRY_RUN" -eq 1 ]; then
ALBUM_ID="<albumId>"; PUBLIC_TOKEN="<publicToken>"; step_pass "建相册(dry-run 占位)"
else
step_fail "建相册 (HTTP=$HTTP_CODE BODY=$BODY)"
fi
if http POST "$BASE_CN/v1/uploads" 201 \
"{\"albumId\":\"$ALBUM_ID\",\"clientTaskId\":\"smoke-$TS\",\"filename\":\"smoke.jpg\",\"bytes\":$JPEG_BYTES,\"contentType\":\"image/jpeg\"}"; then
UPLOAD_ID=$(extract_str "$BODY" uploadId); PHOTO_ID=$(extract_str "$BODY" photoId); CRED_URL=$(extract_str "$BODY" url)
fi
if [ -n "$UPLOAD_ID" ] && [ -n "$CRED_URL" ]; then
step_pass "presign 凭证(uploadId=$UPLOAD_ID)"
elif [ "$DRY_RUN" -eq 1 ]; then
UPLOAD_ID="<uploadId>"; PHOTO_ID="<photoId>"; CRED_URL="<presigned-url>"; step_pass "presign 凭证(dry-run 占位)"
else
step_fail "presign (HTTP=$HTTP_CODE BODY=$BODY)"
fi
if [ "$DRY_RUN" -eq 1 ]; then
echo " curl -sS -X PUT '$CRED_URL' -H 'Content-Type: image/jpeg' --data-binary @$JPG_FILE ($JPEG_BYTES 字节)"
echo " (complete → cn/ov 公开读轮询逻辑与真实流程一致,见脚本 --dry-run 外执行)"
else
PUT_CODE="$(curl -sS -X PUT "$CRED_URL" -H 'Content-Type: image/jpeg' --data-binary @"$JPG_FILE" -o /dev/null -w '%{http_code}' 2>/dev/null || echo 000)"
case "$PUT_CODE" in
2*) step_pass "OSS presign PUT($JPEG_BYTES 字节真实上传)" ;;
*) step_fail "OSS PUT (HTTP=$PUT_CODE)" ;;
esac
if http POST "$BASE_CN/v1/uploads/$UPLOAD_ID/complete" 200; then step_pass "complete(进入 worker 处理)"; else step_fail "complete (HTTP=$HTTP_CODE BODY=$BODY)"; fi
fi
echo "-- ④ 公开读轮询至可见(≤${WAIT_SECONDS}s;worker 变体流水线 + 发布事务为异步)"
if [ "$DRY_RUN" -eq 1 ]; then
echo " 轮询 GET $BASE_CN/v1/public/albums/$PUBLIC_TOKEN/photos(间隔 2s)直到响应含 \"photoId\":\"$PHOTO_ID\""
else
SEEN_CN=0; WAITED=0
while [ "$WAITED" -le "$WAIT_SECONDS" ]; do
http GET "$BASE_CN/v1/public/albums/$PUBLIC_TOKEN/photos" 200 || true
if [ -n "$PHOTO_ID" ] && printf '%s' "$BODY" | grep -q "\"photoId\":\"$PHOTO_ID\""; then SEEN_CN=1; break; fi
sleep 2; WAITED=$((WAITED + 2))
done
if [ "$SEEN_CN" -eq 1 ]; then step_pass "cn 公开可见(${WAITED}s)"; else step_fail "cn 公开可见超时 ${WAIT_SECONDS}s(查 worker 日志)"; fi
fi
if [ "$SKIP_OV" -eq 0 ]; then
echo "-- ④' ov 侧公开读确认(cn→ov 同步:推送 2s 间隔 + 对账 5min 兜底)"
if [ "$DRY_RUN" -eq 1 ]; then
echo " 轮询 GET $BASE_OV/v1/public/albums/$PUBLIC_TOKEN/photos(间隔 2s)直到响应含 \"photoId\":\"$PHOTO_ID\""
else
SEEN_OV=0; WAITED=0
while [ "$WAITED" -le "$WAIT_SECONDS" ]; do
http GET "$BASE_OV/v1/public/albums/$PUBLIC_TOKEN/photos" 200 || true
if [ -n "$PHOTO_ID" ] && printf '%s' "$BODY" | grep -q "\"photoId\":\"$PHOTO_ID\""; then SEEN_OV=1; break; fi
sleep 2; WAITED=$((WAITED + 2))
done
if [ "$SEEN_OV" -eq 1 ]; then step_pass "ov 公开可见(同步链路端到端,${WAITED}s)"; else step_fail "ov 公开可见超时 ${WAIT_SECONDS}s(查 sync 日志 / RUNBOOK §5.1)"; fi
fi
fi
echo "== 摘要:PASS=$PASS FAIL=$FAIL ${FAILED_STEPS:+失败项: $FAILED_STEPS}=="
if [ "$FAIL" -eq 0 ]; then echo "RESULT: PASS"; else echo "RESULT: FAIL"; exit 1; fi