|
15560
|
10842
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
📊 生成综合报告
|
0
|
name: Test Report
"on":
workflow_run:
name: Test Report
"on":
workflow_run:
workflows:
- 'Test Suite'
- 'E2E Tests'
- 'Performance Tests'
types:
- completed
env:
NODE_VERSION: "18"
jobs:
generate-report:
name: "\U0001F4CA 生成综合报告"
runs-on: ubuntu-latest
steps:
- name: "\U0001F4E5 检出代码"
uses: actions/checkout@v4
- name: "\U0001F7E2 设置 Node.js"
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: "\U0001F4E5 下载触发工作流的 artifacts"
uses: dawidd6/action-download-artifact@v3
with:
path: downloaded-artifacts
run_id: ${{ github.event.workflow_run.id }}
workflow: ${{ github.event.workflow_run.workflow_id }}
continue-on-error: true
- name: "\U0001F4CA 生成综合测试报告"
run: "echo \"# \U0001F4CA 测试报告\" > test-report.md\necho \"\" >> test-report.md\necho \"**生成时间**: $(date)\" >> test-report.md\necho \"**触发工作流**: ${{ github.event.workflow_run.name }}\" >> test-report.md\necho \"**运行 ID**: ${{ github.event.workflow_run.id }}\" >> test-report.md\necho \"**分支**: ${{ github.event.workflow_run.head_branch }}\" >> test-report.md\necho \"**结果**: ${{ github.event.workflow_run.conclusion }}\" >> test-report.md\necho \"\" >> test-report.md\n\necho \"## \U0001F4C1 测试结果\" >> test-report.md\necho \"\" >> test-report.md\n\n# 遍历下载的 artifacts\nif [ -d \"downloaded-artifacts\" ]; then\n echo \"### 已下载的测试结果\" >> test-report.md\n echo \"\" >> test-report.md\n\n for dir in downloaded-artifacts/*; do\n if [ -d \"$dir\" ]; then\n name=$(basename \"$dir\")\n echo \"- **$name**\" >> test-report.md\n\n # 检查是否有 JSON 结果文件\n for json in \"$dir\"/*.json; do\n if [ -f \"$json\" ]; then\n echo \" - $(basename \"$json\")\" >> test-report.md\n fi\n done\n fi\n done\nelse\n echo \"暂无测试结果下载\" >> test-report.md\nfi\n\necho \"\" >> test-report.md\necho \"---\" >> test-report.md\necho \"*此报告由 CI/CD 自动生成*\" >> test-report.md\n"
- name: "\U0001F4DD 生成 GitHub Summary"
run: "echo \"## \U0001F4CA 测试报告汇总\" >> $GITHUB_STEP_SUMMARY\necho \"\" >> $GITHUB_STEP_SUMMARY\necho \"| 项目 | 值 |\" >> $GITHUB_STEP_SUMMARY\necho \"|------|------|\" >> $GITHUB_STEP_SUMMARY\necho \"| 触发工作流 | ${{ github.event.workflow_run.name }} |\" >> $GITHUB_STEP_SUMMARY\necho \"| 运行 ID | ${{ github.event.workflow_run.id }} |\" >> $GITHUB_STEP_SUMMARY\necho \"| 分支 | ${{ github.event.workflow_run.head_branch }} |\" >> $GITHUB_STEP_SUMMARY\necho \"| 结果 | ${{ github.event.workflow_run.conclusion }} |\" >> $GITHUB_STEP_SUMMARY\necho \"\" >> $GITHUB_STEP_SUMMARY\n\nif [ \"${{ github.event.workflow_run.conclusion }}\" == \"success\" ]; then\n echo \"### ✅ 测试通过\" >> $GITHUB_STEP_SUMMARY\nelse\n echo \"### ❌ 测试失败\" >> $GITHUB_STEP_SUMMARY\n echo \"\" >> $GITHUB_STEP_SUMMARY\n echo \"请查看 [运行详情](https://github.com/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}) 了解更多信息。\" >> $GITHUB_STEP_SUMMARY\nfi\n"
- name: "\U0001F4E4 上传综合报告"
uses: actions/upload-artifact@v4
with:
name: test-report-${{ github.event.workflow_run.id }}
path: test-report.md
retention-days: "30"
...
|
generate-report
|
["collect-results"]
|
["ubuntu-latest"]
|
0
|
4
|
0
|
0
|
1775185999
|
1775186046
|
|
1
|
|
0
|
Edit
Delete
|
|
15561
|
10842
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
📢 发送通知
|
1
|
name: Test Report
"on":
workflow_run:
name: Test Report
"on":
workflow_run:
workflows:
- 'Test Suite'
- 'E2E Tests'
- 'Performance Tests'
types:
- completed
env:
NODE_VERSION: "18"
jobs:
notify:
name: "\U0001F4E2 发送通知"
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'failure'
steps:
- name: "\U0001F4E2 创建失败 Issue(可选)"
uses: actions/github-script@v7
with:
script: "// 检查是否已经存在相关 issue\nconst issues = await github.rest.issues.listForRepo({\n owner: context.repo.owner,\n repo: context.repo.repo,\n labels: 'test-failure',\n state: 'open',\n});\n\nconst workflowName = '${{ github.event.workflow_run.name }}';\nconst runId = '${{ github.event.workflow_run.id }}';\nconst branch = '${{ github.event.workflow_run.head_branch }}';\n\n// 如果是主分支失败,创建 issue\nif (branch === 'main' || branch === 'develop') {\n const existingIssue = issues.data.find(i =>\n i.title.includes(workflowName) && i.title.includes(branch)\n );\n\n if (!existingIssue) {\n await github.rest.issues.create({\n owner: context.repo.owner,\n repo: context.repo.repo,\n title: `\U0001F6A8 ${workflowName} 测试失败 (${branch})`,\n body: `## 测试失败通知\n\n**工作流**: ${workflowName}\n**分支**: ${branch}\n**运行 ID**: ${runId}\n\n### 详情\n\n请查看 [运行详情](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}) 了解更多信息。\n\n### 后续步骤\n\n1. 查看失败的测试用例\n2. 修复问题\n3. 重新运行测试\n4. 关闭此 issue\n\n---\n*此 issue 由 CI/CD 自动创建*`,\n labels: ['test-failure', 'automated'],\n });\n console.log('已创建测试失败 issue');\n } else {\n // 添加评论\n await github.rest.issues.createComment({\n owner: context.repo.owner,\n repo: context.repo.repo,\n issue_number: existingIssue.number,\n body: `测试再次失败。运行 ID: ${runId}\\n\\n[查看详情](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId})`,\n });\n console.log('已在现有 issue 中添加评论');\n }\n}\n"
...
|
notify
|
["collect-results","generate-report ["collect-results","generate-report"]...
|
["ubuntu-latest"]
|
12858
|
4
|
1775186238
|
1775186239
|
1775185999
|
1775186239
|
|
1
|
|
0
|
Edit
Delete
|
|
15562
|
10842
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
📈 趋势分析
|
1
|
name: Test Report
"on":
workflow_run:
name: Test Report
"on":
workflow_run:
workflows:
- 'Test Suite'
- 'E2E Tests'
- 'Performance Tests'
types:
- completed
env:
NODE_VERSION: "18"
jobs:
trend-analysis:
name: "\U0001F4C8 趋势分析"
runs-on: ubuntu-latest
if: github.event.workflow_run.name == 'Test Suite'
steps:
- name: "\U0001F4E5 检出代码"
uses: actions/checkout@v4
- name: "\U0001F4CA 分析测试趋势"
uses: actions/github-script@v7
with:
script: "// 获取最近 20 次运行记录\nconst runs = await github.rest.actions.listWorkflowRuns({\n owner: context.repo.owner,\n repo: context.repo.repo,\n workflow_id: 'test.yml',\n per_page: 20,\n});\n\nlet successCount = 0;\nlet failureCount = 0;\nconst durations = [];\n\nfor (const run of runs.data.workflow_runs) {\n if (run.conclusion === 'success') {\n successCount++;\n } else if (run.conclusion === 'failure') {\n failureCount++;\n }\n\n if (run.updated_at && run.created_at) {\n const duration = new Date(run.updated_at) - new Date(run.created_at);\n durations.push(duration / 1000 / 60); // 转换为分钟\n }\n}\n\nconst total = successCount + failureCount;\nconst successRate = total > 0 ? (successCount / total * 100).toFixed(1) : 0;\nconst avgDuration = durations.length > 0\n ? (durations.reduce((a, b) => a + b, 0) / durations.length).toFixed(1)\n : 0;\n\nconsole.log(`最近 ${total} 次运行:`);\nconsole.log(`- 成功: ${successCount}`);\nconsole.log(`- 失败: ${failureCount}`);\nconsole.log(`- 成功率: ${successRate}%`);\nconsole.log(`- 平均耗时: ${avgDuration} 分钟`);\n\n// 写入 summary\nconst fs = require('fs');\nconst summary = `## \U0001F4C8 测试趋势分析\n\n| 指标 | 值 |\n|------|------|\n| 分析样本 | 最近 ${total} 次运行 |\n| 成功次数 | ${successCount} |\n| 失败次数 | ${failureCount} |\n| 成功率 | ${successRate}% |\n| 平均耗时 | ${avgDuration} 分钟 |\n\n${successRate >= 90 ? '✅ 测试稳定性良好' : successRate >= 70 ? '⚠️ 测试稳定性一般' : '❌ 测试稳定性较差,需要关注'}\n`;\n\nfs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);\n"
...
|
trend-analysis
|
["collect-results"]
|
["ubuntu-latest"]
|
12859
|
4
|
1775186240
|
1775186240
|
1775185999
|
1775186241
|
|
1
|
|
0
|
Edit
Delete
|
|
15575
|
10845
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
紧急验证(跳过测试时)
|
1
|
name: CI/CD Deploy
"on":
# test-pipeli name: CI/CD Deploy
"on":
# test-pipeline 通过后自动触发(仅 main 分支)
workflow_run:
workflows: ["Test Pipeline"]
types: [completed]
branches: [main]
# 版本标签触发完整部署
push:
tags: ['v*']
# 手动触发
workflow_dispatch:
inputs:
environment:
description: '部署环境'
required: true
default: 'staging'
type: choice
options:
- staging
- production
- aliyun
- rollback-production
- rollback-aliyun
skip_tests:
description: '跳过测试(紧急修复)'
required: false
default: false
type: boolean
version:
description: '部署版本号(留空使用自动版本)'
required: false
type: string
env:
IMAGE_PREFIX: ${{ github.repository_owner }}/juhi
NODE_VERSION: "20"
PNPM_VERSION: "8"
REGISTRY: ghcr.io
jobs:
quick-check:
name: 紧急验证(跳过测试时)
runs-on: ubuntu-latest
if: >-
needs.gate.outputs.should_deploy == 'true' && needs.gate.outputs.is_rollback == 'false' && github.event.inputs.skip_tests == 'true'
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
cache: pnpm
node-version: ${{ env.NODE_VERSION }}
- name: 安装依赖
run: pnpm install --frozen-lockfile
- name: 构建共享包
run: pnpm --filter @juhi/shared run build
- name: 后端类型检查
run: pnpm --filter juhi-api run type-check
- name: 前端类型检查
run: pnpm --filter juhi-frontend run type-check
timeout-minutes: "10"
...
|
quick-check
|
["gate"]
|
["ubuntu-latest"]
|
12868
|
4
|
1775186349
|
1775186349
|
1775186343
|
1775186350
|
|
1
|
|
0
|
Edit
Delete
|
|
15576
|
10845
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
构建并推送镜像
|
1
|
name: CI/CD Deploy
"on":
# test-pipeli name: CI/CD Deploy
"on":
# test-pipeline 通过后自动触发(仅 main 分支)
workflow_run:
workflows: ["Test Pipeline"]
types: [completed]
branches: [main]
# 版本标签触发完整部署
push:
tags: ['v*']
# 手动触发
workflow_dispatch:
inputs:
environment:
description: '部署环境'
required: true
default: 'staging'
type: choice
options:
- staging
- production
- aliyun
- rollback-production
- rollback-aliyun
skip_tests:
description: '跳过测试(紧急修复)'
required: false
default: false
type: boolean
version:
description: '部署版本号(留空使用自动版本)'
required: false
type: string
env:
IMAGE_PREFIX: ${{ github.repository_owner }}/juhi
NODE_VERSION: "20"
PNPM_VERSION: "8"
REGISTRY: ghcr.io
jobs:
build-and-push:
name: 构建并推送镜像
runs-on: ubuntu-latest
if: >-
always() && needs.gate.outputs.should_deploy == 'true' && needs.gate.outputs.is_rollback == 'false' && (needs.quick-check.result == 'success' || needs.quick-check.result == 'skipped')
steps:
- uses: actions/checkout@v4
- id: version
name: 获取版本号
run: |
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/v}
elif [ -n "${{ github.event.inputs.version }}" ]; then
VERSION=${{ github.event.inputs.version }}
else
VERSION=$(date +%Y%m%d)-${{ github.run_number }}
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "short_sha=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: 设置 Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 登录容器仓库
uses: docker/login-action@v3
with:
password: ${{ secrets.GITHUB_TOKEN }}
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
- id: meta-api
name: 后端镜像元数据
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-api
tags: |
type=raw,value=${{ steps.version.outputs.version }}
type=raw,value=latest
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: 构建并推送后端镜像
uses: docker/build-push-action@v5
with:
build-args: NODE_ENV=production
cache-from: type=gha
cache-to: type=gha,mode=max
context: .
file: backend/Dockerfile
labels: ${{ steps.meta-api.outputs.labels }}
push: "true"
tags: ${{ steps.meta-api.outputs.tags }}
- id: meta-frontend
name: 前端镜像元数据
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-frontend
tags: |
type=raw,value=${{ steps.version.outputs.version }}
type=raw,value=latest
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: 构建并推送前端镜像
uses: docker/build-push-action@v5
with:
build-args: VITE_API_BASE_URL=/api/v1
cache-from: type=gha
cache-to: type=gha,mode=max
context: .
file: frontend/Dockerfile
labels: ${{ steps.meta-frontend.outputs.labels }}
push: "true"
tags: ${{ steps.meta-frontend.outputs.tags }}
- name: 输出构建信息
run: |
echo "## 构建信息" >> $GITHUB_STEP_SUMMARY
echo "- 版本: \`${{ steps.version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
echo "- 提交: \`${{ steps.version.outputs.short_sha }}\`" >> $GITHUB_STEP_SUMMARY
timeout-minutes: "20"
outputs:
api-image: ${{ steps.meta-api.outputs.tags }}
frontend-image: ${{ steps.meta-frontend.outputs.tags }}
version: ${{ steps.version.outputs.version }}
permissions:
contents: read
packages: write
...
|
build-and-push
|
["gate","quick-check"]
|
["ubuntu-latest"]
|
12870
|
4
|
1775186353
|
1775186354
|
1775186343
|
1775186354
|
|
1
|
|
0
|
Edit
Delete
|
|
15577
|
10845
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
部署到 Staging
|
1
|
name: CI/CD Deploy
"on":
# test-pipeli name: CI/CD Deploy
"on":
# test-pipeline 通过后自动触发(仅 main 分支)
workflow_run:
workflows: ["Test Pipeline"]
types: [completed]
branches: [main]
# 版本标签触发完整部署
push:
tags: ['v*']
# 手动触发
workflow_dispatch:
inputs:
environment:
description: '部署环境'
required: true
default: 'staging'
type: choice
options:
- staging
- production
- aliyun
- rollback-production
- rollback-aliyun
skip_tests:
description: '跳过测试(紧急修复)'
required: false
default: false
type: boolean
version:
description: '部署版本号(留空使用自动版本)'
required: false
type: string
env:
IMAGE_PREFIX: ${{ github.repository_owner }}/juhi
NODE_VERSION: "20"
PNPM_VERSION: "8"
REGISTRY: ghcr.io
jobs:
deploy-staging:
name: 部署到 Staging
runs-on: ubuntu-latest
if: >-
needs.gate.outputs.target_env == 'staging' || needs.gate.outputs.target_env == 'production'
steps:
- uses: actions/checkout@v4
- name: 配置 SSH
uses: webfactory/ssh-agent@v0.8.0
with:
ssh-private-key: ${{ secrets.STAGING_SSH_KEY }}
- name: 配置 SSH Known Hosts
run: |
mkdir -p ~/.ssh
echo "${{ secrets.STAGING_SSH_KNOWN_HOSTS }}" >> ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
- name: 同步部署文件
run: |
rsync -avz --delete \
docker-compose.prod.yml \
scripts/ \
deploy/ \
$USER@$HOST:$DEPLOY_PATH/
env:
HOST: ${{ secrets.STAGING_HOST }}
USER: ${{ secrets.STAGING_USER }}
DEPLOY_PATH: /opt/juhi
- name: 部署到 Staging
run: |
ssh $USER@$HOST << EOF
cd $DEPLOY_PATH
export API_IMAGE="${API_IMAGE}"
export FRONTEND_IMAGE="${FRONTEND_IMAGE}"
# 拉取最新镜像
docker compose -f docker-compose.prod.yml pull api frontend
# 数据库迁移
echo "==> 数据库迁移..."
docker compose -f docker-compose.prod.yml --profile migrate run --rm migrate
if [ \$? -ne 0 ]; then
echo "数据库迁移失败"
exit 1
fi
# 滚动更新 API
docker compose -f docker-compose.prod.yml up -d --no-deps api
# 健康检查
RETRY=0
MAX_RETRY=12
until curl -sf http://localhost:3000/health > /dev/null 2>&1; do
RETRY=\$((RETRY + 1))
if [ \$RETRY -ge \$MAX_RETRY ]; then
echo "API 健康检查超时"
docker compose -f docker-compose.prod.yml logs --tail=50 api
exit 1
fi
echo " 等待 API 就绪... (\${RETRY}/\${MAX_RETRY})"
sleep 5
done
# 更新前端 + 重载 Nginx
docker compose -f docker-compose.prod.yml up -d --no-deps frontend
docker compose -f docker-compose.prod.yml exec -T nginx nginx -s reload 2>/dev/null || true
docker image prune -f
echo "${VERSION}" > .deployed_version
echo "==> Staging 部署完成: ${VERSION}"
EOF
env:
HOST: ${{ secrets.STAGING_HOST }}
USER: ${{ secrets.STAGING_USER }}
DEPLOY_PATH: /opt/juhi
API_IMAGE: ${{ needs.build-and-push.outputs.api-image }}
FRONTEND_IMAGE: ${{ needs.build-and-push.outputs.frontend-image }}
VERSION: ${{ needs.build-and-push.outputs.version }}
- name: Staging 部署验证
run: |
ssh $USER@$HOST << 'EOF'
cd /opt/juhi
if [ -f "./scripts/post-deploy-verify.sh" ]; then
chmod +x ./scripts/post-deploy-verify.sh
./scripts/post-deploy-verify.sh --quick
else
curl -sf http://localhost:3000/health || exit 1
echo "Staging 健康检查通过"
fi
EOF
env:
HOST: ${{ secrets.STAGING_HOST }}
USER: ${{ secrets.STAGING_USER }}
timeout-minutes: "15"
...
|
deploy-staging
|
["gate","build-and-push"]
|
["ubuntu-latest"]
|
12871
|
4
|
1775186355
|
1775186355
|
1775186343
|
1775186356
|
|
1
|
|
0
|
Edit
Delete
|
|
15578
|
10845
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
部署到 Production
|
1
|
name: CI/CD Deploy
"on":
# test-pipeli name: CI/CD Deploy
"on":
# test-pipeline 通过后自动触发(仅 main 分支)
workflow_run:
workflows: ["Test Pipeline"]
types: [completed]
branches: [main]
# 版本标签触发完整部署
push:
tags: ['v*']
# 手动触发
workflow_dispatch:
inputs:
environment:
description: '部署环境'
required: true
default: 'staging'
type: choice
options:
- staging
- production
- aliyun
- rollback-production
- rollback-aliyun
skip_tests:
description: '跳过测试(紧急修复)'
required: false
default: false
type: boolean
version:
description: '部署版本号(留空使用自动版本)'
required: false
type: string
env:
IMAGE_PREFIX: ${{ github.repository_owner }}/juhi
NODE_VERSION: "20"
PNPM_VERSION: "8"
REGISTRY: ghcr.io
jobs:
deploy-production:
name: 部署到 Production
runs-on: ubuntu-latest
if: >-
startsWith(github.ref, 'refs/tags/v') || needs.gate.outputs.target_env == 'production'
steps:
- uses: actions/checkout@v4
- name: 配置 SSH
uses: webfactory/ssh-agent@v0.8.0
with:
ssh-private-key: ${{ secrets.PRODUCTION_SSH_KEY }}
- name: 配置 SSH Known Hosts
run: |
mkdir -p ~/.ssh
echo "${{ secrets.PRODUCTION_SSH_KNOWN_HOSTS }}" >> ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
- name: 同步部署文件
run: |
rsync -avz --delete \
docker-compose.prod.yml \
scripts/ \
deploy/ \
$USER@$HOST:$DEPLOY_PATH/
env:
HOST: ${{ secrets.PRODUCTION_HOST }}
USER: ${{ secrets.PRODUCTION_USER }}
DEPLOY_PATH: /opt/juhi
- name: 数据库备份
run: |
ssh $USER@$HOST << 'EOF'
cd /opt/juhi
BACKUP_DIR="/opt/juhi/backups"
mkdir -p "$BACKUP_DIR"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/pre_deploy_${TIMESTAMP}.sql"
docker compose -f docker-compose.prod.yml exec -T postgres \
pg_dump -U "${DB_USER:-juhi}" -d "${DB_NAME:-juhi_revops}" -Fc > "$BACKUP_FILE"
if [ $? -eq 0 ]; then
echo "备份完成: $(du -h "$BACKUP_FILE" | cut -f1)"
else
echo "备份失败"
exit 1
fi
find "$BACKUP_DIR" -name "pre_deploy_*.sql" -mtime +30 -delete 2>/dev/null || true
EOF
env:
HOST: ${{ secrets.PRODUCTION_HOST }}
USER: ${{ secrets.PRODUCTION_USER }}
- name: 部署到 Production
run: |
ssh $USER@$HOST << EOF
cd $DEPLOY_PATH
export API_IMAGE="${API_IMAGE}"
export FRONTEND_IMAGE="${FRONTEND_IMAGE}"
docker compose -f docker-compose.prod.yml pull api frontend
# 数据库迁移
echo "==> 数据库迁移..."
docker compose -f docker-compose.prod.yml --profile migrate run --rm migrate
if [ \$? -ne 0 ]; then
echo "数据库迁移失败"
exit 1
fi
# 记录部署历史
CURRENT_API=\$(docker inspect --format='{{.Config.Image}}' juhi-api 2>/dev/null || echo "none")
CURRENT_FE=\$(docker inspect --format='{{.Config.Image}}' juhi-frontend 2>/dev/null || echo "none")
echo "\$(date -Iseconds)|\${CURRENT_API}|\${CURRENT_FE}" >> .deploy-history
tail -20 .deploy-history > .deploy-history.tmp && mv .deploy-history.tmp .deploy-history
# 蓝绿部署
docker compose -f docker-compose.prod.yml up -d --no-deps --scale api=2 api
RETRY=0
MAX_RETRY=15
until curl -sf http://localhost:3000/health > /dev/null 2>&1; do
RETRY=\$((RETRY + 1))
if [ \$RETRY -ge \$MAX_RETRY ]; then
echo "健康检查超时,回滚到单实例"
docker compose -f docker-compose.prod.yml up -d --no-deps --scale api=1 api
exit 1
fi
sleep 5
done
docker compose -f docker-compose.prod.yml up -d --no-deps --scale api=1 api
docker compose -f docker-compose.prod.yml up -d --no-deps frontend
docker compose -f docker-compose.prod.yml exec -T nginx nginx -s reload 2>/dev/null || true
docker image prune -f
echo "${VERSION}" > .deployed_version
echo "==> Production 部署完成: ${VERSION}"
EOF
env:
HOST: ${{ secrets.PRODUCTION_HOST }}
USER: ${{ secrets.PRODUCTION_USER }}
DEPLOY_PATH: /opt/juhi
API_IMAGE: ${{ needs.build-and-push.outputs.api-image }}
FRONTEND_IMAGE: ${{ needs.build-and-push.outputs.frontend-image }}
VERSION: ${{ needs.build-and-push.outputs.version }}
- name: Production 部署验证
run: |
ssh $USER@$HOST << 'EOF'
cd /opt/juhi
if [ -f "./scripts/post-deploy-verify.sh" ]; then
chmod +x ./scripts/post-deploy-verify.sh
./scripts/post-deploy-verify.sh --quick || exit 1
else
curl -sf http://localhost:3000/health || exit 1
UNHEALTHY=$(docker compose -f docker-compose.prod.yml ps --format json | grep -c '"unhealthy"' || true)
if [ "$UNHEALTHY" -gt 0 ]; then
docker compose -f docker-compose.prod.yml ps
exit 1
fi
echo "Production 验证通过"
fi
EOF
env:
HOST: ${{ secrets.PRODUCTION_HOST }}
USER: ${{ secrets.PRODUCTION_USER }}
- if: startsWith(github.ref, 'refs/tags/v')
name: 创建 GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: "true"
make_latest: "true"
timeout-minutes: "20"
...
|
deploy-production
|
["gate","build-and-push","depl ["gate","build-and-push","deploy-staging"]...
|
["ubuntu-latest"]
|
12873
|
4
|
1775186359
|
1775186360
|
1775186343
|
1775186360
|
|
1
|
|
0
|
Edit
Delete
|
|
15579
|
10845
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
部署到阿里云
|
1
|
name: CI/CD Deploy
"on":
# test-pipeli name: CI/CD Deploy
"on":
# test-pipeline 通过后自动触发(仅 main 分支)
workflow_run:
workflows: ["Test Pipeline"]
types: [completed]
branches: [main]
# 版本标签触发完整部署
push:
tags: ['v*']
# 手动触发
workflow_dispatch:
inputs:
environment:
description: '部署环境'
required: true
default: 'staging'
type: choice
options:
- staging
- production
- aliyun
- rollback-production
- rollback-aliyun
skip_tests:
description: '跳过测试(紧急修复)'
required: false
default: false
type: boolean
version:
description: '部署版本号(留空使用自动版本)'
required: false
type: string
env:
IMAGE_PREFIX: ${{ github.repository_owner }}/juhi
NODE_VERSION: "20"
PNPM_VERSION: "8"
REGISTRY: ghcr.io
jobs:
deploy-aliyun:
name: 部署到阿里云
runs-on: ubuntu-latest
if: needs.gate.outputs.target_env == 'aliyun'
steps:
- uses: actions/checkout@v4
- name: 配置 SSH
uses: webfactory/ssh-agent@v0.8.0
with:
ssh-private-key: ${{ secrets.ALIYUN_SSH_PRIVATE_KEY }}
- name: 配置 SSH Known Hosts
run: |
mkdir -p ~/.ssh
echo "${{ secrets.ALIYUN_SSH_KNOWN_HOSTS }}" >> ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
- name: 同步部署文件
run: |
rsync -avz --delete \
--exclude 'node_modules' --exclude '.git' \
--exclude 'backups' --exclude 'data' --exclude 'logs' \
docker-compose.prod.yml \
scripts/ \
deploy/ \
.env.production.example \
$USER@$HOST:$DEPLOY_PATH/
env:
HOST: ${{ secrets.ALIYUN_HOST }}
USER: ${{ secrets.ALIYUN_USER }}
DEPLOY_PATH: /opt/juhi
- name: 部署前检查
run: |
ssh $USER@$HOST << 'EOF'
cd /opt/juhi
command -v docker &> /dev/null || { echo "Docker 未安装"; exit 1; }
docker compose version &> /dev/null || docker-compose version &> /dev/null || { echo "Docker Compose 未安装"; exit 1; }
DISK_USAGE=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')
if [ "$DISK_USAGE" -gt 90 ]; then
echo "磁盘使用率过高: ${DISK_USAGE}%"
exit 1
fi
EOF
env:
HOST: ${{ secrets.ALIYUN_HOST }}
USER: ${{ secrets.ALIYUN_USER }}
- name: 数据库备份
run: |
ssh $USER@$HOST << 'EOF'
cd /opt/juhi
BACKUP_DIR="/opt/juhi/backups"
mkdir -p "$BACKUP_DIR"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/pre_deploy_${TIMESTAMP}.sql.gz"
docker exec juhi-postgres pg_dump -U "${DB_USER:-juhi_user}" -d "${DB_NAME:-juhi_db}" | gzip > "$BACKUP_FILE"
if [ $? -eq 0 ]; then
echo "备份完成: $(du -h "$BACKUP_FILE" | cut -f1)"
else
echo "备份失败"
exit 1
fi
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +30 -delete 2>/dev/null || true
EOF
env:
HOST: ${{ secrets.ALIYUN_HOST }}
USER: ${{ secrets.ALIYUN_USER }}
- name: 部署到阿里云
run: |
ssh $USER@$HOST << EOF
cd $DEPLOY_PATH
CURRENT_VERSION=\$(cat .deployed_version 2>/dev/null || echo "unknown")
docker compose -f docker-compose.prod.yml pull api frontend
echo "==> 数据库迁移..."
docker compose -f docker-compose.prod.yml --profile migrate run --rm migrate
if [ \$? -ne 0 ]; then
echo "数据库迁移失败"
exit 1
fi
docker compose -f docker-compose.prod.yml up -d --no-deps --remove-orphans api
RETRY=0
MAX_RETRY=15
until curl -sf http://localhost:3000/health > /dev/null 2>&1; do
RETRY=\$((RETRY + 1))
if [ \$RETRY -ge \$MAX_RETRY ]; then
echo "API 健康检查超时"
docker compose -f docker-compose.prod.yml logs --tail=50 api
exit 1
fi
sleep 5
done
docker compose -f docker-compose.prod.yml up -d --no-deps frontend
docker compose -f docker-compose.prod.yml exec -T nginx nginx -s reload 2>/dev/null || true
docker image prune -f --filter "until=24h"
echo "${VERSION}" > .deployed_version
echo "\$(date -Iseconds)|${VERSION}|\${CURRENT_VERSION}" >> .deploy-history
tail -20 .deploy-history > .deploy-history.tmp && mv .deploy-history.tmp .deploy-history
echo "==> 阿里云部署完成: ${VERSION}"
EOF
env:
HOST: ${{ secrets.ALIYUN_HOST }}
USER: ${{ secrets.ALIYUN_USER }}
DEPLOY_PATH: /opt/juhi
VERSION: ${{ needs.build-and-push.outputs.version }}
- name: 部署验证
run: |
ssh $USER@$HOST << 'EOF'
cd /opt/juhi
if [ -f "./scripts/post-deploy-verify.sh" ]; then
chmod +x ./scripts/post-deploy-verify.sh
./scripts/post-deploy-verify.sh --quick
else
curl -sf http://localhost:3000/health || exit 1
curl -sf http://localhost/ || echo "前端检查跳过"
UNHEALTHY=$(docker compose -f docker-compose.prod.yml ps --format json | grep -c '"unhealthy"' || true)
if [ "$UNHEALTHY" -gt 0 ]; then
docker compose -f docker-compose.prod.yml ps
exit 1
fi
echo "阿里云部署验证通过"
fi
EOF
env:
HOST: ${{ secrets.ALIYUN_HOST }}
USER: ${{ secrets.ALIYUN_USER }}
- if: startsWith(github.ref, 'refs/tags/v')
name: 创建 GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: "true"
make_latest: "true"
timeout-minutes: "20"
...
|
deploy-aliyun
|
["gate","build-and-push"]
|
["ubuntu-latest"]
|
12872
|
4
|
1775186357
|
1775186357
|
1775186343
|
1775186358
|
|
1
|
|
0
|
Edit
Delete
|
|
15580
|
10845
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
Production 自动回滚
|
1
|
name: CI/CD Deploy
"on":
# test-pipeli name: CI/CD Deploy
"on":
# test-pipeline 通过后自动触发(仅 main 分支)
workflow_run:
workflows: ["Test Pipeline"]
types: [completed]
branches: [main]
# 版本标签触发完整部署
push:
tags: ['v*']
# 手动触发
workflow_dispatch:
inputs:
environment:
description: '部署环境'
required: true
default: 'staging'
type: choice
options:
- staging
- production
- aliyun
- rollback-production
- rollback-aliyun
skip_tests:
description: '跳过测试(紧急修复)'
required: false
default: false
type: boolean
version:
description: '部署版本号(留空使用自动版本)'
required: false
type: string
env:
IMAGE_PREFIX: ${{ github.repository_owner }}/juhi
NODE_VERSION: "20"
PNPM_VERSION: "8"
REGISTRY: ghcr.io
jobs:
auto-rollback-production:
name: Production 自动回滚
runs-on: ubuntu-latest
if: failure() && needs.deploy-production.result == 'failure'
steps:
- uses: webfactory/ssh-agent@v0.8.0
with:
ssh-private-key: ${{ secrets.PRODUCTION_SSH_KEY }}
- name: 配置 SSH Known Hosts
run: |
mkdir -p ~/.ssh
echo "${{ secrets.PRODUCTION_SSH_KNOWN_HOSTS }}" >> ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
- name: 执行回滚
run: |
ssh $USER@$HOST << 'EOF'
cd /opt/juhi
echo "==> Production 自动回滚..."
if [ -f "./scripts/rollback.sh" ]; then
chmod +x ./scripts/rollback.sh
./scripts/rollback.sh --confirm -y
else
PREV_LINE=$(tail -1 .deploy-history 2>/dev/null)
PREV_API=$(echo "$PREV_LINE" | cut -d'|' -f2)
PREV_FE=$(echo "$PREV_LINE" | cut -d'|' -f3)
if [ -n "$PREV_API" ] && [ "$PREV_API" != "none" ]; then
export API_IMAGE="$PREV_API"
export FRONTEND_IMAGE="$PREV_FE"
docker compose -f docker-compose.prod.yml up -d --no-deps api frontend
RETRY=0
until curl -sf http://localhost:3000/health > /dev/null 2>&1; do
RETRY=$((RETRY + 1))
[ $RETRY -ge 12 ] && { echo "回滚后健康检查失败"; exit 1; }
sleep 5
done
echo "==> 回滚成功"
else
echo "无法获取上一版本,需要手动回滚"
exit 1
fi
fi
EOF
env:
HOST: ${{ secrets.PRODUCTION_HOST }}
USER: ${{ secrets.PRODUCTION_USER }}
timeout-minutes: "10"
...
|
auto-rollback-production
|
["deploy-production"]
|
["ubuntu-latest"]
|
12875
|
4
|
1775186363
|
1775186363
|
1775186343
|
1775186364
|
|
1
|
|
0
|
Edit
Delete
|
|
15581
|
10845
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
阿里云自动回滚
|
1
|
name: CI/CD Deploy
"on":
# test-pipeli name: CI/CD Deploy
"on":
# test-pipeline 通过后自动触发(仅 main 分支)
workflow_run:
workflows: ["Test Pipeline"]
types: [completed]
branches: [main]
# 版本标签触发完整部署
push:
tags: ['v*']
# 手动触发
workflow_dispatch:
inputs:
environment:
description: '部署环境'
required: true
default: 'staging'
type: choice
options:
- staging
- production
- aliyun
- rollback-production
- rollback-aliyun
skip_tests:
description: '跳过测试(紧急修复)'
required: false
default: false
type: boolean
version:
description: '部署版本号(留空使用自动版本)'
required: false
type: string
env:
IMAGE_PREFIX: ${{ github.repository_owner }}/juhi
NODE_VERSION: "20"
PNPM_VERSION: "8"
REGISTRY: ghcr.io
jobs:
auto-rollback-aliyun:
name: 阿里云自动回滚
runs-on: ubuntu-latest
if: failure() && needs.deploy-aliyun.result == 'failure'
steps:
- uses: webfactory/ssh-agent@v0.8.0
with:
ssh-private-key: ${{ secrets.ALIYUN_SSH_PRIVATE_KEY }}
- name: 配置 SSH Known Hosts
run: |
mkdir -p ~/.ssh
echo "${{ secrets.ALIYUN_SSH_KNOWN_HOSTS }}" >> ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
- name: 执行回滚
run: |
ssh $USER@$HOST << 'EOF'
cd /opt/juhi
echo "==> 阿里云自动回滚..."
if [ -f "./scripts/rollback.sh" ]; then
chmod +x ./scripts/rollback.sh
./scripts/rollback.sh --version 1 --confirm
else
PREV_VERSION=$(tail -1 .deploy-history 2>/dev/null | cut -d'|' -f3)
if [ -n "$PREV_VERSION" ]; then
docker compose -f docker-compose.prod.yml up -d --no-deps api frontend
echo "==> 回滚完成"
else
echo "无法获取上一版本"
exit 1
fi
fi
EOF
env:
HOST: ${{ secrets.ALIYUN_HOST }}
USER: ${{ secrets.ALIYUN_USER }}
timeout-minutes: "10"
...
|
auto-rollback-aliyun
|
["deploy-aliyun"]
|
["ubuntu-latest"]
|
12874
|
4
|
1775186361
|
1775186361
|
1775186343
|
1775186362
|
|
1
|
|
0
|
Edit
Delete
|
|
15582
|
10845
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
手动回滚
|
1
|
name: CI/CD Deploy
"on":
# test-pipeli name: CI/CD Deploy
"on":
# test-pipeline 通过后自动触发(仅 main 分支)
workflow_run:
workflows: ["Test Pipeline"]
types: [completed]
branches: [main]
# 版本标签触发完整部署
push:
tags: ['v*']
# 手动触发
workflow_dispatch:
inputs:
environment:
description: '部署环境'
required: true
default: 'staging'
type: choice
options:
- staging
- production
- aliyun
- rollback-production
- rollback-aliyun
skip_tests:
description: '跳过测试(紧急修复)'
required: false
default: false
type: boolean
version:
description: '部署版本号(留空使用自动版本)'
required: false
type: string
env:
IMAGE_PREFIX: ${{ github.repository_owner }}/juhi
NODE_VERSION: "20"
PNPM_VERSION: "8"
REGISTRY: ghcr.io
jobs:
manual-rollback:
name: 手动回滚
runs-on: ubuntu-latest
if: needs.gate.outputs.is_rollback == 'true'
steps:
- uses: actions/checkout@v4
- if: needs.gate.outputs.target_env == 'rollback-production'
name: 配置 SSH(Production)
uses: webfactory/ssh-agent@v0.8.0
with:
ssh-private-key: ${{ secrets.PRODUCTION_SSH_KEY }}
- if: needs.gate.outputs.target_env == 'rollback-aliyun'
name: 配置 SSH(阿里云)
uses: webfactory/ssh-agent@v0.8.0
with:
ssh-private-key: ${{ secrets.ALIYUN_SSH_PRIVATE_KEY }}
- name: 配置 SSH Known Hosts
run: |
mkdir -p ~/.ssh
if [ "${{ needs.gate.outputs.target_env }}" == "rollback-production" ]; then
echo "${{ secrets.PRODUCTION_SSH_KNOWN_HOSTS }}" >> ~/.ssh/known_hosts
else
echo "${{ secrets.ALIYUN_SSH_KNOWN_HOSTS }}" >> ~/.ssh/known_hosts
fi
chmod 644 ~/.ssh/known_hosts
- name: 执行回滚
run: |
if [ "${{ needs.gate.outputs.target_env }}" == "rollback-production" ]; then
HOST="${{ secrets.PRODUCTION_HOST }}"
USER="${{ secrets.PRODUCTION_USER }}"
else
HOST="${{ secrets.ALIYUN_HOST }}"
USER="${{ secrets.ALIYUN_USER }}"
fi
ssh $USER@$HOST << 'EOF'
cd /opt/juhi
echo "==> 手动回滚..."
if [ -f "./scripts/rollback.sh" ]; then
chmod +x ./scripts/rollback.sh
./scripts/rollback.sh --version 1 --confirm
else
echo "rollback.sh 不存在,需要手动操作"
echo "部署历史:"
tail -10 .deploy-history 2>/dev/null || echo "(无记录)"
exit 1
fi
if [ -f "./scripts/post-deploy-verify.sh" ]; then
chmod +x ./scripts/post-deploy-verify.sh
./scripts/post-deploy-verify.sh --quick
fi
EOF
timeout-minutes: "10"
...
|
manual-rollback
|
["gate"]
|
["ubuntu-latest"]
|
12869
|
4
|
1775186351
|
1775186352
|
1775186343
|
1775186352
|
|
1
|
|
0
|
Edit
Delete
|
|
15583
|
10845
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
部署通知
|
1
|
name: CI/CD Deploy
"on":
# test-pipeli name: CI/CD Deploy
"on":
# test-pipeline 通过后自动触发(仅 main 分支)
workflow_run:
workflows: ["Test Pipeline"]
types: [completed]
branches: [main]
# 版本标签触发完整部署
push:
tags: ['v*']
# 手动触发
workflow_dispatch:
inputs:
environment:
description: '部署环境'
required: true
default: 'staging'
type: choice
options:
- staging
- production
- aliyun
- rollback-production
- rollback-aliyun
skip_tests:
description: '跳过测试(紧急修复)'
required: false
default: false
type: boolean
version:
description: '部署版本号(留空使用自动版本)'
required: false
type: string
env:
IMAGE_PREFIX: ${{ github.repository_owner }}/juhi
NODE_VERSION: "20"
PNPM_VERSION: "8"
REGISTRY: ghcr.io
jobs:
notify:
name: 部署通知
runs-on: ubuntu-latest
if: always()
steps:
- name: 生成部署报告
run: |
echo "## CI/CD 部署报告" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **触发**: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
echo "- **分支**: \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **提交**: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **触发者**: ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| 阶段 | 状态 |" >> $GITHUB_STEP_SUMMARY
echo "|------|------|" >> $GITHUB_STEP_SUMMARY
echo "| Staging | ${{ needs.deploy-staging.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Production | ${{ needs.deploy-production.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY
echo "| 阿里云 | ${{ needs.deploy-aliyun.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY
echo "| 手动回滚 | ${{ needs.manual-rollback.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY
- if: always() && (secrets.SLACK_WEBHOOK != '')
name: Slack 通知
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: CI/CD Deploy - ${{ needs.gate.outputs.target_env }} - ${{ github.ref_name }}
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
...
|
notify
|
["gate","deploy-staging","depl ["gate","deploy-staging","deploy-production","deploy-aliyun","manual-rollback","auto-rollback-production","auto-rollback-aliyun"]...
|
["ubuntu-latest"]
|
12876
|
1
|
1775186367
|
1775186369
|
1775186343
|
1775186369
|
|
1
|
|
0
|
Edit
Delete
|
|
15840
|
11095
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
test-summary
|
1
|
name: E2E Tests
"on":
# PR validation name: E2E Tests
"on":
# PR validation - 运行关键测试
pull_request:
branches: [main, develop]
paths:
- 'frontend/**'
- 'backend/**'
- 'e2e/**'
- 'package.json'
- 'pnpm-lock.yaml'
# Push to main - 运行完整测试套件
push:
branches: [main]
# 每日定时全量测试 (UTC 时间 00:00 = 北京时间 08:00)
schedule:
- cron: '0 0 * * *'
# 手动触发
workflow_dispatch:
inputs:
test_suite:
description: 'Test suite to run'
required: true
default: 'all'
type: choice
options:
- all
- critical
- business-flows
- visual-regression
- performance
env:
NODE_VERSION: "18"
PNPM_VERSION: "8"
jobs:
test-summary:
name: test-summary
runs-on: ubuntu-latest
if: always()
steps:
- name: Download all test artifacts
uses: actions/download-artifact@v4
- name: Generate summary report
run: "echo \"## \U0001F9EA E2E 测试汇总\" >> $GITHUB_STEP_SUMMARY\necho \"\" >> $GITHUB_STEP_SUMMARY\n\nif [ -d \"playwright-report-pr\" ]; then\n echo \"### PR 快速验证\" >> $GITHUB_STEP_SUMMARY\n echo \"✅ 关键测试通过\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"business-flows-report\" ]; then\n echo \"### 业务流程测试\" >> $GITHUB_STEP_SUMMARY\n echo \"✅ 业务流程测试完成\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"performance-report\" ]; then\n echo \"### 性能基准测试\" >> $GITHUB_STEP_SUMMARY\n echo \"\U0001F4CA 性能测试报告已生成\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"visual-regression-report\" ]; then\n echo \"### 视觉回归测试\" >> $GITHUB_STEP_SUMMARY\n echo \"\U0001F3A8 视觉对比完成\" >> $GITHUB_STEP_SUMMARY\nfi\n"
...
|
test-summary
|
["pr-validation","full-test-suite" ["pr-validation","full-test-suite","business-flows","performance-benchmarks","visual-regression"]...
|
["ubuntu-latest"]
|
13135
|
2
|
1775261529
|
1775261532
|
1775260818
|
1775261532
|
|
1
|
|
0
|
Edit
Delete
|
|
16136
|
11384
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
test-summary
|
1
|
name: E2E Tests
"on":
# PR validation name: E2E Tests
"on":
# PR validation - 运行关键测试
pull_request:
branches: [main, develop]
paths:
- 'frontend/**'
- 'backend/**'
- 'e2e/**'
- 'package.json'
- 'pnpm-lock.yaml'
# Push to main - 运行完整测试套件
push:
branches: [main]
# 每日定时全量测试 (UTC 时间 00:00 = 北京时间 08:00)
schedule:
- cron: '0 0 * * *'
# 手动触发
workflow_dispatch:
inputs:
test_suite:
description: 'Test suite to run'
required: true
default: 'all'
type: choice
options:
- all
- critical
- business-flows
- visual-regression
- performance
env:
NODE_VERSION: "18"
PNPM_VERSION: "8"
jobs:
test-summary:
name: test-summary
runs-on: ubuntu-latest
if: always()
steps:
- name: Download all test artifacts
uses: actions/download-artifact@v4
- name: Generate summary report
run: "echo \"## \U0001F9EA E2E 测试汇总\" >> $GITHUB_STEP_SUMMARY\necho \"\" >> $GITHUB_STEP_SUMMARY\n\nif [ -d \"playwright-report-pr\" ]; then\n echo \"### PR 快速验证\" >> $GITHUB_STEP_SUMMARY\n echo \"✅ 关键测试通过\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"business-flows-report\" ]; then\n echo \"### 业务流程测试\" >> $GITHUB_STEP_SUMMARY\n echo \"✅ 业务流程测试完成\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"performance-report\" ]; then\n echo \"### 性能基准测试\" >> $GITHUB_STEP_SUMMARY\n echo \"\U0001F4CA 性能测试报告已生成\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"visual-regression-report\" ]; then\n echo \"### 视觉回归测试\" >> $GITHUB_STEP_SUMMARY\n echo \"\U0001F3A8 视觉对比完成\" >> $GITHUB_STEP_SUMMARY\nfi\n"
...
|
test-summary
|
["pr-validation","full-test-suite" ["pr-validation","full-test-suite","business-flows","performance-benchmarks","visual-regression"]...
|
["ubuntu-latest"]
|
13431
|
2
|
1775347910
|
1775348001
|
1775347218
|
1775348001
|
|
1
|
|
0
|
Edit
Delete
|
|
16432
|
11673
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
test-summary
|
1
|
name: E2E Tests
"on":
# PR validation name: E2E Tests
"on":
# PR validation - 运行关键测试
pull_request:
branches: [main, develop]
paths:
- 'frontend/**'
- 'backend/**'
- 'e2e/**'
- 'package.json'
- 'pnpm-lock.yaml'
# Push to main - 运行完整测试套件
push:
branches: [main]
# 每日定时全量测试 (UTC 时间 00:00 = 北京时间 08:00)
schedule:
- cron: '0 0 * * *'
# 手动触发
workflow_dispatch:
inputs:
test_suite:
description: 'Test suite to run'
required: true
default: 'all'
type: choice
options:
- all
- critical
- business-flows
- visual-regression
- performance
env:
NODE_VERSION: "18"
PNPM_VERSION: "8"
jobs:
test-summary:
name: test-summary
runs-on: ubuntu-latest
if: always()
steps:
- name: Download all test artifacts
uses: actions/download-artifact@v4
- name: Generate summary report
run: "echo \"## \U0001F9EA E2E 测试汇总\" >> $GITHUB_STEP_SUMMARY\necho \"\" >> $GITHUB_STEP_SUMMARY\n\nif [ -d \"playwright-report-pr\" ]; then\n echo \"### PR 快速验证\" >> $GITHUB_STEP_SUMMARY\n echo \"✅ 关键测试通过\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"business-flows-report\" ]; then\n echo \"### 业务流程测试\" >> $GITHUB_STEP_SUMMARY\n echo \"✅ 业务流程测试完成\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"performance-report\" ]; then\n echo \"### 性能基准测试\" >> $GITHUB_STEP_SUMMARY\n echo \"\U0001F4CA 性能测试报告已生成\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"visual-regression-report\" ]; then\n echo \"### 视觉回归测试\" >> $GITHUB_STEP_SUMMARY\n echo \"\U0001F3A8 视觉对比完成\" >> $GITHUB_STEP_SUMMARY\nfi\n"
...
|
test-summary
|
["pr-validation","full-test-suite" ["pr-validation","full-test-suite","business-flows","performance-benchmarks","visual-regression"]...
|
["ubuntu-latest"]
|
13725
|
2
|
1775433870
|
1775433900
|
1775433618
|
1775433900
|
|
1
|
|
0
|
Edit
Delete
|
|
16457
|
11697
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
📊 API 基准测试
|
1
|
name: Performance Tests
"on":
# 每周一凌晨 name: Performance Tests
"on":
# 每周一凌晨 2 点运行
schedule:
- cron: '0 2 * * 1'
# 允许手动触发
workflow_dispatch:
inputs:
test_type:
description: '测试类型'
required: true
default: 'benchmark'
type: choice
options:
- benchmark
- load
- stress
- all
duration:
description: '测试持续时间(秒)'
required: false
default: '60'
type: string
concurrency:
description: '并发数'
required: false
default: '10'
type: string
# PR 触发时只运行基准测试
pull_request:
branches: [main]
paths:
- 'backend/src/**'
- 'backend/prisma/**'
env:
NODE_VERSION: "18"
PNPM_VERSION: "8"
jobs:
benchmark:
name: "\U0001F4CA API 基准测试"
runs-on: ubuntu-latest
if: needs.setup.outputs.test_type == 'benchmark' || needs.setup.outputs.test_type == 'all'
steps:
- name: "\U0001F4E5 检出代码"
uses: actions/checkout@v4
- name: "\U0001F4E6 安装 pnpm"
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: "\U0001F7E2 设置 Node.js"
uses: actions/setup-node@v4
with:
cache: pnpm
node-version: ${{ env.NODE_VERSION }}
- name: "\U0001F4E6 安装依赖"
run: pnpm install --frozen-lockfile
- name: "\U0001F5C4️ 初始化数据库"
run: |
cd backend
npx prisma migrate deploy
npx prisma db seed
env:
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_perf
- name: "\U0001F528 构建后端"
run: |
pnpm --filter shared build
pnpm --filter backend build
- name: "\U0001F680 启动后端服务"
run: |
cd backend
npm run start &
sleep 10
env:
NODE_ENV: production
PORT: 3000
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_perf
REDIS_URL: redis://localhost:6379
JWT_SECRET: perf-test-jwt-secret
REFRESH_TOKEN_SECRET: perf-test-refresh-token
- name: ⏳ 等待服务就绪
run: |
timeout 60 bash -c 'until curl -s http://localhost:3000/health > /dev/null; do sleep 2; done'
echo "服务已就绪"
- name: "\U0001F4CA 运行基准测试"
run: |
cd backend
npm run test -- tests/examples/performance.test.ts --reporter=json --outputFile=benchmark-results.json
env:
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_perf
API_URL: http://localhost:3000
- name: "\U0001F4DD 生成基准报告"
run: "echo \"## \U0001F4CA API 基准测试报告\" >> $GITHUB_STEP_SUMMARY\necho \"\" >> $GITHUB_STEP_SUMMARY\necho \"测试时间: $(date)\" >> $GITHUB_STEP_SUMMARY\necho \"\" >> $GITHUB_STEP_SUMMARY\n\nif [ -f \"backend/benchmark-results.json\" ]; then\n echo \"### 测试结果\" >> $GITHUB_STEP_SUMMARY\n cat backend/benchmark-results.json | jq '.testResults[].assertionResults[] | {name: .ancestorTitles[-1] + \" > \" + .title, status: .status}' >> $GITHUB_STEP_SUMMARY || true\nfi\n"
- name: "\U0001F4E4 上传基准测试结果"
uses: actions/upload-artifact@v4
with:
name: benchmark-results
path: backend/benchmark-results.json
retention-days: "30"
timeout-minutes: "30"
services:
postgres:
image: postgres:15
env:
POSTGRES_DB: juhi_perf
POSTGRES_PASSWORD: test
POSTGRES_USER: test
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
...
|
benchmark
|
["setup"]
|
["ubuntu-latest"]
|
13751
|
2
|
1775440822
|
1775440994
|
1775440818
|
1775440994
|
|
1
|
|
0
|
Edit
Delete
|
|
16458
|
11697
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
🔥 负载测试
|
1
|
name: Performance Tests
"on":
# 每周一凌晨 name: Performance Tests
"on":
# 每周一凌晨 2 点运行
schedule:
- cron: '0 2 * * 1'
# 允许手动触发
workflow_dispatch:
inputs:
test_type:
description: '测试类型'
required: true
default: 'benchmark'
type: choice
options:
- benchmark
- load
- stress
- all
duration:
description: '测试持续时间(秒)'
required: false
default: '60'
type: string
concurrency:
description: '并发数'
required: false
default: '10'
type: string
# PR 触发时只运行基准测试
pull_request:
branches: [main]
paths:
- 'backend/src/**'
- 'backend/prisma/**'
env:
NODE_VERSION: "18"
PNPM_VERSION: "8"
jobs:
load-test:
name: "\U0001F525 负载测试"
runs-on: ubuntu-latest
if: needs.setup.outputs.test_type == 'load' || needs.setup.outputs.test_type == 'all'
steps:
- name: "\U0001F4E5 检出代码"
uses: actions/checkout@v4
- name: "\U0001F4E6 安装 pnpm"
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: "\U0001F7E2 设置 Node.js"
uses: actions/setup-node@v4
with:
cache: pnpm
node-version: ${{ env.NODE_VERSION }}
- name: "\U0001F4E6 安装依赖"
run: pnpm install --frozen-lockfile
- name: "\U0001F527 安装 k6"
run: |
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
- name: "\U0001F5C4️ 初始化数据库"
run: |
cd backend
npx prisma migrate deploy
npx prisma db seed
env:
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_load
- name: "\U0001F528 构建后端"
run: |
pnpm --filter shared build
pnpm --filter backend build
- name: "\U0001F680 启动后端服务"
run: |
cd backend
npm run start &
sleep 10
env:
NODE_ENV: production
PORT: 3000
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_load
REDIS_URL: redis://localhost:6379
JWT_SECRET: load-test-jwt-secret
REFRESH_TOKEN_SECRET: load-test-refresh-token
- name: ⏳ 等待服务就绪
run: |
timeout 60 bash -c 'until curl -s http://localhost:3000/health > /dev/null; do sleep 2; done'
- name: "\U0001F525 运行负载测试"
run: |
mkdir -p load-test-results
# 创建 k6 负载测试脚本
cat > load-test.js << 'EOF'
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
const errorRate = new Rate('errors');
const responseTime = new Trend('response_time');
export const options = {
stages: [
{ duration: '30s', target: 10 }, // 预热
{ duration: '1m', target: 50 }, // 逐步增加
{ duration: '2m', target: 50 }, // 稳定负载
{ duration: '30s', target: 0 }, // 降低
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% 请求小于 500ms
errors: ['rate<0.1'], // 错误率小于 10%
},
};
const BASE_URL = 'http://localhost:3000';
export default function () {
// 健康检查
let healthRes = http.get(`${BASE_URL}/health`);
check(healthRes, { 'health check ok': (r) => r.status === 200 });
// 列表接口
let listRes = http.get(`${BASE_URL}/api/leads?page=1&pageSize=20`, {
headers: {
'Authorization': 'Bearer test-token',
'Content-Type': 'application/json',
},
});
errorRate.add(listRes.status !== 200 && listRes.status !== 401);
responseTime.add(listRes.timings.duration);
sleep(0.1);
}
EOF
k6 run --out json=load-test-results/results.json load-test.js
- name: "\U0001F4CA 分析负载测试结果"
run: "echo \"## \U0001F525 负载测试报告\" >> $GITHUB_STEP_SUMMARY\necho \"\" >> $GITHUB_STEP_SUMMARY\n\nif [ -f \"load-test-results/results.json\" ]; then\n echo \"### 关键指标\" >> $GITHUB_STEP_SUMMARY\n echo \"| 指标 | 值 |\" >> $GITHUB_STEP_SUMMARY\n echo \"|------|------|\" >> $GITHUB_STEP_SUMMARY\n\n # 提取关键指标\n avg_duration=$(cat load-test-results/results.json | jq -s '[.[] | select(.type==\"Point\" and .metric==\"http_req_duration\") | .data.value] | add / length' 2>/dev/null || echo \"N/A\")\n p95_duration=$(cat load-test-results/results.json | jq -s '[.[] | select(.type==\"Point\" and .metric==\"http_req_duration\") | .data.value] | sort | .[length * 0.95 | floor]' 2>/dev/null || echo \"N/A\")\n total_requests=$(cat load-test-results/results.json | jq -s '[.[] | select(.type==\"Point\" and .metric==\"http_reqs\")] | length' 2>/dev/null || echo \"N/A\")\n\n echo \"| 平均响应时间 | ${avg_duration}ms |\" >> $GITHUB_STEP_SUMMARY\n echo \"| P95 响应时间 | ${p95_duration}ms |\" >> $GITHUB_STEP_SUMMARY\n echo \"| 总请求数 | $total_requests |\" >> $GITHUB_STEP_SUMMARY\nfi\n"
- name: "\U0001F4E4 上传负载测试结果"
uses: actions/upload-artifact@v4
with:
name: load-test-results
path: load-test-results/
retention-days: "30"
timeout-minutes: "45"
services:
postgres:
image: postgres:15
env:
POSTGRES_DB: juhi_load
POSTGRES_PASSWORD: test
POSTGRES_USER: test
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
...
|
load-test
|
["setup"]
|
["ubuntu-latest"]
|
13752
|
2
|
1775440994
|
1775441168
|
1775440818
|
1775441168
|
|
1
|
|
0
|
Edit
Delete
|
|
16459
|
11697
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
💥 压力测试
|
1
|
name: Performance Tests
"on":
# 每周一凌晨 name: Performance Tests
"on":
# 每周一凌晨 2 点运行
schedule:
- cron: '0 2 * * 1'
# 允许手动触发
workflow_dispatch:
inputs:
test_type:
description: '测试类型'
required: true
default: 'benchmark'
type: choice
options:
- benchmark
- load
- stress
- all
duration:
description: '测试持续时间(秒)'
required: false
default: '60'
type: string
concurrency:
description: '并发数'
required: false
default: '10'
type: string
# PR 触发时只运行基准测试
pull_request:
branches: [main]
paths:
- 'backend/src/**'
- 'backend/prisma/**'
env:
NODE_VERSION: "18"
PNPM_VERSION: "8"
jobs:
stress-test:
name: "\U0001F4A5 压力测试"
runs-on: ubuntu-latest
if: needs.setup.outputs.test_type == 'stress' || needs.setup.outputs.test_type == 'all'
steps:
- name: "\U0001F4E5 检出代码"
uses: actions/checkout@v4
- name: "\U0001F4E6 安装 pnpm"
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: "\U0001F7E2 设置 Node.js"
uses: actions/setup-node@v4
with:
cache: pnpm
node-version: ${{ env.NODE_VERSION }}
- name: "\U0001F4E6 安装依赖"
run: pnpm install --frozen-lockfile
- name: "\U0001F527 安装 k6"
run: |
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
- name: "\U0001F5C4️ 初始化数据库"
run: |
cd backend
npx prisma migrate deploy
npx prisma db seed
env:
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_stress
- name: "\U0001F528 构建后端"
run: |
pnpm --filter shared build
pnpm --filter backend build
- name: "\U0001F680 启动后端服务"
run: |
cd backend
npm run start &
sleep 10
env:
NODE_ENV: production
PORT: 3000
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_stress
REDIS_URL: redis://localhost:6379
JWT_SECRET: stress-test-jwt-secret
REFRESH_TOKEN_SECRET: stress-test-refresh-token
- name: ⏳ 等待服务就绪
run: |
timeout 60 bash -c 'until curl -s http://localhost:3000/health > /dev/null; do sleep 2; done'
- name: "\U0001F4A5 运行压力测试"
run: |
mkdir -p stress-test-results
# 创建 k6 压力测试脚本
cat > stress-test.js << 'EOF'
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend, Counter } from 'k6/metrics';
const errorRate = new Rate('errors');
const responseTime = new Trend('response_time');
const requestCount = new Counter('requests');
export const options = {
stages: [
{ duration: '1m', target: 20 }, // 预热
{ duration: '2m', target: 100 }, // 逐步增加到 100 并发
{ duration: '2m', target: 200 }, // 增加到 200 并发
{ duration: '2m', target: 300 }, // 增加到 300 并发
{ duration: '1m', target: 0 }, // 降低
],
thresholds: {
http_req_duration: ['p(99)<2000'], // 99% 请求小于 2s
errors: ['rate<0.3'], // 错误率小于 30%
},
};
const BASE_URL = 'http://localhost:3000';
export default function () {
requestCount.add(1);
let res = http.get(`${BASE_URL}/health`);
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 1000ms': (r) => r.timings.duration < 1000,
});
errorRate.add(res.status !== 200);
responseTime.add(res.timings.duration);
sleep(0.05);
}
EOF
k6 run --out json=stress-test-results/results.json stress-test.js || true
- name: "\U0001F4CA 分析压力测试结果"
run: "echo \"## \U0001F4A5 压力测试报告\" >> $GITHUB_STEP_SUMMARY\necho \"\" >> $GITHUB_STEP_SUMMARY\necho \"### 测试目标\" >> $GITHUB_STEP_SUMMARY\necho \"- 最大并发: 300\" >> $GITHUB_STEP_SUMMARY\necho \"- 持续时间: 8 分钟\" >> $GITHUB_STEP_SUMMARY\necho \"\" >> $GITHUB_STEP_SUMMARY\n\nif [ -f \"stress-test-results/results.json\" ]; then\n echo \"### 结果分析\" >> $GITHUB_STEP_SUMMARY\n\n max_vus=$(cat stress-test-results/results.json | jq -s 'max_by(.data.value | numbers) | .data.value // 0' 2>/dev/null || echo \"N/A\")\n echo \"- 最大达到 VUs: $max_vus\" >> $GITHUB_STEP_SUMMARY\nfi\n"
- name: "\U0001F4E4 上传压力测试结果"
uses: actions/upload-artifact@v4
with:
name: stress-test-results
path: stress-test-results/
retention-days: "30"
timeout-minutes: "60"
services:
postgres:
image: postgres:15
env:
POSTGRES_DB: juhi_stress
POSTGRES_PASSWORD: test
POSTGRES_USER: test
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
...
|
stress-test
|
["setup"]
|
["ubuntu-latest"]
|
13753
|
2
|
1775441168
|
1775441259
|
1775440818
|
1775441259
|
|
1
|
|
0
|
Edit
Delete
|
|
16460
|
11697
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
🗄️ 数据库性能测试
|
1
|
name: Performance Tests
"on":
# 每周一凌晨 name: Performance Tests
"on":
# 每周一凌晨 2 点运行
schedule:
- cron: '0 2 * * 1'
# 允许手动触发
workflow_dispatch:
inputs:
test_type:
description: '测试类型'
required: true
default: 'benchmark'
type: choice
options:
- benchmark
- load
- stress
- all
duration:
description: '测试持续时间(秒)'
required: false
default: '60'
type: string
concurrency:
description: '并发数'
required: false
default: '10'
type: string
# PR 触发时只运行基准测试
pull_request:
branches: [main]
paths:
- 'backend/src/**'
- 'backend/prisma/**'
env:
NODE_VERSION: "18"
PNPM_VERSION: "8"
jobs:
db-performance:
name: "\U0001F5C4️ 数据库性能测试"
runs-on: ubuntu-latest
if: needs.setup.outputs.test_type == 'all'
steps:
- name: "\U0001F4E5 检出代码"
uses: actions/checkout@v4
- name: "\U0001F4E6 安装 pnpm"
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: "\U0001F7E2 设置 Node.js"
uses: actions/setup-node@v4
with:
cache: pnpm
node-version: ${{ env.NODE_VERSION }}
- name: "\U0001F4E6 安装依赖"
run: pnpm install --frozen-lockfile
- name: "\U0001F5C4️ 初始化数据库"
run: |
cd backend
npx prisma migrate deploy
env:
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_db_perf
- name: "\U0001F528 生成测试数据"
run: |
cd backend
# 生成大量测试数据用于性能测试
cat > generate-test-data.ts << 'EOF'
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const TENANT_ID = '00000000-0000-0000-0000-000000000001';
async function main() {
console.log('生成测试数据...');
// 创建租户
await prisma.tenants.upsert({
where: { id: TENANT_ID },
create: { id: TENANT_ID, name: 'Performance Test Tenant', code: 'PERF' },
update: {},
});
// 批量创建线索(10000 条)
const leads = [];
for (let i = 0; i < 10000; i++) {
leads.push({
id: `lead-${i.toString().padStart(5, '0')}`,
tenant_id: TENANT_ID,
biz_id: `LEAD-${Date.now()}-${i}`,
company_name: `测试公司 ${i}`,
contact_name: `联系人 ${i}`,
contact_phone: `138${i.toString().padStart(8, '0')}`,
status: ['new', 'contacted', 'qualified'][i % 3],
source: ['website', 'referral', 'advertisement'][i % 3],
created_at: new Date(),
updated_at: new Date(),
});
}
await prisma.leads.createMany({
data: leads,
skipDuplicates: true,
});
console.log('已创建 10000 条线索');
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());
EOF
npx tsx generate-test-data.ts
env:
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_db_perf
- name: "\U0001F9EA 运行数据库性能测试"
run: "cd backend\ncat > db-perf-test.ts << 'EOF'\nimport { PrismaClient } from '@prisma/client';\n\nconst prisma = new PrismaClient({\n log: ['query'],\n});\nconst TENANT_ID = '00000000-0000-0000-0000-000000000001';\n\ninterface PerfResult {\n name: string;\n avgMs: number;\n minMs: number;\n maxMs: number;\n iterations: number;\n}\n\nasync function benchmark(name: string, fn: () => Promise<any>, iterations = 100): Promise<PerfResult> {\n const times: number[] = [];\n\n // 预热\n for (let i = 0; i < 5; i++) {\n await fn();\n }\n\n // 正式测试\n for (let i = 0; i < iterations; i++) {\n const start = performance.now();\n await fn();\n times.push(performance.now() - start);\n }\n\n return {\n name,\n avgMs: times.reduce((a, b) => a + b, 0) / times.length,\n minMs: Math.min(...times),\n maxMs: Math.max(...times),\n iterations,\n };\n}\n\nasync function main() {\n const results: PerfResult[] = [];\n\n // 测试 1: 简单查询\n results.push(await benchmark('简单查询 (findMany)', async () => {\n await prisma.leads.findMany({\n where: { tenant_id: TENANT_ID },\n take: 20,\n });\n }));\n\n // 测试 2: 带筛选查询\n results.push(await benchmark('带筛选查询', async () => {\n await prisma.leads.findMany({\n where: {\n tenant_id: TENANT_ID,\n status: 'new',\n },\n take: 20,\n });\n }));\n\n // 测试 3: 计数查询\n results.push(await benchmark('计数查询', async () => {\n await prisma.leads.count({\n where: { tenant_id: TENANT_ID },\n });\n }));\n\n // 测试 4: 分页查询\n results.push(await benchmark('分页查询 (第50页)', async () => {\n await prisma.leads.findMany({\n where: { tenant_id: TENANT_ID },\n skip: 1000,\n take: 20,\n });\n }));\n\n // 输出结果\n console.log('\\n\U0001F4CA 数据库性能测试结果\\n');\n console.log('| 测试项 | 平均耗时 | 最小耗时 | 最大耗时 | 迭代次数 |');\n console.log('|--------|----------|----------|----------|----------|');\n\n for (const r of results) {\n console.log(`| ${r.name} | ${r.avgMs.toFixed(2)}ms | ${r.minMs.toFixed(2)}ms | ${r.maxMs.toFixed(2)}ms | ${r.iterations} |`);\n }\n\n // 写入 JSON 结果\n const fs = await import('fs');\n fs.writeFileSync('db-perf-results.json', JSON.stringify(results, null, 2));\n}\n\nmain()\n .catch(console.error)\n .finally(() => prisma.$disconnect());\nEOF\n\nnpx tsx db-perf-test.ts | tee db-perf-output.txt\n"
env:
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_db_perf
- name: "\U0001F4DD 生成数据库性能报告"
run: "echo \"## \U0001F5C4️ 数据库性能测试报告\" >> $GITHUB_STEP_SUMMARY\necho \"\" >> $GITHUB_STEP_SUMMARY\n\nif [ -f \"backend/db-perf-output.txt\" ]; then\n cat backend/db-perf-output.txt >> $GITHUB_STEP_SUMMARY\nfi\n"
- name: "\U0001F4E4 上传数据库性能结果"
uses: actions/upload-artifact@v4
with:
name: db-performance-results
path: |
backend/db-perf-results.json
backend/db-perf-output.txt
retention-days: "30"
timeout-minutes: "30"
services:
postgres:
image: postgres:15
env:
POSTGRES_DB: juhi_db_perf
POSTGRES_PASSWORD: test
POSTGRES_USER: test
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
...
|
db-performance
|
["setup"]
|
["ubuntu-latest"]
|
13754
|
2
|
1775441259
|
1775441437
|
1775440818
|
1775441437
|
|
1
|
|
0
|
Edit
Delete
|
|
16461
|
11697
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
🧠 内存泄漏检测
|
1
|
name: Performance Tests
"on":
# 每周一凌晨 name: Performance Tests
"on":
# 每周一凌晨 2 点运行
schedule:
- cron: '0 2 * * 1'
# 允许手动触发
workflow_dispatch:
inputs:
test_type:
description: '测试类型'
required: true
default: 'benchmark'
type: choice
options:
- benchmark
- load
- stress
- all
duration:
description: '测试持续时间(秒)'
required: false
default: '60'
type: string
concurrency:
description: '并发数'
required: false
default: '10'
type: string
# PR 触发时只运行基准测试
pull_request:
branches: [main]
paths:
- 'backend/src/**'
- 'backend/prisma/**'
env:
NODE_VERSION: "18"
PNPM_VERSION: "8"
jobs:
memory-leak-detection:
name: "\U0001F9E0 内存泄漏检测"
runs-on: ubuntu-latest
if: needs.setup.outputs.test_type == 'all'
steps:
- name: "\U0001F4E5 检出代码"
uses: actions/checkout@v4
- name: "\U0001F4E6 安装 pnpm"
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: "\U0001F7E2 设置 Node.js"
uses: actions/setup-node@v4
with:
cache: pnpm
node-version: ${{ env.NODE_VERSION }}
- name: "\U0001F4E6 安装依赖"
run: pnpm install --frozen-lockfile
- name: "\U0001F5C4️ 初始化数据库"
run: |
cd backend
npx prisma migrate deploy
npx prisma db seed
env:
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_mem
- name: "\U0001F528 构建后端"
run: |
pnpm --filter shared build
pnpm --filter backend build
- name: "\U0001F9E0 运行内存泄漏检测"
run: "cd backend\n# 使用 --expose-gc 启动服务并检测内存\nnode --expose-gc -e \"\nconst http = require('http');\n\nasync function measureMemory() {\n if (global.gc) global.gc();\n const used = process.memoryUsage();\n return {\n heapUsed: Math.round(used.heapUsed / 1024 / 1024),\n heapTotal: Math.round(used.heapTotal / 1024 / 1024),\n rss: Math.round(used.rss / 1024 / 1024),\n };\n}\n\nasync function main() {\n console.log('\U0001F9E0 内存泄漏检测开始');\n\n const before = await measureMemory();\n console.log('初始内存:', before);\n\n // 模拟 1000 次请求\n for (let i = 0; i < 1000; i++) {\n // 模拟内存分配\n const arr = new Array(10000).fill(Math.random());\n if (i % 100 === 0) {\n if (global.gc) global.gc();\n console.log('进度:', i);\n }\n }\n\n if (global.gc) global.gc();\n await new Promise(r => setTimeout(r, 1000));\n if (global.gc) global.gc();\n\n const after = await measureMemory();\n console.log('最终内存:', after);\n\n const diff = after.heapUsed - before.heapUsed;\n console.log('内存增长:', diff, 'MB');\n\n if (diff > 50) {\n console.log('⚠️ 警告: 可能存在内存泄漏');\n process.exit(1);\n } else {\n console.log('✅ 内存使用正常');\n }\n}\n\nmain().catch(err => {\n console.error(err);\n process.exit(1);\n});\n\" | tee memory-leak-output.txt\n"
env:
NODE_ENV: test
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_mem
REDIS_URL: redis://localhost:6379
- name: "\U0001F4DD 生成内存检测报告"
run: "echo \"## \U0001F9E0 内存泄漏检测报告\" >> $GITHUB_STEP_SUMMARY\necho \"\" >> $GITHUB_STEP_SUMMARY\n\nif [ -f \"backend/memory-leak-output.txt\" ]; then\n echo '```' >> $GITHUB_STEP_SUMMARY\n cat backend/memory-leak-output.txt >> $GITHUB_STEP_SUMMARY\n echo '```' >> $GITHUB_STEP_SUMMARY\nfi\n"
timeout-minutes: "30"
services:
postgres:
image: postgres:15
env:
POSTGRES_DB: juhi_mem
POSTGRES_PASSWORD: test
POSTGRES_USER: test
ports:
- 5432:5432
redis:
image: redis:7
ports:
- 6379:6379
...
|
memory-leak-detection
|
["setup"]
|
["ubuntu-latest"]
|
13755
|
2
|
1775441437
|
1775441602
|
1775440818
|
1775441603
|
|
1
|
|
0
|
Edit
Delete
|
|
16462
|
11697
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
📋 性能测试汇总
|
1
|
name: Performance Tests
"on":
# 每周一凌晨 name: Performance Tests
"on":
# 每周一凌晨 2 点运行
schedule:
- cron: '0 2 * * 1'
# 允许手动触发
workflow_dispatch:
inputs:
test_type:
description: '测试类型'
required: true
default: 'benchmark'
type: choice
options:
- benchmark
- load
- stress
- all
duration:
description: '测试持续时间(秒)'
required: false
default: '60'
type: string
concurrency:
description: '并发数'
required: false
default: '10'
type: string
# PR 触发时只运行基准测试
pull_request:
branches: [main]
paths:
- 'backend/src/**'
- 'backend/prisma/**'
env:
NODE_VERSION: "18"
PNPM_VERSION: "8"
jobs:
performance-summary:
name: "\U0001F4CB 性能测试汇总"
runs-on: ubuntu-latest
if: always()
steps:
- name: "\U0001F4E5 下载所有结果"
uses: actions/download-artifact@v4
with:
path: all-results
continue-on-error: true
- name: "\U0001F4DD 生成汇总报告"
run: "echo \"## \U0001F4CA 性能测试汇总报告\" >> $GITHUB_STEP_SUMMARY\necho \"\" >> $GITHUB_STEP_SUMMARY\necho \"测试时间: $(date)\" >> $GITHUB_STEP_SUMMARY\necho \"\" >> $GITHUB_STEP_SUMMARY\n\necho \"| 测试类型 | 状态 |\" >> $GITHUB_STEP_SUMMARY\necho \"|----------|------|\" >> $GITHUB_STEP_SUMMARY\n\nif [ \"${{ needs.benchmark.result }}\" == \"success\" ]; then\n echo \"| \U0001F4CA 基准测试 | ✅ 通过 |\" >> $GITHUB_STEP_SUMMARY\nelif [ \"${{ needs.benchmark.result }}\" == \"skipped\" ]; then\n echo \"| \U0001F4CA 基准测试 | ⏭️ 跳过 |\" >> $GITHUB_STEP_SUMMARY\nelse\n echo \"| \U0001F4CA 基准测试 | ❌ 失败 |\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ \"${{ needs.load-test.result }}\" == \"success\" ]; then\n echo \"| \U0001F525 负载测试 | ✅ 通过 |\" >> $GITHUB_STEP_SUMMARY\nelif [ \"${{ needs.load-test.result }}\" == \"skipped\" ]; then\n echo \"| \U0001F525 负载测试 | ⏭️ 跳过 |\" >> $GITHUB_STEP_SUMMARY\nelse\n echo \"| \U0001F525 负载测试 | ❌ 失败 |\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ \"${{ needs.stress-test.result }}\" == \"success\" ]; then\n echo \"| \U0001F4A5 压力测试 | ✅ 通过 |\" >> $GITHUB_STEP_SUMMARY\nelif [ \"${{ needs.stress-test.result }}\" == \"skipped\" ]; then\n echo \"| \U0001F4A5 压力测试 | ⏭️ 跳过 |\" >> $GITHUB_STEP_SUMMARY\nelse\n echo \"| \U0001F4A5 压力测试 | ❌ 失败 |\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ \"${{ needs.db-performance.result }}\" == \"success\" ]; then\n echo \"| \U0001F5C4️ 数据库性能 | ✅ 通过 |\" >> $GITHUB_STEP_SUMMARY\nelif [ \"${{ needs.db-performance.result }}\" == \"skipped\" ]; then\n echo \"| \U0001F5C4️ 数据库性能 | ⏭️ 跳过 |\" >> $GITHUB_STEP_SUMMARY\nelse\n echo \"| \U0001F5C4️ 数据库性能 | ❌ 失败 |\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ \"${{ needs.memory-leak-detection.result }}\" == \"success\" ]; then\n echo \"| \U0001F9E0 内存泄漏检测 | ✅ 通过 |\" >> $GITHUB_STEP_SUMMARY\nelif [ \"${{ needs.memory-leak-detection.result }}\" == \"skipped\" ]; then\n echo \"| \U0001F9E0 内存泄漏检测 | ⏭️ 跳过 |\" >> $GITHUB_STEP_SUMMARY\nelse\n echo \"| \U0001F9E0 内存泄漏检测 | ❌ 失败 |\" >> $GITHUB_STEP_SUMMARY\nfi\n"
...
|
performance-summary
|
["benchmark","load-test","stre ["benchmark","load-test","stress-test","db-performance","memory-leak-detection"]...
|
["ubuntu-latest"]
|
13758
|
1
|
1775441607
|
1775441610
|
1775440818
|
1775441610
|
|
1
|
|
0
|
Edit
Delete
|
|
16736
|
11964
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
test-summary
|
1
|
name: E2E Tests
"on":
# PR validation name: E2E Tests
"on":
# PR validation - 运行关键测试
pull_request:
branches: [main, develop]
paths:
- 'frontend/**'
- 'backend/**'
- 'e2e/**'
- 'package.json'
- 'pnpm-lock.yaml'
# Push to main - 运行完整测试套件
push:
branches: [main]
# 每日定时全量测试 (UTC 时间 00:00 = 北京时间 08:00)
schedule:
- cron: '0 0 * * *'
# 手动触发
workflow_dispatch:
inputs:
test_suite:
description: 'Test suite to run'
required: true
default: 'all'
type: choice
options:
- all
- critical
- business-flows
- visual-regression
- performance
env:
NODE_VERSION: "18"
PNPM_VERSION: "8"
jobs:
test-summary:
name: test-summary
runs-on: ubuntu-latest
if: always()
steps:
- name: Download all test artifacts
uses: actions/download-artifact@v4
- name: Generate summary report
run: "echo \"## \U0001F9EA E2E 测试汇总\" >> $GITHUB_STEP_SUMMARY\necho \"\" >> $GITHUB_STEP_SUMMARY\n\nif [ -d \"playwright-report-pr\" ]; then\n echo \"### PR 快速验证\" >> $GITHUB_STEP_SUMMARY\n echo \"✅ 关键测试通过\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"business-flows-report\" ]; then\n echo \"### 业务流程测试\" >> $GITHUB_STEP_SUMMARY\n echo \"✅ 业务流程测试完成\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"performance-report\" ]; then\n echo \"### 性能基准测试\" >> $GITHUB_STEP_SUMMARY\n echo \"\U0001F4CA 性能测试报告已生成\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"visual-regression-report\" ]; then\n echo \"### 视觉回归测试\" >> $GITHUB_STEP_SUMMARY\n echo \"\U0001F3A8 视觉对比完成\" >> $GITHUB_STEP_SUMMARY\nfi\n"
...
|
test-summary
|
["pr-validation","full-test-suite" ["pr-validation","full-test-suite","business-flows","performance-benchmarks","visual-regression"]...
|
["ubuntu-latest"]
|
14034
|
2
|
1775521564
|
1775521566
|
1775520018
|
1775521567
|
|
1
|
|
0
|
Edit
Delete
|
|
17032
|
12253
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
test-summary
|
1
|
name: E2E Tests
"on":
# PR validation name: E2E Tests
"on":
# PR validation - 运行关键测试
pull_request:
branches: [main, develop]
paths:
- 'frontend/**'
- 'backend/**'
- 'e2e/**'
- 'package.json'
- 'pnpm-lock.yaml'
# Push to main - 运行完整测试套件
push:
branches: [main]
# 每日定时全量测试 (UTC 时间 00:00 = 北京时间 08:00)
schedule:
- cron: '0 0 * * *'
# 手动触发
workflow_dispatch:
inputs:
test_suite:
description: 'Test suite to run'
required: true
default: 'all'
type: choice
options:
- all
- critical
- business-flows
- visual-regression
- performance
env:
NODE_VERSION: "18"
PNPM_VERSION: "8"
jobs:
test-summary:
name: test-summary
runs-on: ubuntu-latest
if: always()
steps:
- name: Download all test artifacts
uses: actions/download-artifact@v4
- name: Generate summary report
run: "echo \"## \U0001F9EA E2E 测试汇总\" >> $GITHUB_STEP_SUMMARY\necho \"\" >> $GITHUB_STEP_SUMMARY\n\nif [ -d \"playwright-report-pr\" ]; then\n echo \"### PR 快速验证\" >> $GITHUB_STEP_SUMMARY\n echo \"✅ 关键测试通过\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"business-flows-report\" ]; then\n echo \"### 业务流程测试\" >> $GITHUB_STEP_SUMMARY\n echo \"✅ 业务流程测试完成\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"performance-report\" ]; then\n echo \"### 性能基准测试\" >> $GITHUB_STEP_SUMMARY\n echo \"\U0001F4CA 性能测试报告已生成\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"visual-regression-report\" ]; then\n echo \"### 视觉回归测试\" >> $GITHUB_STEP_SUMMARY\n echo \"\U0001F3A8 视觉对比完成\" >> $GITHUB_STEP_SUMMARY\nfi\n"
...
|
test-summary
|
["pr-validation","full-test-suite" ["pr-validation","full-test-suite","business-flows","performance-benchmarks","visual-regression"]...
|
["ubuntu-latest"]
|
14327
|
2
|
1775607042
|
1775607132
|
1775606418
|
1775607132
|
|
1
|
|
0
|
Edit
Delete
|
|
17328
|
12542
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
test-summary
|
1
|
name: E2E Tests
"on":
# PR validation name: E2E Tests
"on":
# PR validation - 运行关键测试
pull_request:
branches: [main, develop]
paths:
- 'frontend/**'
- 'backend/**'
- 'e2e/**'
- 'package.json'
- 'pnpm-lock.yaml'
# Push to main - 运行完整测试套件
push:
branches: [main]
# 每日定时全量测试 (UTC 时间 00:00 = 北京时间 08:00)
schedule:
- cron: '0 0 * * *'
# 手动触发
workflow_dispatch:
inputs:
test_suite:
description: 'Test suite to run'
required: true
default: 'all'
type: choice
options:
- all
- critical
- business-flows
- visual-regression
- performance
env:
NODE_VERSION: "18"
PNPM_VERSION: "8"
jobs:
test-summary:
name: test-summary
runs-on: ubuntu-latest
if: always()
steps:
- name: Download all test artifacts
uses: actions/download-artifact@v4
- name: Generate summary report
run: "echo \"## \U0001F9EA E2E 测试汇总\" >> $GITHUB_STEP_SUMMARY\necho \"\" >> $GITHUB_STEP_SUMMARY\n\nif [ -d \"playwright-report-pr\" ]; then\n echo \"### PR 快速验证\" >> $GITHUB_STEP_SUMMARY\n echo \"✅ 关键测试通过\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"business-flows-report\" ]; then\n echo \"### 业务流程测试\" >> $GITHUB_STEP_SUMMARY\n echo \"✅ 业务流程测试完成\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"performance-report\" ]; then\n echo \"### 性能基准测试\" >> $GITHUB_STEP_SUMMARY\n echo \"\U0001F4CA 性能测试报告已生成\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"visual-regression-report\" ]; then\n echo \"### 视觉回归测试\" >> $GITHUB_STEP_SUMMARY\n echo \"\U0001F3A8 视觉对比完成\" >> $GITHUB_STEP_SUMMARY\nfi\n"
...
|
test-summary
|
["pr-validation","full-test-suite" ["pr-validation","full-test-suite","business-flows","performance-benchmarks","visual-regression"]...
|
["ubuntu-latest"]
|
14624
|
2
|
1775693764
|
1775694169
|
1775692818
|
1775694169
|
|
1
|
|
0
|
Edit
Delete
|
|
17624
|
12831
|
6
|
5
|
9d69e1960ec649a49c0c6f307c0fc197f47ee4c4
|
0
|
test-summary
|
1
|
name: E2E Tests
"on":
# PR validation name: E2E Tests
"on":
# PR validation - 运行关键测试
pull_request:
branches: [main, develop]
paths:
- 'frontend/**'
- 'backend/**'
- 'e2e/**'
- 'package.json'
- 'pnpm-lock.yaml'
# Push to main - 运行完整测试套件
push:
branches: [main]
# 每日定时全量测试 (UTC 时间 00:00 = 北京时间 08:00)
schedule:
- cron: '0 0 * * *'
# 手动触发
workflow_dispatch:
inputs:
test_suite:
description: 'Test suite to run'
required: true
default: 'all'
type: choice
options:
- all
- critical
- business-flows
- visual-regression
- performance
env:
NODE_VERSION: "18"
PNPM_VERSION: "8"
jobs:
test-summary:
name: test-summary
runs-on: ubuntu-latest
if: always()
steps:
- name: Download all test artifacts
uses: actions/download-artifact@v4
- name: Generate summary report
run: "echo \"## \U0001F9EA E2E 测试汇总\" >> $GITHUB_STEP_SUMMARY\necho \"\" >> $GITHUB_STEP_SUMMARY\n\nif [ -d \"playwright-report-pr\" ]; then\n echo \"### PR 快速验证\" >> $GITHUB_STEP_SUMMARY\n echo \"✅ 关键测试通过\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"business-flows-report\" ]; then\n echo \"### 业务流程测试\" >> $GITHUB_STEP_SUMMARY\n echo \"✅ 业务流程测试完成\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"performance-report\" ]; then\n echo \"### 性能基准测试\" >> $GITHUB_STEP_SUMMARY\n echo \"\U0001F4CA 性能测试报告已生成\" >> $GITHUB_STEP_SUMMARY\nfi\n\nif [ -d \"visual-regression-report\" ]; then\n echo \"### 视觉回归测试\" >> $GITHUB_STEP_SUMMARY\n echo \"\U0001F3A8 视觉对比完成\" >> $GITHUB_STEP_SUMMARY\nfi\n"
...
|
test-summary
|
["pr-validation","full-test-suite" ["pr-validation","full-test-suite","business-flows","performance-benchmarks","visual-regression"]...
|
["ubuntu-latest"]
|
14918
|
2
|
1775779792
|
1775779797
|
1775779218
|
1775779797
|
|
1
|
|
0
|
Edit
Delete
|