Files
lijiaoqiao/scripts/ci/cross_service_smoke.sh
Your Name 1fec3e981d feat(ci): 实现 Phase 1/2 严格退出标准的所有代码实现
Phase 1 Criterion 4: contract tests 场景清单 → backend-verify.sh --phase1-contract-gate(含四个场景:合法token全链路、吊销拒绝、scope不足拒绝、runtime快速失败),repo_integrity_check.sh 集成调用

Phase 2 Criterion 1: manifest.json 系统(lib/manifest_lib.sh + staging_release_pipeline.sh),run_id 作为硬门禁,manifest_hard_gate_run_id() 验证非空

Phase 2 Criterion 2: superpowers_stage_validate.sh exit 1 条件从 NO_GO 扩展到 CONDITIONAL_GO,staging 硬门禁不再放行条件通过

Phase 2 Criterion 3: DEFERRED 语义修正,CONDITIONAL_GO 不再出现在复审结论选项中;CONDITIONAL_GO 在 pipeline 中强制 exit 1

Phase 2 Criterion 5: cross_service_smoke.sh 从 DESIGN_ONLY 变为可执行(exit 0=PASS/1=FAIL/2=SKIP_LOCAL_PLACEHOLDER),纳入 staging_release_pipeline.sh STEP-03

Phase 2 Criterion 4: 配置分离(已之前落地,本次确认)

环境问题记录: docs/plans/2026-04-21-environmental-issues-log.md
- P3-A: HTTP timeout + cache eviction(需要真实 staging env + env var 热加载支持)
- P3-B/C: /metrics 端点(需要 Prometheus scrape 配置 + 运维介入)
- P3-D: graceful shutdown(需要 staging 流量压测验证)
2026-04-21 12:14:50 +08:00

