为什么:用户下午要部署,但全仓没有任何部署产物;且生产漏配密钥会静默使用代码里的 dev 默认值,
属上线阻塞项(04-部署架构 / 12-上线检查清单)。
- 配置外置(三件):DB_URL、JWT 双密钥(**发现 app-secret 此前没外置,一直用 dev 默认值**)、
MEDIA_SIGNATURE_SECRET、MEDIA_SIGNATURE_REQUIRED、CORS_ALLOWED_ORIGINS、MEDIA_ROOT_DIR
- 新增三份 application-prod.yml:签名强制开启、CORS 收紧、媒资指向挂载点、日志级别
- 新增 ProdSecretGuard(@Profile("prod")):密钥缺失/仍是 dev 默认值/不足 32 位、签名未开、
DB_URL 或 CORS 为空 → 一次列全并终止启动(exit 1)
- 新增 /healthz 轻量健康端点:查明 /actuator/health 的"超时"是旧 jar 未重启所致,项目本就
没有 actuator 依赖,故不引新依赖;同时把 /healthz 加进安全白名单
- Docker 化:多阶段 Dockerfile(非 root、tini、依赖层缓存、ffmpeg 只给 worker)、.dockerignore、
自包含 docker-compose.yml(含 PostgreSQL 18 + Redis + 命名卷 + 按序初始化 sql + healthcheck)
- tools/smoke.sh(失败非 0 退出)+ DEPLOY.md(Compose 主线 / 裸 jar 兜底 / 回滚 / 故障速查)
- 实测:mvn test 83 条全过;两入口 package 通过;prod 缺密钥退出码 1 且报错明确、配齐后正常启动;
冒烟脚本全 PASS;本机无 Docker,镜像与 compose 未经构建验证(已在回执如实标注)
106 lines
4.2 KiB
Bash
Executable File
106 lines
4.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Author: fruit.com.cn
|
||
#
|
||
# 发布后冒烟:确认两个入口"活着 + 文档在线 + 未授权路径按统一信封拒绝"。
|
||
# 任一检查失败 → 非 0 退出(可直接用在部署流水线里)。
|
||
#
|
||
# 用法:
|
||
# tools/smoke.sh # 默认 127.0.0.1:8081 / 127.0.0.1:8080
|
||
# APP_BASE=http://10.0.0.5:8081 ADMIN_BASE=http://10.0.0.5:8080 tools/smoke.sh
|
||
|
||
set -u -o pipefail
|
||
|
||
APP_BASE="${APP_BASE:-http://127.0.0.1:8081}"
|
||
ADMIN_BASE="${ADMIN_BASE:-http://127.0.0.1:8080}"
|
||
TIMEOUT="${SMOKE_TIMEOUT:-8}"
|
||
FAILED=0
|
||
|
||
pass() { printf ' \033[32mPASS\033[0m %s\n' "$1"; }
|
||
fail() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; FAILED=1; }
|
||
|
||
# http_code <url> [跟随重定向]:只取状态码,连不上返回 000
|
||
http_code() {
|
||
local follow="${2:-}"
|
||
if [ "$follow" = "follow" ]; then
|
||
curl -sL -o /dev/null -m "$TIMEOUT" -w '%{http_code}' "$1" 2>/dev/null || echo 000
|
||
else
|
||
curl -s -o /dev/null -m "$TIMEOUT" -w '%{http_code}' "$1" 2>/dev/null || echo 000
|
||
fi
|
||
}
|
||
|
||
# body <url>:取响应体(用于断言统一信封)
|
||
body() {
|
||
curl -s -m "$TIMEOUT" "$1" 2>/dev/null || true
|
||
}
|
||
|
||
# expect_code <期望> <url> <说明> [follow]
|
||
expect_code() {
|
||
local want="$1" url="$2" label="$3" follow="${4:-}" got
|
||
got="$(http_code "$url" "$follow")"
|
||
if [ "$got" = "$want" ]; then
|
||
pass "${label}(${got})"
|
||
else
|
||
fail "${label}:期望 ${want},实际 ${got} —— ${url}"
|
||
fi
|
||
}
|
||
|
||
# expect_body <url> <必须包含的片段> <说明>
|
||
# 只看 HTTP 200 是不够的:本项目未授权时也是 HTTP 200 + 信封 code=401,
|
||
# 所以健康检查/文档必须断言响应体的关键字段。
|
||
expect_body() {
|
||
local url="$1" needle="$2" label="$3" out
|
||
out="$(body "$url")"
|
||
if printf '%s' "$out" | grep -q "$needle"; then
|
||
pass "${label}"
|
||
else
|
||
fail "${label}:期望包含 ${needle},实际:$(printf '%s' "$out" | head -c 120)"
|
||
fi
|
||
}
|
||
|
||
# expect_envelope <url> <说明>:未授权路径必须返回统一响应体(code 字段),而不是挂住/空响应
|
||
expect_envelope() {
|
||
local url="$1" label="$2" out
|
||
out="$(body "$url")"
|
||
if printf '%s' "$out" | grep -q '"code"'; then
|
||
pass "${label}($(printf '%s' "$out" | head -c 80))"
|
||
else
|
||
fail "$label:未返回统一响应体 —— $url,实际:$(printf '%s' "$out" | head -c 120)"
|
||
fi
|
||
}
|
||
|
||
printf '冒烟开始:APP=%s ADMIN=%s\n' "$APP_BASE" "$ADMIN_BASE"
|
||
|
||
printf '\n[1/5] 健康检查 /healthz\n'
|
||
expect_code 200 "$APP_BASE/healthz" "app-api /healthz 可访问"
|
||
expect_body "$APP_BASE/healthz" '"status":"UP"' "app-api /healthz 返回 status=UP"
|
||
expect_code 200 "$ADMIN_BASE/healthz" "admin-api /healthz 可访问"
|
||
expect_body "$ADMIN_BASE/healthz" '"status":"UP"' "admin-api /healthz 返回 status=UP"
|
||
|
||
printf '\n[2/5] 客户端目录接口\n'
|
||
expect_code 200 "$APP_BASE/app/categories" "GET /app/categories"
|
||
expect_code 200 "$APP_BASE/app/home/feed" "GET /app/home/feed"
|
||
expect_body "$APP_BASE/app/categories" '"code":0' "GET /app/categories 返回 code=0"
|
||
|
||
printf '\n[3/5] 接口文档在线\n'
|
||
expect_code 200 "$APP_BASE/v3/api-docs" "app-api /v3/api-docs"
|
||
expect_code 200 "$ADMIN_BASE/v3/api-docs" "admin-api /v3/api-docs"
|
||
expect_body "$APP_BASE/v3/api-docs" '"openapi"' "app-api /v3/api-docs 是 OpenAPI 文档"
|
||
# /doc.html 是 springdoc 的 302 跳转,跟随重定向后应落到 Swagger UI(200)
|
||
expect_code 200 "$APP_BASE/doc.html" "app-api /doc.html(跟随 302)" follow
|
||
|
||
printf '\n[4/5] 未授权路径按统一信封拒绝(不挂住)\n'
|
||
expect_envelope "$APP_BASE/" "app-api /"
|
||
expect_envelope "$ADMIN_BASE/admin/auth/profile" "admin-api 未带令牌访问受保护接口"
|
||
|
||
printf '\n[5/5] 响应体完整性(/app/categories 必须是 {code,data})\n'
|
||
expect_body "$APP_BASE/app/categories" '"data"' "GET /app/categories 带 data 字段"
|
||
# 用错误的 HTTP 方法访问 POST-only 接口:只要是"统一信封"就算合格(当前实现返回 code=500 的系统异常信封)
|
||
expect_envelope "$ADMIN_BASE/admin/auth/login" "admin-api 用 GET 访问 POST-only 接口仍返回统一信封"
|
||
|
||
if [ "$FAILED" -eq 0 ]; then
|
||
printf '\n\033[32m冒烟通过\033[0m:全部检查通过\n'
|
||
exit 0
|
||
fi
|
||
printf '\n\033[31m冒烟失败\033[0m:存在未通过项,请先看上面的 FAIL 行\n'
|
||
exit 1
|