ci(retag/deploy): 增加镜像内容校验,防止静默部署旧/损坏镜像
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 3m58s
CI/CD Pipeline / Check push changed paths (push) Successful in 4m19s
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4m22s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 4m43s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 5m5s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 5m15s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 5m18s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m51s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m7s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m9s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m57s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m18s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 8m31s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m23s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 2m28s
AI Code Review / AI Code Review (pull_request) Successful in 11m10s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Successful in 2m23s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / Integration Tests (push) Successful in 4m31s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m32s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m23s
CI/CD Pipeline / Unit Tests (push) Successful in 17m40s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Successful in 5m1s

retag_skipped_image.sh 加固:
- 源镜像 layers 有效性检查(layers=0 拒绝 retag)
- 镜像创建时间审计(>72h 输出警告)
- digest 审计记录,方便溯源

ci_staging_deploy.sh 加固:
- pull 后逐服务校验镜像 layers 有效性
- 记录每个镜像的 digest/size/created
- 对比上次部署 manifest,标注变化状态
- 写入 deploy-manifest.json 供下次对比
- 校验失败直接阻断部署

concurrency 保持 cancel-in-progress: true(不变)
生产发版链路不动
This commit was merged in pull request #1569.
This commit is contained in:
xiaoxia
2026-08-31 12:01:19 +08:00
parent 107a391c47
commit f7693baeae
2 changed files with 135 additions and 0 deletions
+51
View File
@@ -50,8 +50,59 @@ if [ -z "$SRC_TAG" ]; then
fi
echo "✅ 源镜像: ${IMAGE}:${SRC_TAG}"
# ====== 镜像内容校验(CI 加固 - 防止静默部署旧/损坏镜像) ======
echo ""
echo "--- 镜像内容校验 ---"
# 1. 检查镜像 layers 有效性
LAYERS=$(docker inspect --format='{{len .RootFS.Layers}}' "${IMAGE}:${SRC_TAG}" 2>/dev/null || echo "0")
echo "源镜像 layers 数量: $LAYERS"
if [ "$LAYERS" -eq 0 ]; then
echo "❌ 源镜像无有效 layers,可能为损坏镜像,拒绝 retag"
exit 1
fi
# 2. 镜像创建时间检查
CREATED=$(docker inspect --format='{{.Created}}' "${IMAGE}:${SRC_TAG}")
echo "源镜像创建时间: $CREATED"
# 解析创建时间距现在多少小时
CREATED_EPOCH=$(date -d "$CREATED" +%s 2>/dev/null || echo "0")
NOW_EPOCH=$(date +%s)
AGE_HOURS=$(( (NOW_EPOCH - CREATED_EPOCH) / 3600 ))
echo "源镜像年龄: ${AGE_HOURS}小时"
if [ "$AGE_HOURS" -gt 72 ]; then
echo "⚠️ 警告: 源镜像已超过 72 小时,可能存在分支 tag 未更新的风险"
echo " 请检查最近几次 CI 是否对该服务成功构建过"
fi
# 3. Digest 审计记录
SRC_DIGEST=$(docker inspect --format='{{.Id}}' "${IMAGE}:${SRC_TAG}")
echo "源镜像 ID: $SRC_DIGEST"
echo "--- 镜像内容校验通过 ---"
echo ""
docker tag "${IMAGE}:${SRC_TAG}" "${NEW_REF}"
# 推新 SHA tag;分支 tag 若指向的就是源 digest 则无需重复,失败可忽略
docker push "${NEW_REF}"
NEW_DIGEST=$(docker inspect --format='{{.Id}}' "${NEW_REF}")
echo "✅ retag 推送完成: ${NEW_REF} (from ${SRC_TAG})"
echo " 源 digest: ${SRC_DIGEST}"
echo " 新 tag digest: ${NEW_DIGEST}"
# 写入 retag 审计记录(供 deploy 步骤参考)
RETAG_LOG="/tmp/retag-audit-$(date +%s).txt"
cat > "$RETAG_LOG" <<EOF
timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
image=${IMAGE}
src_tag=${SRC_TAG}
new_tag=${NEW_TAG}
src_digest=${SRC_DIGEST}
new_digest=${NEW_DIGEST}
age_hours=${AGE_HOURS}
EOF
echo "审计记录: $RETAG_LOG"
+84
View File
@@ -260,6 +260,90 @@ fi
echo "All images pulled."
# ====== 镜像内容校验(CI 加固 - 防止静默部署损坏/过期镜像) ======
echo ""
echo "=========================================="
echo " 镜像内容校验"
echo "=========================================="
VERIFY_FAILED=0
DEPLOY_MANIFEST="${GENERATED_DIR}/deploy-manifest.json"
# 读取上次部署的 manifest(用于对比)
PREV_MANIFEST=""
if [ -f "$DEPLOY_MANIFEST" ]; then
PREV_MANIFEST=$(cat "$DEPLOY_MANIFEST")
echo "上次部署 manifest 存在"
fi
NEW_MANIFEST_LINES=""
for svc in api worker web; do
img_var="REGISTRY_$(echo $svc | tr '[:lower:]' '[:upper:]')"
img_val=$(eval echo "\$$img_var")
# 1. 检查镜像是否存在
if ! docker image inspect "$img_val" >/dev/null 2>&1; then
echo "$svc: 镜像不存在 ($img_val)"
VERIFY_FAILED=$((VERIFY_FAILED + 1))
continue
fi
# 2. 检查 layers 有效性
LAYER_COUNT=$(docker inspect --format='{{len .RootFS.Layers}}' "$img_val" 2>/dev/null || echo "0")
if [ "$LAYER_COUNT" -eq 0 ]; then
echo "$svc: 镜像无有效 layers ($img_val)"
VERIFY_FAILED=$((VERIFY_FAILED + 1))
continue
fi
# 3. 获取 digest 和创建时间
IMG_ID=$(docker inspect --format='{{.Id}}' "$img_val")
IMG_CREATED=$(docker inspect --format='{{.Created}}' "$img_val")
IMG_SIZE=$(docker inspect --format='{{.Size}}' "$img_val")
echo "$svc: ${LAYER_COUNT} layers, size=${IMG_SIZE}, created=${IMG_CREATED}"
echo " id: $IMG_ID"
# 4. 对比上次部署
CHANGED="unchanged"
if [ -n "$PREV_MANIFEST" ]; then
PREV_ID=$(echo "$PREV_MANIFEST" | grep "\"${svc}_id\"" | sed 's/.*: *"\(.*\)".*/\1/' 2>/dev/null || echo "")
if [ -n "$PREV_ID" ] && [ "$PREV_ID" != "$IMG_ID" ]; then
CHANGED="changed"
elif [ -n "$PREV_ID" ] && [ "$PREV_ID" = "$IMG_ID" ]; then
CHANGED="unchanged"
else
CHANGED="unknown"
fi
else
CHANGED="first-deploy"
fi
echo " vs last deploy: $CHANGED"
NEW_MANIFEST_LINES="${NEW_MANIFEST_LINES} \"${svc}_id\": \"${IMG_ID}\",
\"${svc}_created\": \"${IMG_CREATED}\",
\"${svc}_layers\": ${LAYER_COUNT},
"
done
if [ "$VERIFY_FAILED" -gt 0 ]; then
echo ""
echo "ERROR: $VERIFY_FAILED 个镜像校验失败,拒绝部署"
exit 1
fi
# 写入新 manifest
cat > "$DEPLOY_MANIFEST" <<MANIFEST_EOF
{
"deployed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"image_tag": "$IMAGE_TAG",
${NEW_MANIFEST_LINES} "verified": true
}
MANIFEST_EOF
echo ""
echo "部署 manifest 已更新: $DEPLOY_MANIFEST"
echo "=========================================="
echo ""
echo "Backing up legacy assets from current web container..."
if docker inspect xiaoxia-web-staging >/dev/null 2>&1; then
_tmpdir="/tmp/legacy-assets-$$"