2026-06-01 19:57:06 +08:00
|
|
|
#!/bin/bash
|
2026-06-18 06:01:50 +08:00
|
|
|
# Nexus Pre-deploy Gate Check (v3 — 8 gates)
|
|
|
|
|
# 在部署前强制检查 8 道门控,任何一道不过则阻止部署
|
2026-06-01 19:57:06 +08:00
|
|
|
#
|
|
|
|
|
# 门控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 交叉验证
|
2026-06-18 06:01:50 +08:00
|
|
|
# 门控8: AI_REVIEW — cursor-agent 审查 HEAD~1..HEAD;缓存 docs/reviews/<head_sha>.json
|
2026-06-01 19:57:06 +08:00
|
|
|
#
|
|
|
|
|
# 用法: bash deploy/pre_deploy_check.sh
|
|
|
|
|
# 退出码: 0=全部通过, 1=至少一道门未过
|
|
|
|
|
# 日志: deploy/gate_log.jsonl(每次检查自动追加记录)
|
|
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
|
_REPO_ROOT="$(cd "${_SCRIPT_DIR}/.." && pwd)"
|
|
|
|
|
DEPLOY_DIR="${NEXUS_DEPLOY_DIR:-${_REPO_ROOT}}"
|
|
|
|
|
TODAY=$(date +%Y-%m-%d)
|
|
|
|
|
TIMESTAMP=$(date -Iseconds)
|
|
|
|
|
GATES_PASSED=0
|
2026-06-18 06:01:50 +08:00
|
|
|
GATES_TOTAL=8
|
2026-06-01 19:57:06 +08:00
|
|
|
BLOCKED=0
|
|
|
|
|
GATE_RESULTS=""
|
|
|
|
|
|
2026-06-04 23:02:21 +08:00
|
|
|
# Resolve tool from .venv / venv / PATH
|
|
|
|
|
_pick_bin() {
|
|
|
|
|
local name="$1"
|
|
|
|
|
for p in "${DEPLOY_DIR}/.venv/bin/${name}" "${DEPLOY_DIR}/venv/bin/${name}"; do
|
|
|
|
|
if [ -x "${p}" ]; then
|
|
|
|
|
echo "${p}"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
command -v "${name}" 2>/dev/null || return 1
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 19:57:06 +08:00
|
|
|
RED='\033[0;31m'
|
|
|
|
|
GREEN='\033[0;32m'
|
|
|
|
|
YELLOW='\033[0;33m'
|
|
|
|
|
NC='\033[0m' # No Color
|
|
|
|
|
|
|
|
|
|
# ── 日志函数 ──
|
|
|
|
|
gate_log() {
|
|
|
|
|
local gate="$1" result="$2" detail="${3:-}"
|
|
|
|
|
local entry="{\"ts\":\"${TIMESTAMP}\",\"date\":\"${TODAY}\",\"gate\":\"${gate}\",\"result\":\"${result}\",\"detail\":\"${detail}\"}"
|
|
|
|
|
GATE_RESULTS="${GATE_RESULTS}${entry},"
|
|
|
|
|
# 追加到 JSONL 日志文件
|
|
|
|
|
echo "${entry}" >> "${DEPLOY_DIR}/deploy/gate_log.jsonl"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
echo "========================================"
|
2026-06-18 06:01:50 +08:00
|
|
|
echo " Nexus Pre-deploy Gate Check (v3)"
|
2026-06-01 19:57:06 +08:00
|
|
|
echo " Date: ${TODAY}"
|
|
|
|
|
echo " Gates: ${GATES_TOTAL}"
|
|
|
|
|
echo "========================================"
|
|
|
|
|
echo ""
|
|
|
|
|
|
|
|
|
|
# ── Gate 1: Changelog (存在 + 行数≥10) ──
|
2026-06-18 06:01:50 +08:00
|
|
|
echo -n "Gate 1/8: Changelog ... "
|
2026-06-01 19:57:06 +08:00
|
|
|
CHANGELOG_DIR="${DEPLOY_DIR}/docs/changelog"
|
|
|
|
|
if ls "${CHANGELOG_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
|
|
|
|
FILE=$(ls -t "${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))
|
|
|
|
|
gate_log "changelog" "PASS" "$(basename "${FILE}") ${LINES}lines"
|
|
|
|
|
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
|
|
|
|
|
gate_log "changelog" "BLOCK" "only ${LINES} lines"
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
echo -e "${RED}BLOCK${NC}"
|
|
|
|
|
echo " └─ No changelog file found: ${CHANGELOG_DIR}/${TODAY}-*.md"
|
|
|
|
|
echo " └─ Create changelog before deploying"
|
|
|
|
|
BLOCKED=1
|
|
|
|
|
gate_log "changelog" "BLOCK" "file not found"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── Gate 2: Audit (存在 + 关键段落) ──
|
2026-06-18 06:01:50 +08:00
|
|
|
echo -n "Gate 2/8: Audit ... "
|
2026-06-01 19:57:06 +08:00
|
|
|
AUDIT_DIR="${DEPLOY_DIR}/docs/audit"
|
|
|
|
|
if ls "${AUDIT_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
|
|
|
|
FILE=$(ls -t "${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))
|
|
|
|
|
gate_log "audit" "PASS" "$(basename "${FILE}")"
|
|
|
|
|
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
|
|
|
|
|
gate_log "audit" "BLOCK" "missing sections"
|
|
|
|
|
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
|
|
|
|
|
gate_log "audit" "BLOCK" "file not found"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── Gate 3: Test (必须存在 + 必须通过) ──
|
2026-06-18 06:01:50 +08:00
|
|
|
echo -n "Gate 3/8: Test ... "
|
2026-06-01 19:57:06 +08:00
|
|
|
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
|
|
|
|
|
gate_log "test" "BLOCK" "script not found"
|
|
|
|
|
else
|
2026-06-08 04:30:24 +08:00
|
|
|
# Always hit local API for gate 3 (ignore shell NEXUS_TEST_BASE used by import/e2e scripts)
|
|
|
|
|
TEST_OUTPUT=$(cd "${DEPLOY_DIR}" && env -u NEXUS_TEST_BASE NEXUS_TEST_BASE=http://127.0.0.1:8600 python3 "${TEST_SCRIPT}" 2>&1) || true
|
2026-06-01 19:57:06 +08:00
|
|
|
FAIL_COUNT=$(echo "${TEST_OUTPUT}" | grep -cE "^\s+\[FAIL\]" 2>/dev/null || true)
|
|
|
|
|
FAIL_COUNT=${FAIL_COUNT:-0}
|
2026-07-01 12:09:02 +08:00
|
|
|
GATE3_PASSED=0
|
2026-06-01 19:57:06 +08:00
|
|
|
if [ "${FAIL_COUNT}" -gt 0 ] || echo "${TEST_OUTPUT}" | grep -qE "[0-9]+ test(s) failed"; then
|
2026-07-01 12:09:02 +08:00
|
|
|
# 本机未起 :8600 时,若失败均为 Connection refused,回退 pytest 专项
|
|
|
|
|
NON_CONN_FAILS=$(echo "${TEST_OUTPUT}" | grep -E "^\s+\[FAIL\]" | grep -vcE "Connection refused|Errno 111" 2>/dev/null || true)
|
|
|
|
|
NON_CONN_FAILS=${NON_CONN_FAILS:-0}
|
|
|
|
|
if [ "${FAIL_COUNT}" -gt 0 ] && [ "${NON_CONN_FAILS}" -eq 0 ]; then
|
|
|
|
|
PYTEST_BIN=$(_pick_bin pytest || true)
|
|
|
|
|
FALLBACK_TESTS="tests/test_btpanel_ssh_bootstrap.py tests/test_terminal_quick_commands.py tests/test_gate_ai_review.py"
|
|
|
|
|
if [ -n "${PYTEST_BIN}" ]; then
|
|
|
|
|
echo -e "${YELLOW}fallback${NC}"
|
|
|
|
|
echo " └─ Local :8600 unreachable — running pytest gate batch"
|
|
|
|
|
FALLBACK_OUTPUT=$(cd "${DEPLOY_DIR}" && "${PYTEST_BIN}" ${FALLBACK_TESTS} -q 2>&1) || FALLBACK_RC=$?
|
|
|
|
|
FALLBACK_RC=${FALLBACK_RC:-0}
|
|
|
|
|
if [ "${FALLBACK_RC}" -eq 0 ]; then
|
|
|
|
|
echo -e " └─ ${GREEN}PASS${NC} (pytest fallback)"
|
|
|
|
|
GATES_PASSED=$((GATES_PASSED + 1))
|
|
|
|
|
gate_log "test" "PASS" "pytest fallback :8600 down"
|
|
|
|
|
GATE3_PASSED=1
|
|
|
|
|
else
|
|
|
|
|
echo -e " └─ ${RED}BLOCK${NC} pytest fallback failed:"
|
|
|
|
|
echo "${FALLBACK_OUTPUT}" | tail -15 | sed 's/^/ │ /'
|
|
|
|
|
BLOCKED=1
|
|
|
|
|
gate_log "test" "BLOCK" "pytest fallback failed"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
if [ "${GATE3_PASSED}" -eq 0 ]; then
|
|
|
|
|
echo -e "${RED}BLOCK${NC}"
|
|
|
|
|
echo " └─ Tests failed. Output:"
|
|
|
|
|
echo "${TEST_OUTPUT}" | head -20 | sed 's/^/ │ /'
|
|
|
|
|
BLOCKED=1
|
|
|
|
|
gate_log "test" "BLOCK" "tests failed"
|
|
|
|
|
fi
|
2026-06-01 19:57:06 +08:00
|
|
|
else
|
|
|
|
|
echo -e "${GREEN}PASS${NC}"
|
|
|
|
|
echo " └─ All tests passed"
|
|
|
|
|
GATES_PASSED=$((GATES_PASSED + 1))
|
|
|
|
|
gate_log "test" "PASS" "all passed"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── Gate 4: Lint (ruff — F only: undefined names, unused imports) ──
|
2026-06-18 06:01:50 +08:00
|
|
|
echo -n "Gate 4/8: Lint ... "
|
2026-06-04 23:02:21 +08:00
|
|
|
RUFF_BIN=$(_pick_bin ruff || true)
|
|
|
|
|
if [ -n "${RUFF_BIN}" ]; then
|
|
|
|
|
LINT_OUTPUT=$(cd "${DEPLOY_DIR}" && "${RUFF_BIN}" check server/ --select F 2>&1) || true
|
2026-06-01 19:57:06 +08:00
|
|
|
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))
|
|
|
|
|
gate_log "lint" "PASS" "0 violations"
|
|
|
|
|
else
|
|
|
|
|
echo -e "${RED}BLOCK${NC}"
|
|
|
|
|
echo " └─ ruff found ${LINT_COUNT} violations:"
|
|
|
|
|
echo "${LINT_OUTPUT}" | head -15 | sed 's/^/ │ /'
|
|
|
|
|
BLOCKED=1
|
|
|
|
|
gate_log "lint" "BLOCK" "${LINT_COUNT} violations"
|
|
|
|
|
fi
|
|
|
|
|
else
|
2026-06-04 23:02:21 +08:00
|
|
|
echo -e "${RED}BLOCK${NC}"
|
|
|
|
|
echo " └─ ruff not found (.venv/bin/ruff or pip install ruff)"
|
|
|
|
|
BLOCKED=1
|
|
|
|
|
gate_log "lint" "BLOCK" "ruff not found"
|
2026-06-01 19:57:06 +08:00
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── Gate 5: Import (python -c "import server.main") ──
|
2026-06-18 06:01:50 +08:00
|
|
|
echo -n "Gate 5/8: Import ... "
|
2026-06-04 23:02:21 +08:00
|
|
|
IMPORT_PYTHON=""
|
|
|
|
|
for p in "${DEPLOY_DIR}/.venv/bin/python3" "${DEPLOY_DIR}/venv/bin/python3"; do
|
|
|
|
|
if [ -x "${p}" ]; then
|
|
|
|
|
IMPORT_PYTHON="${p}"
|
|
|
|
|
break
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
if [ -z "${IMPORT_PYTHON}" ]; then
|
2026-06-01 19:57:06 +08:00
|
|
|
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))
|
|
|
|
|
gate_log "import" "PASS" "ok"
|
|
|
|
|
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
|
|
|
|
|
gate_log "import" "BLOCK" "import/syntax error"
|
|
|
|
|
else
|
|
|
|
|
# 运行时依赖缺失(如数据库连接)不算BLOCK,但警告
|
|
|
|
|
echo -e "${YELLOW}WARN${NC}"
|
|
|
|
|
echo " └─ Import has runtime dependency issues (not code errors)"
|
|
|
|
|
GATES_PASSED=$((GATES_PASSED + 1))
|
|
|
|
|
gate_log "import" "WARN" "runtime deps only"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── Gate 6: Security (bandit — HIGH only) ──
|
2026-06-18 06:01:50 +08:00
|
|
|
echo -n "Gate 6/8: Security ... "
|
2026-06-04 23:02:21 +08:00
|
|
|
BANDIT_BIN=$(_pick_bin bandit || true)
|
|
|
|
|
if [ -n "${BANDIT_BIN}" ]; then
|
|
|
|
|
SEC_OUTPUT=$(cd "${DEPLOY_DIR}" && "${BANDIT_BIN}" -r server/ -f txt -ll 2>&1) || true
|
2026-06-01 19:57:06 +08:00
|
|
|
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))
|
|
|
|
|
gate_log "security" "PASS" "0 HIGH"
|
|
|
|
|
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
|
|
|
|
|
gate_log "security" "BLOCK" "${HIGH_COUNT} HIGH findings"
|
|
|
|
|
fi
|
|
|
|
|
else
|
2026-06-04 23:02:21 +08:00
|
|
|
echo -e "${RED}BLOCK${NC}"
|
|
|
|
|
echo " └─ bandit not found (.venv/bin/bandit or pip install bandit)"
|
|
|
|
|
BLOCKED=1
|
|
|
|
|
gate_log "security" "BLOCK" "bandit not found"
|
2026-06-01 19:57:06 +08:00
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── Gate 7: Review (审计文件交叉验证) ──
|
2026-06-18 06:01:50 +08:00
|
|
|
echo -n "Gate 7/8: Review ... "
|
2026-06-01 19:57:06 +08:00
|
|
|
if ls "${AUDIT_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
|
|
|
|
FILE=$(ls -t "${AUDIT_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
|
|
|
|
|
# 检查审计文件中是否列出了实际改动的文件
|
|
|
|
|
REVIEW_OK=1
|
|
|
|
|
# 如果是git仓库,交叉验证改动文件
|
|
|
|
|
if command -v git &>/dev/null && [ -d "${DEPLOY_DIR}/.git" ]; then
|
2026-06-08 07:09:46 +08:00
|
|
|
CHANGED_FILES=$(cd "${DEPLOY_DIR}" && git diff --name-only HEAD~1..HEAD 2>/dev/null || true)
|
2026-06-01 19:57:06 +08:00
|
|
|
for CF in ${CHANGED_FILES}; do
|
2026-06-08 07:09:46 +08:00
|
|
|
# 跳过非代码文件与门控运行日志
|
2026-06-01 19:57:06 +08:00
|
|
|
case "${CF}" in
|
2026-06-08 07:21:20 +08:00
|
|
|
docs/*|CLAUDE.md|*.md|deploy/gate_log.jsonl|web/app/assets/*) continue ;;
|
2026-06-01 19:57:06 +08:00
|
|
|
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))
|
|
|
|
|
gate_log "review" "PASS" "audit covers changes"
|
|
|
|
|
else
|
|
|
|
|
BLOCKED=1
|
|
|
|
|
gate_log "review" "BLOCK" "audit missing changed files"
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
echo -e "${RED}BLOCK${NC}"
|
|
|
|
|
echo " └─ No audit file to validate against"
|
|
|
|
|
BLOCKED=1
|
|
|
|
|
gate_log "review" "BLOCK" "no audit file"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-06-18 06:01:50 +08:00
|
|
|
# ── Gate 8: AI Review (cursor-agent + docs/reviews cache) ──
|
|
|
|
|
echo -n "Gate 8/8: AI Review ... "
|
|
|
|
|
AI_REVIEW_OUT=$(cd "${DEPLOY_DIR}" && python3 "${DEPLOY_DIR}/scripts/gate_ai_review.py" 2>&1) || AI_REVIEW_RC=$?
|
|
|
|
|
AI_REVIEW_RC=${AI_REVIEW_RC:-0}
|
|
|
|
|
if [ "${AI_REVIEW_RC}" -eq 0 ]; then
|
|
|
|
|
echo -e "${GREEN}PASS${NC}"
|
|
|
|
|
echo "${AI_REVIEW_OUT}" | sed 's/^/ └─ /' | head -5
|
|
|
|
|
GATES_PASSED=$((GATES_PASSED + 1))
|
|
|
|
|
if echo "${AI_REVIEW_OUT}" | grep -qi "skip"; then
|
|
|
|
|
gate_log "ai_review" "PASS" "skipped no code"
|
|
|
|
|
else
|
|
|
|
|
gate_log "ai_review" "PASS" "ok"
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
echo -e "${RED}BLOCK${NC}"
|
|
|
|
|
echo "${AI_REVIEW_OUT}" | sed 's/^/ │ /' | head -12
|
|
|
|
|
BLOCKED=1
|
|
|
|
|
gate_log "ai_review" "BLOCK" "see gate_ai_review.py output"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-06-03 00:42:55 +08:00
|
|
|
# ── Pre-flight: shell scripts LF ──
|
|
|
|
|
echo -n "Pre-flight: Shell EOL (LF) ... "
|
|
|
|
|
if bash "${DEPLOY_DIR}/deploy/check_shell_eol.sh" >/dev/null 2>&1; then
|
|
|
|
|
echo -e "${GREEN}PASS${NC}"
|
|
|
|
|
gate_log "shell_eol" "PASS" "all tracked sh LF"
|
|
|
|
|
else
|
|
|
|
|
echo -e "${RED}BLOCK${NC}"
|
|
|
|
|
echo " └─ Run: bash deploy/check_shell_eol.sh"
|
|
|
|
|
echo " └─ Fix: git add --renormalize '*.sh'"
|
|
|
|
|
BLOCKED=1
|
|
|
|
|
gate_log "shell_eol" "BLOCK" "CR in tracked sh"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── Pre-flight: deploy-critical paths LF in git index ──
|
|
|
|
|
echo -n "Pre-flight: Text EOL (git index) ... "
|
|
|
|
|
if python3 "${DEPLOY_DIR}/scripts/check_text_eol.py" >/dev/null 2>&1; then
|
|
|
|
|
echo -e "${GREEN}PASS${NC}"
|
|
|
|
|
gate_log "text_eol" "PASS" "git index LF"
|
|
|
|
|
else
|
|
|
|
|
echo -e "${RED}BLOCK${NC}"
|
|
|
|
|
echo " └─ Run: python3 scripts/check_text_eol.py --fix-hint"
|
|
|
|
|
BLOCKED=1
|
|
|
|
|
gate_log "text_eol" "BLOCK" "CR in git index"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-06-01 19:57:06 +08:00
|
|
|
# ── Summary ──
|
|
|
|
|
echo ""
|
|
|
|
|
echo "========================================"
|
|
|
|
|
if [ "${BLOCKED}" -eq 0 ]; then
|
|
|
|
|
echo -e " Result: ${GREEN}${GATES_PASSED}/${GATES_TOTAL} gates passed${NC} — Deploy allowed"
|
|
|
|
|
echo "========================================"
|
|
|
|
|
gate_log "summary" "PASS" "${GATES_PASSED}/${GATES_TOTAL}"
|
|
|
|
|
exit 0
|
|
|
|
|
else
|
|
|
|
|
echo -e " Result: ${RED}${GATES_PASSED}/${GATES_TOTAL} gates passed${NC} — Deploy BLOCKED"
|
|
|
|
|
echo "========================================"
|
|
|
|
|
gate_log "summary" "BLOCK" "${GATES_PASSED}/${GATES_TOTAL}"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|