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:
Vendored
+147
-17
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user