Files
Nexus/deploy/pre_deploy_check.sh
T
2026-05-26 10:02:14 +08:00

236 lines
9.0 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.
#!/bin/bash
# Nexus Pre-deploy Gate Check (v2 — 7 gates)
# 在部署前强制检查 7 道门控,任何一道不过则阻止部署
#
# 门控1: CHANGELOG — docs/changelog/ 下必须有今天的 .md 文件(且行数≥10)
# 门控2: AUDIT — docs/audit/ 下必须有今天的审计记录(且包含关键段落)
# 门控3: TEST — tests/test_api.py 必须存在且全通过
# 门控4: LINT — ruff check server/ 零错误
# 门控5: IMPORT — python -c "import server.main" 成功
# 门控6: SECURITY — bandit -r server/ 无 HIGH/MEDIUM
# 门控7: REVIEW — 审计文件包含 Closure表+文件清单+DoD,且文件清单与 git diff 交叉验证
#
# 用法: bash deploy/pre_deploy_check.sh
# 退出码: 0=全部通过, 1=至少一道门未过
set -euo pipefail
DEPLOY_DIR="${NEXUS_DEPLOY_DIR:-/opt/nexus}"
TODAY=$(date +%Y-%m-%d)
GATES_PASSED=0
GATES_TOTAL=7
BLOCKED=0
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
echo "========================================"
echo " Nexus Pre-deploy Gate Check (v2)"
echo " Date: ${TODAY}"
echo " Gates: ${GATES_TOTAL}"
echo "========================================"
echo ""
# ── Gate 1: Changelog (存在 + 行数≥10) ──
echo -n "Gate 1/7: Changelog ... "
CHANGELOG_DIR="${DEPLOY_DIR}/docs/changelog"
if ls "${CHANGELOG_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
FILE=$(ls "${CHANGELOG_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
LINES=$(wc -l < "${FILE}")
if [ "${LINES}" -ge 10 ]; then
echo -e "${GREEN}PASS${NC}"
echo " └─ Found: $(basename "${FILE}") (${LINES} lines)"
GATES_PASSED=$((GATES_PASSED + 1))
else
echo -e "${RED}BLOCK${NC}"
echo " └─ File exists but only ${LINES} lines (need ≥10)"
echo " └─ Changelog must have substance, not just a placeholder"
BLOCKED=1
fi
else
echo -e "${RED}BLOCK${NC}"
echo " └─ No changelog file found: ${CHANGELOG_DIR}/${TODAY}-*.md"
echo " └─ Create changelog before deploying"
BLOCKED=1
fi
# ── Gate 2: Audit (存在 + 关键段落) ──
echo -n "Gate 2/7: Audit ... "
AUDIT_DIR="${DEPLOY_DIR}/docs/audit"
if ls "${AUDIT_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
FILE=$(ls "${AUDIT_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
# 检查关键段落是否存在
HAS_STEP3=$(grep -c "Step 3" "${FILE}" 2>/dev/null || echo 0)
HAS_CLOSURE=$(grep -c "Closure" "${FILE}" 2>/dev/null || echo 0)
HAS_DOD=$(grep -c "DoD" "${FILE}" 2>/dev/null || echo 0)
if [ "${HAS_STEP3}" -ge 1 ] && [ "${HAS_CLOSURE}" -ge 1 ] && [ "${HAS_DOD}" -ge 1 ]; then
echo -e "${GREEN}PASS${NC}"
echo " └─ Found: $(basename "${FILE}") (Step3✓ Closure✓ DoD✓)"
GATES_PASSED=$((GATES_PASSED + 1))
else
echo -e "${RED}BLOCK${NC}"
echo " └─ Audit file exists but missing required sections:"
[ "${HAS_STEP3}" -eq 0 ] && echo " └─ Missing: Step 3 (规则扫描)"
[ "${HAS_CLOSURE}" -eq 0 ] && echo " └─ Missing: Closure表"
[ "${HAS_DOD}" -eq 0 ] && echo " └─ Missing: DoD (Definition of Done)"
BLOCKED=1
fi
else
echo -e "${RED}BLOCK${NC}"
echo " └─ No audit file found: ${AUDIT_DIR}/${TODAY}-*.md"
echo " └─ Run line-walk audit (8 steps) and save report before deploying"
BLOCKED=1
fi
# ── Gate 3: Test (必须存在 + 必须通过) ──
echo -n "Gate 3/7: Test ... "
TEST_SCRIPT="${DEPLOY_DIR}/tests/test_api.py"
if [ ! -f "${TEST_SCRIPT}" ]; then
echo -e "${RED}BLOCK${NC}"
echo " └─ Test script not found: ${TEST_SCRIPT}"
echo " └─ Tests are MANDATORY — cannot deploy without test coverage"
BLOCKED=1
else
TEST_OUTPUT=$(cd "${DEPLOY_DIR}" && python3 "${TEST_SCRIPT}" 2>&1) || true
if echo "${TEST_OUTPUT}" | grep -qiE "FAIL|ERROR|failed|error"; then
echo -e "${RED}BLOCK${NC}"
echo " └─ Tests failed. Output:"
echo "${TEST_OUTPUT}" | head -20 | sed 's/^/ │ /'
BLOCKED=1
else
echo -e "${GREEN}PASS${NC}"
echo " └─ All tests passed"
GATES_PASSED=$((GATES_PASSED + 1))
fi
fi
# ── Gate 4: Lint (ruff — F only: undefined names, unused imports) ──
echo -n "Gate 4/7: Lint ... "
if command -v ruff &>/dev/null; then
# 只检查 F (pyflakes) — 真正的BUG:未定义名称、未使用导入
# S/B 问题由 Security 门扫描但不阻断部署
LINT_OUTPUT=$(cd "${DEPLOY_DIR}" && ruff check server/ --select F 2>&1) || true
LINT_COUNT=$(echo "${LINT_OUTPUT}" | grep -cE "^server/" 2>/dev/null || true)
LINT_COUNT=$(echo "${LINT_COUNT}" | head -1 | tr -d '[:space:]')
LINT_COUNT=${LINT_COUNT:-0}
if [ "${LINT_COUNT}" -eq 0 ]; then
echo -e "${GREEN}PASS${NC}"
echo " └─ ruff check: 0 violations"
GATES_PASSED=$((GATES_PASSED + 1))
else
echo -e "${RED}BLOCK${NC}"
echo " └─ ruff found ${LINT_COUNT} violations:"
echo "${LINT_OUTPUT}" | head -15 | sed 's/^/ │ /'
BLOCKED=1
fi
else
echo -e "${YELLOW}SKIP${NC}"
echo " └─ ruff not installed (pip install ruff)"
GATES_PASSED=$((GATES_PASSED + 1))
fi
# ── Gate 5: Import (python -c "import server.main") ──
echo -n "Gate 5/7: Import ... "
# 优先使用 venv python,回退到系统 python
VENV_PYTHON="${DEPLOY_DIR}/venv/bin/python3"
if [ -x "${VENV_PYTHON}" ]; then
IMPORT_PYTHON="${VENV_PYTHON}"
else
IMPORT_PYTHON="python3"
fi
IMPORT_OUTPUT=$(cd "${DEPLOY_DIR}" && "${IMPORT_PYTHON}" -c "import server.main" 2>&1) && IMPORT_OK=1 || IMPORT_OK=0
if [ "${IMPORT_OK}" -eq 1 ]; then
echo -e "${GREEN}PASS${NC}"
echo " └─ server.main imports successfully"
GATES_PASSED=$((GATES_PASSED + 1))
else
# 检查是否有真正的导入错误(不是缺少运行时依赖如数据库)
if echo "${IMPORT_OUTPUT}" | grep -qiE "ImportError|ModuleNotFoundError|SyntaxError"; then
echo -e "${RED}BLOCK${NC}"
echo " └─ Import failed:"
echo "${IMPORT_OUTPUT}" | head -5 | sed 's/^/ │ /'
BLOCKED=1
else
# 运行时依赖缺失(如数据库连接)不算BLOCK,但警告
echo -e "${YELLOW}WARN${NC}"
echo " └─ Import has runtime dependency issues (not code errors)"
GATES_PASSED=$((GATES_PASSED + 1))
fi
fi
# ── Gate 6: Security (bandit — HIGH only) ──
echo -n "Gate 6/7: Security ... "
if command -v bandit &>/dev/null; then
# 只阻断 HIGH severityMEDIUM/LOW 由 ruff S 规则覆盖
SEC_OUTPUT=$(cd "${DEPLOY_DIR}" && bandit -r server/ -f txt -ll 2>&1) || true
HIGH_COUNT=$(echo "${SEC_OUTPUT}" | grep -cE "Severity: High" 2>/dev/null || true)
HIGH_COUNT=$(echo "${HIGH_COUNT}" | head -1 | tr -d '[:space:]')
HIGH_COUNT=${HIGH_COUNT:-0}
if [ "${HIGH_COUNT}" -eq 0 ]; then
echo -e "${GREEN}PASS${NC}"
echo " └─ bandit: 0 HIGH findings"
GATES_PASSED=$((GATES_PASSED + 1))
else
echo -e "${RED}BLOCK${NC}"
echo " └─ bandit found ${HIGH_COUNT} HIGH severity issues:"
echo "${SEC_OUTPUT}" | grep -A3 "Severity: High" | head -15 | sed 's/^/ │ /'
BLOCKED=1
fi
else
echo -e "${YELLOW}SKIP${NC}"
echo " └─ bandit not installed (pip install bandit)"
GATES_PASSED=$((GATES_PASSED + 1))
fi
# ── Gate 7: Review (审计文件交叉验证) ──
echo -n "Gate 7/7: Review ... "
if ls "${AUDIT_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
FILE=$(ls "${AUDIT_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
# 检查审计文件中是否列出了实际改动的文件
REVIEW_OK=1
# 如果是git仓库,交叉验证改动文件
if command -v git &>/dev/null && [ -d "${DEPLOY_DIR}/.git" ]; then
CHANGED_FILES=$(cd "${DEPLOY_DIR}" && git diff --name-only HEAD~1 2>/dev/null || true)
for CF in ${CHANGED_FILES}; do
# 跳过非代码文件
case "${CF}" in
docs/*|CLAUDE.md|*.md) continue ;;
esac
# 检查审计文件是否提到了这个文件
if ! grep -q "$(basename "${CF}")" "${FILE}" 2>/dev/null; then
if [ "${REVIEW_OK}" -eq 1 ]; then
echo -e "${RED}BLOCK${NC}"
REVIEW_OK=0
fi
echo " └─ Changed file not in audit: ${CF}"
fi
done
fi
if [ "${REVIEW_OK}" -eq 1 ]; then
echo -e "${GREEN}PASS${NC}"
echo " └─ Audit covers all changed files"
GATES_PASSED=$((GATES_PASSED + 1))
else
BLOCKED=1
fi
else
echo -e "${RED}BLOCK${NC}"
echo " └─ No audit file to validate against"
BLOCKED=1
fi
# ── Summary ──
echo ""
echo "========================================"
if [ "${BLOCKED}" -eq 0 ]; then
echo -e " Result: ${GREEN}${GATES_PASSED}/${GATES_TOTAL} gates passed${NC} — Deploy allowed"
echo "========================================"
exit 0
else
echo -e " Result: ${RED}${GATES_PASSED}/${GATES_TOTAL} gates passed${NC} — Deploy BLOCKED"
echo "========================================"
exit 1
fi