295 lines
10 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
# scripts/ci/cross_service_smoke.sh
# 跨服务 smoke 测试gateway -> token-runtime -> supply-api
# 退出码语义:
# 0 = PASS真实 staging smoke 通过)
# 1 = FAIL任意链路失败
# 2 = SKIP_LOCAL_PLACEHOLDER本地/mock 输入,非真实 staging 证据)
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
OUT_DIR="${ROOT_DIR}/reports/archive/gate_verification"
mkdir -p "${OUT_DIR}"
TS="$(date +%F_%H%M%S)"
LOG_FILE="${OUT_DIR}/cross_service_smoke_${TS}.log"
REPORT_FILE="${OUT_DIR}/cross_service_smoke_${TS}.md"
# ── 输入环境变量(默认值适配本地 dev ───────────────────────
GATEWAY_URL="${SMOKE_GATEWAY_BASE_URL:-http://127.0.0.1:18080}"
TOK_URL="${SMOKE_TOKEN_RUNTIME_BASE_URL:-http://127.0.0.1:18081}"
SUPPLY_URL="${SMOKE_SUPPLY_API_BASE_URL:-http://127.0.0.1:18082}"
BEARER_TOKEN="${SMOKE_BEARER_TOKEN:-}"
EXPECTED_SCOPE="${SMOKE_EXPECTED_SCOPE:-supply:read supply:write}"
EXPECTED_MODEL="${SMOKE_EXPECTED_MODEL:-}"
ALLOW_LOCAL_PLACEHOLDER="${SMOKE_ALLOW_LOCAL_PLACEHOLDER:-0}"
log() {
echo "$1" | tee -a "${LOG_FILE}"
}
# ── 前置检测:是否是本地 mock/stub 环境 ──────────────────────
is_local_placeholder() {
# 若所有服务 URL 都是 localhost/127.0.0.1,则认为是本地占位
if [[ "${GATEWAY_URL}" == *"localhost"* || "${GATEWAY_URL}" == *"127.0.0.1"* ]] && \
[[ "${TOK_URL}" == *"localhost"* || "${TOK_URL}" == *"127.0.0.1"* ]] && \
[[ "${SUPPLY_URL}" == *"localhost"* || "${SUPPLY_URL}" == *"127.0.0.1"* ]]; then
return 0
fi
return 1
}
# ── Smoke 场景 1全链路健康检查 ─────────────────────────────
smoke_check_services() {
log "[SMOKE-1] 服务健康检查"
local fail=0
for svc in "gateway:${GATEWAY_URL}" "token-runtime:${TOK_URL}" "supply-api:${SUPPLY_URL}"; do
local name="${svc%%:*}"
local url="${svc#*:}"
local health_url="${url}/actuator/health"
local resp
resp="$(curl -sS -m 5 "${health_url}" -w "\n__HTTP_CODE__:%{http_code}" 2>&1)" || true
local code
code="$(echo "${resp}" | grep -o '__HTTP_CODE__.*' | cut -d: -f2 || echo '000')"
if [[ "${code}" == "200" ]]; then
log " [PASS] ${name} health=${code}"
else
log " [FAIL] ${name} health=${code} (URL: ${health_url})"
fail=1
fi
done
return "${fail}"
}
# ── Smoke 场景 2通过 gateway 转发带 token 的受保护请求 ─────
smoke_gateway_protected_request() {
log "[SMOKE-2] Gateway 受保护请求"
# 如果没有提供 bearer token先尝试创建一个 smoke token
if [[ -z "${BEARER_TOKEN}" ]]; then
log " [INFO] No bearer token provided, creating a smoke token"
local create_resp
create_resp="$(curl -sS -m 5 -X POST "${TOK_URL}/api/v1/platform/tokens" \
-H "Content-Type: application/json" \
-d "{\"subject_id\":\"smoke-test-user\",\"tenant_id\":\"smoke-tenant\",\"scope\":\"${EXPECTED_SCOPE}\",\"expires_in\":60}" \
-w "\n__HTTP_CODE__:%{http_code}" 2>&1)" || true
local create_code
create_code="$(echo "${create_resp}" | grep -o '__HTTP_CODE__.*' | cut -d: -f2 || echo '000')"
BEARER_TOKEN="$(echo "${create_resp}" | sed 's/__HTTP_CODE__.*//' | python3 -c "import sys,json; print(json.load(sys.stdin).get('token_id',''))" 2>/dev/null || true)"
if [[ -z "${BEARER_TOKEN}" || "${create_code}" != "201" ]]; then
log " [FAIL] Cannot create smoke token: code=${create_code}"
return 1
fi
log " [INFO] Created smoke token: ${BEARER_TOKEN}"
fi
# 通过 gateway 发送受保护请求gateway -> token-runtime introspect -> supply-api
# 路径GET /api/v1/accounts需要 supply:read scope
local req_resp
req_resp="$(curl -sS -m 10 -X GET "${GATEWAY_URL}/api/v1/accounts" \
-H "Authorization: Bearer ${BEARER_TOKEN}" \
-w "\n__HTTP_CODE__:%{http_code}" 2>&1)" || true
local req_code
req_code="$(echo "${req_resp}" | grep -o '__HTTP_CODE__.*' | cut -d: -f2 || echo '000')"
log " [INFO] Gateway protected request: method=GET path=/api/v1/accounts status=${req_code}"
# 验收:返回 200正常或 401/403token 有效但权限不足,即 token 被正确传递到了 supply-api
# 不能是 502/503token-runtime 不可达)或 404路径错误
if [[ "${req_code}" == "200" || "${req_code}" == "401" || "${req_code}" == "403" ]]; then
log " [PASS] Gateway protected request: token correctly forwarded to supply-api (status=${req_code})"
return 0
elif [[ "${req_code}" == "502" || "${req_code}" == "503" || "${req_code}" == "504" ]]; then
log " [FAIL] Gateway protected request: token-runtime unreachable (status=${req_code})"
return 1
else
log " [FAIL] Gateway protected request: unexpected status=${req_code}"
return 1
fi
}
# ── Smoke 场景 3Supply-api scope 验证 ───────────────────────
smoke_supply_scope_check() {
log "[SMOKE-3] Supply-api scope 验证"
# 创建一个只有 supply:read scope 的 token验证 supply:write 请求被拒绝
local create_resp3
create_resp3="$(curl -sS -m 5 -X POST "${TOK_URL}/api/v1/platform/tokens" \
-H "Content-Type: application/json" \
-d '{"subject_id":"smoke-scope-user","tenant_id":"smoke-tenant","scope":"supply:read","expires_in":60}"' \
-w "\n__HTTP_CODE__:%{http_code}" 2>&1)" || true
local create_code3
create_code3="$(echo "${create_resp3}" | grep -o '__HTTP_CODE__.*' | cut -d: -f2 || echo '000')"
local read_only_token
read_only_token="$(echo "${create_resp3}" | sed 's/__HTTP_CODE__.*//' | python3 -c "import sys,json; print(json.load(sys.stdin).get('token_id',''))" 2>/dev/null || true)"
if [[ -z "${read_only_token}" || "${create_code3}" != "201" ]]; then
log " [SKIP] Cannot create scope-test token"
return 2 # SKIP
fi
# 用 read-only token 尝试 supply:write 操作(通过 gateway
local write_resp
write_resp="$(curl -sS -m 10 -X POST "${GATEWAY_URL}/api/v1/accounts" \
-H "Authorization: Bearer ${read_only_token}" \
-H "Content-Type: application/json" \
-d '{"account_name":"smoke-test-write"}' \
-w "\n__HTTP_CODE__:%{http_code}" 2>&1)" || true
local write_code
write_code="$(echo "${write_resp}" | grep -o '__HTTP_CODE__.*' | cut -d: -f2 || echo '000')"
log " [INFO] supply:read token attempted supply:write: status=${write_code}"
# 验收:应返回 401/403scope 不足)或 400不能是 200
if [[ "${write_code}" == "401" || "${write_code}" == "403" || "${write_code}" == "400" ]]; then
log " [PASS] Scope check: insufficient scope correctly rejected (status=${write_code})"
return 0
elif [[ "${write_code}" == "200" ]]; then
log " [FAIL] Scope check: insufficient scope NOT rejected (got 200)"
return 1
else
log " [WARN] Scope check: unexpected status=${write_code}"
return 1
fi
}
# ── 主流程 ────────────────────────────────────────────────────
log "=========================================="
log "[cross_service_smoke] START"
log "[URLS] gateway=${GATEWAY_URL} tok-runtime=${TOK_URL} supply=${SUPPLY_URL}"
log "=========================================="
SMOKE_PASS=0
SMOKE_FAIL=0
SMOKE_SKIP=0
# 前置检查:是否本地占位
if is_local_placeholder; then
if [[ "${ALLOW_LOCAL_PLACEHOLDER}" != "1" ]]; then
log "[SKIP] Local placeholder environment detected — set SMOKE_ALLOW_LOCAL_PLACEHOLDER=1 to force run"
log "[SKIP] This run will be reported as SKIP_LOCAL_PLACEHOLDER (exit 2)"
cat > "${REPORT_FILE}" <<'EOF'
# Cross-Service Smoke 报告
- 时间戳:${TS}
- 结果:**SKIP_LOCAL_PLACEHOLDER**
- 说明:所有服务 URL 均为 localhost/127.0.0.1,判定为本地占位环境,非真实 staging 证据。
## 状态码语义
| 退出码 | 含义 |
|---|---|
| 0 | PASS — 真实 staging smoke 通过 |
| 1 | FAIL — 任意链路失败 |
| 2 | SKIP_LOCAL_PLACEHOLDER — 本地占位,不计入 release 通过 |
## 建议
1. 在真实 staging 环境运行本脚本。
2. 通过环境变量传入真实服务 URL
```bash
SMOKE_GATEWAY_BASE_URL=https://gateway.staging.internal \
SMOKE_TOKEN_RUNTIME_BASE_URL=https://token-runtime.staging.internal \
SMOKE_SUPPLY_API_BASE_URL=https://supply-api.staging.internal \
bash scripts/ci/cross_service_smoke.sh
```
EOF
log "[RESULT] SKIP_LOCAL_PLACEHOLDER"
exit 2
else
log "[WARN] ALLOW_LOCAL_PLACEHOLDER=1 — running against localhost (results will be marked as local)"
fi
fi
# 执行三个 smoke 场景
set +e
smoke_check_services
case $? in
0) ((SMOKE_PASS++)) ;;
1) ((SMOKE_FAIL++)) ;;
2) ((SMOKE_SKIP++)) ;;
esac
smoke_gateway_protected_request
case $? in
0) ((SMOKE_PASS++)) ;;
1) ((SMOKE_FAIL++)) ;;
2) ((SMOKE_SKIP++)) ;;
esac
smoke_supply_scope_check
case $? in
0) ((SMOKE_PASS++)) ;;
1) ((SMOKE_FAIL++)) ;;
2) ((SMOKE_SKIP++)) ;;
esac
set -e
log ""
log "=========================================="
log "[cross_service_smoke] PASS=${SMOKE_PASS} FAIL=${SMOKE_FAIL} SKIP=${SMOKE_SKIP}"
log "=========================================="
# 生成报告
OVERALL="PASS"
if [[ "${SMOKE_FAIL}" -gt 0 ]]; then
OVERALL="FAIL"
elif [[ "${SMOKE_SKIP}" -gt 0 && "${SMOKE_PASS}" -eq 0 ]]; then
OVERALL="SKIP"
fi
cat > "${REPORT_FILE}" <<EOF
# Cross-Service Smoke 报告
- 时间戳:${TS}
- 整体结果:**${OVERALL}**
- 场景通过:${SMOKE_PASS} / 失败:${SMOKE_FAIL} / 跳过:${SMOKE_SKIP}
- Gateway URL${GATEWAY_URL}
- Token Runtime URL${TOK_URL}
- Supply API URL${SUPPLY_URL}
- Bearer Token${BEARER_TOKEN:0:20}...(已脱敏)
## Smoke 场景
| # | 场景 | 结果 |
|---|---|---|
| 1 | 服务健康检查gateway / token-runtime / supply-api | $([[ ${SMOKE_FAIL} -eq 0 ]] && echo "PASS" || echo "FAIL") |
| 2 | Gateway 受保护请求token 转发验证) | $([[ ${SMOKE_FAIL} -eq 0 ]] && echo "PASS" || echo "FAIL") |
| 3 | Supply-api scope 验证 | $([[ ${SMOKE_FAIL} -eq 0 ]] && echo "PASS" || echo "FAIL") |
## 证据
- 日志:\`${LOG_FILE}\`
- 报告:\`${REPORT_FILE}\`
## 状态码语义(供 CI 解析)
| 退出码 | 含义 |
|---|---|
| 0 | PASS — 真实 staging smoke 通过 |
| 1 | FAIL — 任意链路失败 |
| 2 | SKIP_LOCAL_PLACEHOLDER — 本地占位,不计入 release 通过 |
EOF
log "[RESULT] ${OVERALL}"
log "[INFO] report=${REPORT_FILE}"
if [[ "${OVERALL}" == "FAIL" ]]; then
exit 1
elif [[ "${OVERALL}" == "SKIP" ]]; then
exit 2
fi