chore(qa): 验收台账与证据入库 + 构建/部署配置同步

- docs/acceptance/qa-tracker.md:跨周期缺陷单一真源台账(§7=第五轮)。
- 周期 1/2 + iPhone SE/axe 验收证据目录、ACCEPTANCE_REVIEW 快照入库。
- 同步 README/CONTEXT/CLAUDE/DESIGN/testing/deployment/lessons-learned 口径;
  next.config/Dockerfile/nginx/Jenkinsfile/docker-compose/sentry/prisma 对齐
  standalone 产物装配与部署形态。
This commit is contained in:
2026-09-28 10:48:09 +08:00
parent 6bb7c557ee
commit a0328a623f
128 changed files with 22455 additions and 504 deletions
Vendored
+147 -17
View File
@@ -162,6 +162,8 @@ pipeline {
}
// ====== L3: E2E + 用户旅程测试 ======
// 历史上这些命令带 `|| echo`,失败被吞掉、绿灯不代表任何行为正确(验收 A-5)。
// 现统一 set -e:任一例失败即整个 stage 失败。
stage('🌐 E2E 测试') {
when {
branch 'main'
@@ -170,21 +172,16 @@ pipeline {
steps {
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
sh '''
echo "🔨 构建用于 E2E 测试..."
set -e
echo "🔨 构建生产产物(验收 A-10:E2E 打构建产物,dev server 下预渲染/ISR/生产响应头永不被测)..."
npm run build
echo "🚀 启动预览服务器..."
npx serve dist -l 3000 &
sleep 5
echo "🧪 运行 E2E 快速测试(@smoke + @critical)..."
cd e2e && npx playwright test --grep "@smoke|@critical" || echo "⚠️ E2E 测试部分失败,继续执行"
echo "🧪 运行用户旅程测试..."
npx playwright test --grep @journey || echo "⚠️ 用户旅程测试部分失败,继续执行"
echo "🚀 运行 E2E(@smoke + @critical + @journey × 三浏览器,next start 由 Playwright webServer 起)..."
npm run test:e2e:prod
'''
}
}
post {
always {
sh 'kill $(lsof -t -i:3000) 2>/dev/null || true'
publishHTML(target: [
allowMissing: true,
reportDir: 'e2e/playwright-report',
@@ -207,22 +204,80 @@ pipeline {
steps {
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
sh '''
echo "🚀 启动预览服务器..."
npx serve dist -l 3000 &
sleep 5
set -e
echo "🧪 运行视觉回归测试..."
cd e2e && npx playwright test visual-regression.spec.ts --project=visual-chromium-desktop || echo "⚠️ 视觉回归测试失败,请检查基线是否需要更新"
npm run test:visual
'''
}
}
post {
always {
sh 'kill $(lsof -t -i:3000) 2>/dev/null || true'
archiveArtifacts artifacts: 'e2e/test-results/**/*.png', allowEmptyArchive: true
}
}
}
// ====== L4.5: 可访问性与设计契约门禁 ======
stage('♿ 可访问性门禁') {
steps {
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
sh '''
set -e
echo "🎨 令牌对比度(读令牌表,含暗色与 alpha 组)..."
npm run check:contrast
echo "🔴 双通道红契约(禁 text-[var(--color-brand)])..."
npm run check:brand-token
echo "🧱 标题层级..."
npm run check:headings
'''
}
}
}
stage('⚡ Lighthouse 性能与无障碍') {
when {
branch 'main'
beforeAgent true
}
steps {
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
sh '''
set -e
echo "🔨 构建用于 Lighthouse..."
npm run build
echo "🚦 运行 lhci(断言含 axe critical 失败数 = 0)..."
npm run lighthouse
'''
}
}
post {
always {
archiveArtifacts artifacts: 'lighthouse-reports/**/*.report.html, lighthouse-reports/**/*.report.json', allowEmptyArchive: true
}
}
}
stage('🧬 变异测试') {
when {
branch 'main'
beforeAgent true
}
steps {
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
sh '''
set -e
npm run test:mutation:quick
'''
}
}
post {
always {
// 沙箱模式下 Stryker 不改写工作树;此清理仅兜底残留临时目录
sh 'rm -rf .stryker-tmp || true'
}
}
}
// ====== L5: 安全扫描 ======
stage('🔒 安全扫描') {
when {
@@ -232,10 +287,11 @@ pipeline {
steps {
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
sh '''
set -e
echo "🔒 运行依赖安全审计..."
npm audit --audit-level=high || echo "⚠️ 存在高危依赖漏洞,请检查"
echo "🔒 检查安全响应头..."
npm run test:security:headers -- --url https://novalon.cn || echo "⚠️ 安全头检查未通过,请检查 Nginx 配置"
npm audit --audit-level=high
echo "🔒 检查本分支构建产物的安全响应头..."
npm run test:security:headers
'''
}
}
@@ -274,6 +330,80 @@ pipeline {
}
}
// ====== 验收 §8-⑤:全站 axe 节点计数(三规则级覆盖 + 分母闭合的唯一真实来源) ======
// 复用上一个阶段刚产出的 standalone 产物,不重复构建。跑在 main 分支(与 E2E / Lighthouse 同档:
// 34 路由 × 双引擎 × 双主题 = 136 页扫描,约 14 分钟);令牌级 a11y 门禁仍在每个分支跑。
// 服务用 `node dist/standalone/server.js`,不用 `npm run start`——Next 对 output:'standalone' 下
// 的 next start 会直接告警不支持(佐证见 docs/acceptance/2026-09-21-gates/ga4-production-run.txt)。
stage('♿♿ 全站 axe 节点计数') {
when {
branch 'main'
beforeAgent true
}
steps {
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
sh '''
set -e
AXE_PORT="${AXE_PORT:-3100}" # 专用端口:不复用 :3000(chain2 事故就是端口被占 + 静默复用)
ROUTES=/tmp/axe-routes.xml
echo "📦 把浏览器静态资源并入 standalone 根目录(与 Dockerfile.prod / scripts/deploy.sh 同一套装配)..."
echo " standalone 产物不含 dist/static 与 public,缺了它们 /_next/static/** 全 404 ⇒"
echo " 页面落在默认黑白底上,对比度会「意外达标」;harness 的 bgMismatch 判据会抓到,但别拿它当门禁目的。"
mkdir -p dist/standalone/dist/static dist/standalone/public
cp -R dist/static/. dist/standalone/dist/static/
cp -R public/. dist/standalone/public/
echo "🚀 启动 standalone 服务 :$AXE_PORT ..."
PORT="$AXE_PORT" HOSTNAME=127.0.0.1 node dist/standalone/server.js > /tmp/axe-server.log 2>&1 &
AXE_PID=$!
# 只收服本轮自己起的进程(记录 PID)。历史事故:只 kill 包装进程会留下 next-server
# 孤儿继续占端口,下一轮 E2E / Lighthouse 通过 reuseExistingServer 静默复用它 —— 整轮验证被毒化。
cleanup() {
if kill -0 "$AXE_PID" 2>/dev/null; then
kill "$AXE_PID" 2>/dev/null || true
i=0
while kill -0 "$AXE_PID" 2>/dev/null && [ "$i" -lt 20 ]; do i=$((i+1)); sleep 1; done
kill -0 "$AXE_PID" 2>/dev/null && kill -9 "$AXE_PID" 2>/dev/null || true
fi
}
# rc=$? 先行捕获:EXIT/INT/TERM 处理器必须原样带回门禁的退出码,
# 否则「trap 里最后一条命令的状态」会把判红变成判绿(POSIX trap 语义的坑)。
on_exit() { rc=$?; cleanup; exit "$rc"; }
trap on_exit EXIT INT TERM
CODE=000
i=0
while [ "$CODE" != "200" ] && [ "$i" -lt 40 ]; do
i=$((i+1))
CODE=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:$AXE_PORT/" || true)
sleep 2
done
if [ "$CODE" != "200" ]; then
echo "❌ standalone 服务未就绪(HTTP $CODE),日志:"
tail -40 /tmp/axe-server.log
exit 1
fi
export BASE="http://127.0.0.1:$AXE_PORT"
echo "🗺️ 生成全站路由清单(sitemap ∪ 预渲染产物 ∪ 站内链接 BFS)..."
OUT="$ROUTES" npm run check:axe:routes
echo "🔎 双引擎(chromium/firefox)× 双主题(light/dark)逐页 axe 节点计数 + 规则级通道..."
SITEMAP="$ROUTES" npm run check:axe
'''
}
}
post {
always {
// 失败时这份证据就是判红依据(harness 失败也会落盘,含逐条 failures),必须留档
archiveArtifacts artifacts: 'docs/acceptance/2026-09-21-axe/axe-evidence.json', allowEmptyArchive: true
}
failure {
sh 'tail -40 /tmp/axe-server.log || true'
}
}
}
stage('🚀 部署到生产环境') {
when {
allOf {