feat(capture): resize + redraw-settle before screenshot #231
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # cli-box - CI门禁 | |
| # 在PR创建/更新及push到main时触发 | |
| # | |
| # 门禁检查项 (Rust): | |
| # 1. Rust 格式化检查 - cargo fmt | |
| # 2. Rust Clippy - 代码规范 + 编译检查 (macOS, 需要系统框架) | |
| # 3. Rust 单元测试 - cargo test (macOS) | |
| # 4. 安全检查 - 硬编码密钥检测 + 依赖漏洞扫描 | |
| # | |
| # @see CLAUDE.md 七、核心工作流程 | |
| name: CI Gate | |
| on: | |
| push: | |
| branches: [ main ] | |
| pull_request: | |
| branches: [ main ] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| checks: write | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| RUST_VERSION: '1.91' | |
| jobs: | |
| # ==================== Rust 格式化检查 ==================== | |
| rust-fmt: | |
| name: Rust 格式化检查 | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: 检出代码 | |
| uses: actions/checkout@v4 | |
| - name: 安装 Rust ${{ env.RUST_VERSION }} | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: ${{ env.RUST_VERSION }} | |
| components: rustfmt | |
| - name: 运行 cargo fmt | |
| run: cargo fmt --all -- --check | |
| # ==================== Rust Clippy (macOS) ==================== | |
| rust-clippy: | |
| name: Rust Clippy | |
| runs-on: macos-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: 检出代码 | |
| uses: actions/checkout@v4 | |
| - name: 安装 Rust ${{ env.RUST_VERSION }} | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: ${{ env.RUST_VERSION }} | |
| components: clippy | |
| - name: 设置 Rust 缓存 | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| cache-on-failure: true | |
| key: "v2-clippy" | |
| - name: 运行 cargo clippy | |
| run: cargo clippy -p cli-box-core -p cli-box-cli --all-targets -- -D warnings | |
| # ==================== Rust 单元测试 (macOS) ==================== | |
| rust-test: | |
| name: Rust 单元测试 | |
| runs-on: macos-latest | |
| timeout-minutes: 15 | |
| env: | |
| DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer | |
| steps: | |
| - name: 检出代码 | |
| uses: actions/checkout@v4 | |
| - name: 设置 Xcode | |
| run: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer | |
| - name: 安装 Rust ${{ env.RUST_VERSION }} | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: ${{ env.RUST_VERSION }} | |
| - name: 设置 Rust 缓存 | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| cache-on-failure: true | |
| key: "v2-test" | |
| - name: 清理 Rust 缓存 | |
| run: cargo clean 2>/dev/null || true | |
| - name: 运行 cargo test | |
| run: cargo test -p cli-box-core | |
| - name: 安装 cargo-llvm-cov | |
| run: | | |
| rustup component add llvm-tools-preview | |
| cargo install cargo-llvm-cov --locked | |
| - name: 运行测试覆盖率 | |
| run: | | |
| mkdir -p coverage | |
| cargo llvm-cov -p cli-box-core --cobertura --output-path coverage/cobertura.xml | |
| - name: 生成覆盖率摘要 | |
| if: always() | |
| run: | | |
| SUMMARY_FILE="rust-coverage-summary.md" | |
| echo "## Rust 测试覆盖率" > "$SUMMARY_FILE" | |
| echo "" >> "$SUMMARY_FILE" | |
| if [ -f coverage/cobertura.xml ]; then | |
| python3 -c " | |
| import xml.etree.ElementTree as ET | |
| tree = ET.parse('coverage/cobertura.xml') | |
| root = tree.getroot() | |
| rate = float(root.attrib.get('line-rate', 0)) * 100 | |
| branch_rate = float(root.attrib.get('branch-rate', 0)) * 100 | |
| print(f'| 指标 | 覆盖率 |') | |
| print(f'|------|--------|') | |
| print(f'| 行覆盖率 | {rate:.1f}% |') | |
| print(f'| 分支覆盖率 | {branch_rate:.1f}% |') | |
| print() | |
| print('| 模块 | 行覆盖率 |') | |
| print('|------|----------|') | |
| for pkg in root.findall('.//package'): | |
| name = pkg.attrib.get('name', 'unknown') | |
| pkg_rate = float(pkg.attrib.get('line-rate', 0)) * 100 | |
| bar_len = int(pkg_rate / 5) | |
| bar = '█' * bar_len + '░' * (20 - bar_len) | |
| print(f'| {name} | {bar} {pkg_rate:.1f}% |') | |
| " >> "$SUMMARY_FILE" | |
| echo "" >> "$SUMMARY_FILE" | |
| echo "> 详细报告见 Rust 覆盖率 artifact" >> "$SUMMARY_FILE" | |
| else | |
| echo "> ⚠️ 未生成覆盖率报告" >> "$SUMMARY_FILE" | |
| echo "❌ 覆盖率生成失败:cobertura.xml 未生成" >> "$SUMMARY_FILE" | |
| echo "::error::覆盖率报告生成失败,检查 llvm-cov 输出的测试失败信息" | |
| exit 1 | |
| fi | |
| echo "" >> "$SUMMARY_FILE" | |
| cat "$SUMMARY_FILE" >> "$GITHUB_STEP_SUMMARY" | |
| - name: 上传覆盖率摘要 | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: rust-coverage-summary | |
| path: rust-coverage-summary.md | |
| retention-days: 1 | |
| - name: 上传覆盖率报告 | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: rust-coverage | |
| path: coverage/ | |
| retention-days: 14 | |
| # ==================== 前端测试 + 类型检查 + 覆盖率 ==================== | |
| frontend-test: | |
| name: 前端测试 & 覆盖率 | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: 检出代码 | |
| uses: actions/checkout@v4 | |
| - name: 安装 Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| - name: 安装 pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: "10" | |
| - name: 安装依赖 | |
| working-directory: electron-app | |
| run: pnpm install --frozen-lockfile | |
| - name: TypeScript 类型检查 | |
| working-directory: electron-app | |
| run: pnpm typecheck | |
| - name: 运行前端测试 + 覆盖率 | |
| working-directory: electron-app | |
| run: pnpm vitest run --coverage --coverage.reporter=json-summary --coverage.reporter=text | |
| - name: 生成前端覆盖率摘要 | |
| if: always() | |
| working-directory: electron-app | |
| run: | | |
| SUMMARY_FILE="frontend-coverage-summary.md" | |
| echo "## 前端测试覆盖率" > "$SUMMARY_FILE" | |
| echo "" >> "$SUMMARY_FILE" | |
| if [ -f coverage/coverage-summary.json ]; then | |
| node -e " | |
| const fs = require('fs'); | |
| const data = JSON.parse(fs.readFileSync('coverage/coverage-summary.json', 'utf8')); | |
| const total = data.total; | |
| if (!total || total.lines === undefined) { | |
| console.error('No total coverage data found'); | |
| process.exit(1); | |
| } | |
| const lines = total.lines.pct.toFixed(1); | |
| const branches = total.branches.pct.toFixed(1); | |
| const functions = total.functions.pct.toFixed(1); | |
| const statements = total.statements.pct.toFixed(1); | |
| const barLen = Math.round(total.lines.pct / 5); | |
| const bar = '█'.repeat(barLen) + '░'.repeat(20 - barLen); | |
| console.log('| 指标 | 覆盖率 |'); | |
| console.log('|------|--------|'); | |
| console.log('| 行覆盖率 | ' + bar + ' ' + lines + '% |'); | |
| console.log('| 分支覆盖率 | ' + branches + '% |'); | |
| console.log('| 函数覆盖率 | ' + functions + '% |'); | |
| console.log('| 语句覆盖率 | ' + statements + '% |'); | |
| console.log(); | |
| // Print per-file breakdown | |
| console.log('| 文件 | 行覆盖率 |'); | |
| console.log('|------|----------|'); | |
| for (const [file, fileData] of Object.entries(data)) { | |
| if (file === 'total') continue; | |
| const fileLines = fileData.lines.pct.toFixed(1); | |
| const fileBarLen = Math.round(fileData.lines.pct / 5); | |
| const fileBar = '█'.repeat(fileBarLen) + '░'.repeat(20 - fileBarLen); | |
| const shortPath = file.replace(/^.*src\//, 'src/'); | |
| console.log('| ' + shortPath + ' | ' + fileBar + ' ' + fileLines + '% |'); | |
| } | |
| " >> "$SUMMARY_FILE" | |
| echo "" >> "$SUMMARY_FILE" | |
| echo "> 详细报告见前端覆盖率 artifact" >> "$SUMMARY_FILE" | |
| cat "$SUMMARY_FILE" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "> ⚠️ 未生成前端覆盖率报告" >> "$SUMMARY_FILE" | |
| echo "❌ 前端覆盖率生成失败:coverage-summary.json 未生成" >> "$SUMMARY_FILE" | |
| echo "::error::前端覆盖率报告生成失败,检查 vitest 输出的测试失败信息" | |
| cat "$SUMMARY_FILE" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| - name: 上传前端覆盖率摘要 | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: frontend-coverage-summary | |
| path: electron-app/frontend-coverage-summary.md | |
| retention-days: 1 | |
| - name: 上传前端覆盖率报告 | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: frontend-coverage | |
| path: electron-app/coverage/ | |
| retention-days: 14 | |
| # ==================== Playwright E2E 测试 ==================== | |
| e2e-test: | |
| name: Playwright E2E | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: 检出代码 | |
| uses: actions/checkout@v4 | |
| - name: 安装 Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| - name: 安装 pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: "10" | |
| - name: 安装依赖 | |
| working-directory: electron-app | |
| run: pnpm install --frozen-lockfile | |
| - name: 安装 Playwright 浏览器 | |
| working-directory: electron-app | |
| run: npx playwright install chromium --with-deps | |
| - name: 运行 Playwright E2E 测试 | |
| working-directory: electron-app | |
| run: npx playwright test --config e2e/playwright.config.ts --update-snapshots missing | |
| - name: 上传测试报告 | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: playwright-report | |
| path: electron-app/playwright-report/ | |
| retention-days: 7 | |
| - name: 上传测试截图 | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: playwright-screenshots | |
| path: electron-app/test-results/ | |
| retention-days: 7 | |
| # ==================== 安全检查 ==================== | |
| security: | |
| name: 安全检查 | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: 检出代码 | |
| uses: actions/checkout@v4 | |
| - name: 安装 Rust 工具链 | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: ${{ env.RUST_VERSION }} | |
| - name: 安装系统依赖 | |
| run: sudo apt-get update && sudo apt-get install -y cmake clang | |
| - name: Rust 依赖审计 | |
| if: always() | |
| continue-on-error: false | |
| run: | | |
| if [ -f Cargo.lock ]; then | |
| cargo install cargo-audit --locked | |
| cargo audit || echo "::warning::Rust 依赖存在漏洞" | |
| else | |
| echo "::notice::未找到 Cargo.lock,跳过 Rust 依赖审计" | |
| fi | |
| - name: 检查硬编码密钥 | |
| run: | | |
| echo "检查硬编码密钥..." | |
| if grep -rE "(api[_-]?key|apikey|secret|password|token)\s*[=:]\s*['\"][^'\"]{10,}['\"]" \ | |
| --include="*.rs" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.json" \ | |
| --exclude-dir="node_modules" --exclude-dir="target" --exclude-dir="dist" \ | |
| --exclude-dir="coverage" --exclude-dir="tests" --exclude-dir="__tests__" \ | |
| --exclude="*.test.*" --exclude="*.spec.*" --exclude="*_test.*" --exclude="test_*" \ | |
| .; then | |
| echo "::error::发现硬编码的密钥,请使用环境变量或配置文件加载" | |
| exit 1 | |
| fi | |
| echo "::notice::未发现硬编码密钥" | |
| - name: 检查敏感文件 | |
| run: | | |
| echo "检查敏感文件..." | |
| SENSITIVE_FILES=( | |
| ".env" | |
| ".env.local" | |
| ".env.production" | |
| "*.pem" | |
| "*.key" | |
| "secrets.json" | |
| "credentials.json" | |
| ) | |
| for file in "${SENSITIVE_FILES[@]}"; do | |
| if ls $file 2>/dev/null | grep -q .; then | |
| echo "::error::发现敏感文件: $file,请添加到.gitignore" | |
| exit 1 | |
| fi | |
| done | |
| echo "::notice::未发现敏感文件" | |
| # ==================== 统一测试脚本 (test.sh) ==================== | |
| unified-test: | |
| name: 统一测试 (test.sh) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: 检出代码 | |
| uses: actions/checkout@v4 | |
| - name: 安装 Rust ${{ env.RUST_VERSION }} | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: ${{ env.RUST_VERSION }} | |
| - name: 安装 Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| - name: 安装 pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: "10" | |
| - name: 安装前端依赖 | |
| working-directory: electron-app | |
| run: pnpm install --frozen-lockfile | |
| - name: 安装 Playwright 浏览器 | |
| working-directory: electron-app | |
| run: npx playwright install chromium --with-deps | |
| - name: 运行 test.sh | |
| run: bash test.sh | |
| # ==================== 发布模拟验证 ==================== | |
| publish-sim: | |
| name: 发布模拟验证 | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: 检出代码 | |
| uses: actions/checkout@v4 | |
| - name: 安装 Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| - name: npm pack | |
| working-directory: packages/cli-box-skill | |
| run: | | |
| npm pack | |
| ls -la cli-box-skill-*.tgz | |
| - name: 验证 tarball 文件清单 | |
| working-directory: packages/cli-box-skill | |
| run: | | |
| TARBALL=$(ls cli-box-skill-*.tgz) | |
| echo "=== 验证 $TARBALL ===" | |
| tar tzf "$TARBALL" | sort | |
| REQUIRED=("package/postinstall.mjs" "package/installer/cli.mjs" "package/installer/shared.mjs" "package/bin/cli-box-wrapper.js" "package/skill/SKILL.md") | |
| for f in "${REQUIRED[@]}"; do | |
| if tar tzf "$TARBALL" | grep -q "$f"; then | |
| echo "✓ $f" | |
| else | |
| echo "✗ $f MISSING" | |
| exit 1 | |
| fi | |
| done | |
| echo "" | |
| echo "✅ tarball 文件清单验证通过" | |
| - name: 验证包结构 | |
| working-directory: packages/cli-box-skill | |
| run: | | |
| echo "=== package.json 字段检查 ===" | |
| node -e " | |
| const pkg = require('./package.json'); | |
| const checks = [ | |
| ['name', pkg.name === 'cli-box-skill'], | |
| ['bin.cli-box', !!pkg.bin?.['cli-box']], | |
| ['bin.cli-box-skill', !!pkg.bin?.['cli-box-skill']], | |
| ['files includes postinstall.mjs', pkg.files?.includes('postinstall.mjs')], | |
| ['files includes installer/', pkg.files?.some(f => f.startsWith('installer/'))], | |
| ['optionalDependencies.cli-box-darwin-arm64', !!pkg.optionalDependencies?.['cli-box-darwin-arm64']], | |
| ]; | |
| let ok = true; | |
| for (const [name, pass] of checks) { | |
| console.log(pass ? '✓ ' + name : '✗ ' + name); | |
| if (!pass) ok = false; | |
| } | |
| if (!ok) process.exit(1); | |
| console.log('\n✅ 包结构验证通过'); | |
| " | |
| # ==================== 升级流程测试 (macOS) ==================== | |
| skill-upgrade-flow: | |
| name: 升级流程测试 | |
| runs-on: macos-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: 检出代码 | |
| uses: actions/checkout@v4 | |
| - name: 安装 Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| - name: 安装当前 latest 版本 | |
| run: npm install -g cli-box-skill@latest | |
| - name: 验证 latest 安装 | |
| run: | | |
| echo "=== 验证 cli-box-skill 命令 ===" | |
| cli-box-skill --help | |
| echo "" | |
| echo "=== 验证二进制链接 ===" | |
| ls -la ~/.cli-box/bin/cli-box || echo "⚠ cli-box binary not found" | |
| ls -la ~/.cli-box/bin/cli-box-daemon || echo "⚠ cli-box-daemon binary not found" | |
| - name: 模拟用户自定义 SKILL.md | |
| run: | | |
| mkdir -p ~/.claude/skills/cli-box | |
| echo "# Custom SKILL.md (user modified)" > ~/.claude/skills/cli-box/SKILL.md | |
| echo "Custom content that should be preserved" >> ~/.claude/skills/cli-box/SKILL.md | |
| echo "" | |
| echo "=== 自定义 SKILL.md 内容 ===" | |
| cat ~/.claude/skills/cli-box/SKILL.md | |
| - name: 从 PR 分支打包 | |
| working-directory: packages/cli-box-skill | |
| run: | | |
| npm pack | |
| ls -la cli-box-skill-*.tgz | |
| - name: 运行 upgrade 命令 | |
| working-directory: packages/cli-box-skill | |
| run: | | |
| echo "=== 运行 upgrade(无 daemon)===" | |
| node installer/cli.mjs upgrade --yes 2>&1 | tee /tmp/upgrade-output.log | |
| - name: 验证升级结果 | |
| run: | | |
| echo "============================================" | |
| echo " 升级结果验证" | |
| echo "============================================" | |
| echo "" | |
| echo "=== 二进制链接检查 ===" | |
| if [ -L ~/.cli-box/bin/cli-box ]; then | |
| echo "✓ cli-box is a symlink" | |
| ls -la ~/.cli-box/bin/cli-box | |
| else | |
| echo "⚠ cli-box is NOT a symlink" | |
| fi | |
| if [ -L ~/.cli-box/bin/cli-box-daemon ]; then | |
| echo "✓ cli-box-daemon is a symlink" | |
| ls -la ~/.cli-box/bin/cli-box-daemon | |
| else | |
| echo "⚠ cli-box-daemon is NOT a symlink" | |
| fi | |
| echo "" | |
| echo "=== SKILL.md 保留检查 ===" | |
| if [ -f ~/.claude/skills/cli-box/SKILL.md ]; then | |
| CONTENT=$(cat ~/.claude/skills/cli-box/SKILL.md) | |
| if echo "$CONTENT" | grep -q "Custom content that should be preserved"; then | |
| echo "✓ SKILL.md preserved (custom content intact)" | |
| else | |
| echo "✗ SKILL.md was overwritten!" | |
| echo "Expected: 'Custom content that should be preserved'" | |
| echo "Got:" | |
| cat ~/.claude/skills/cli-box/SKILL.md | |
| exit 1 | |
| fi | |
| else | |
| echo "⚠ SKILL.md not found at ~/.claude/skills/cli-box/SKILL.md" | |
| fi | |
| echo "" | |
| echo "=== cli-box-skill 命令检查 ===" | |
| cli-box-skill --help 2>&1 | head -5 | |
| - name: 上传升级日志 | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: upgrade-output | |
| path: /tmp/upgrade-output.log | |
| retention-days: 7 | |
| # ==================== cli-box-skill 单元测试 ==================== | |
| skill-unit-test: | |
| name: cli-box-skill 单元测试 | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: 检出代码 | |
| uses: actions/checkout@v4 | |
| - name: 安装 Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| - name: 安装依赖 | |
| working-directory: packages/cli-box-skill | |
| run: npm install | |
| - name: 运行单元测试 | |
| working-directory: packages/cli-box-skill | |
| run: node --test | |
| # ==================== 门禁结果汇总 ==================== | |
| gate-result: | |
| name: 门禁结果 | |
| runs-on: ubuntu-latest | |
| needs: [rust-fmt, rust-clippy, rust-test, frontend-test, e2e-test, unified-test, security, publish-sim, skill-upgrade-flow, skill-unit-test] | |
| if: always() | |
| steps: | |
| - name: 下载 Rust 覆盖率摘要 | |
| uses: actions/download-artifact@v4 | |
| if: always() | |
| with: | |
| name: rust-coverage-summary | |
| path: coverage-artifacts | |
| continue-on-error: true | |
| - name: 下载前端覆盖率摘要 | |
| uses: actions/download-artifact@v4 | |
| if: always() | |
| with: | |
| name: frontend-coverage-summary | |
| path: coverage-artifacts | |
| continue-on-error: true | |
| - name: 检查门禁结果 | |
| run: | | |
| echo "============================================" | |
| echo " cli-box CI 门禁结果" | |
| echo "============================================" | |
| echo "" | |
| RUST_FMT="${{ needs.rust-fmt.result }}" | |
| RUST_CLIPPY="${{ needs.rust-clippy.result }}" | |
| RUST_TEST="${{ needs.rust-test.result }}" | |
| FRONTEND_TEST="${{ needs.frontend-test.result }}" | |
| E2E_TEST="${{ needs.e2e-test.result }}" | |
| UNIFIED_TEST="${{ needs.unified-test.result }}" | |
| SECURITY_RESULT="${{ needs.security.result }}" | |
| PUBLISH_SIM="${{ needs.publish-sim.result }}" | |
| UPGRADE_FLOW="${{ needs.skill-upgrade-flow.result }}" | |
| SKILL_UNIT="${{ needs.skill-unit-test.result }}" | |
| echo "| 检查项 | 状态 |" | |
| echo "|-------|------|" | |
| echo "| Rust 格式化 | $RUST_FMT |" | |
| echo "| Rust Clippy | $RUST_CLIPPY |" | |
| echo "| Rust 测试 & 覆盖率 | $RUST_TEST |" | |
| echo "| 前端测试 & 覆盖率 | $FRONTEND_TEST |" | |
| echo "| Playwright E2E | $E2E_TEST |" | |
| echo "| 统一测试 (test.sh) | $UNIFIED_TEST |" | |
| echo "| 安全检查 | $SECURITY_RESULT |" | |
| echo "| 发布模拟验证 | $PUBLISH_SIM |" | |
| echo "| 升级流程测试 | $UPGRADE_FLOW |" | |
| echo "| cli-box-skill 单元测试 | $SKILL_UNIT |" | |
| echo "" | |
| if [ -f coverage-artifacts/rust-coverage-summary.md ]; then | |
| echo "---" | |
| cat coverage-artifacts/rust-coverage-summary.md | |
| echo "" | |
| fi | |
| if [ -f coverage-artifacts/frontend-coverage-summary.md ]; then | |
| echo "---" | |
| cat coverage-artifacts/frontend-coverage-summary.md | |
| echo "" | |
| fi | |
| if [[ "$RUST_FMT" == "success" && \ | |
| "$RUST_CLIPPY" == "success" && \ | |
| "$RUST_TEST" == "success" && \ | |
| "$FRONTEND_TEST" == "success" && \ | |
| "$E2E_TEST" == "success" && \ | |
| "$UNIFIED_TEST" == "success" && \ | |
| "$SECURITY_RESULT" == "success" && \ | |
| "$PUBLISH_SIM" == "success" && \ | |
| "$UPGRADE_FLOW" == "success" && \ | |
| "$SKILL_UNIT" == "success" ]]; then | |
| echo "✅ 所有门禁检查通过!" | |
| exit 0 | |
| else | |
| echo "❌ 门禁检查未通过,请修复上述问题" | |
| exit 1 | |
| fi | |
| - name: PR 门禁状态评论 | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const results = { | |
| 'Rust 格式化': '${{ needs.rust-fmt.result }}', | |
| 'Rust Clippy': '${{ needs.rust-clippy.result }}', | |
| 'Rust 测试 & 覆盖率': '${{ needs.rust-test.result }}', | |
| '前端测试 & 覆盖率': '${{ needs.frontend-test.result }}', | |
| 'Playwright E2E': '${{ needs.e2e-test.result }}', | |
| '统一测试 (test.sh)': '${{ needs.unified-test.result }}', | |
| '安全检查': '${{ needs.security.result }}', | |
| '发布模拟验证': '${{ needs.publish-sim.result }}', | |
| '升级流程测试': '${{ needs.skill-upgrade-flow.result }}', | |
| 'cli-box-skill 单元测试': '${{ needs.skill-unit-test.result }}', | |
| }; | |
| const statusEmoji = (status) => status === 'success' ? '✅' : (status === 'skipped' ? '⏭️' : '❌'); | |
| let tableRows = Object.entries(results).map(([name, status]) => | |
| `| ${name} | ${statusEmoji(status)} ${status} |` | |
| ).join('\n'); | |
| const allPassed = Object.values(results).every(r => r === 'success'); | |
| let coverageSection = ''; | |
| try { | |
| const rustCov = fs.readFileSync('coverage-artifacts/rust-coverage-summary.md', 'utf8'); | |
| coverageSection += '\n' + rustCov + '\n'; | |
| } catch (e) {} | |
| try { | |
| const feCov = fs.readFileSync('coverage-artifacts/frontend-coverage-summary.md', 'utf8'); | |
| coverageSection += '\n' + feCov + '\n'; | |
| } catch (e) {} | |
| const body = `## 🔒 门禁检查结果 | |
| | 检查项 | 状态 | | |
| |-------|------| | |
| ${tableRows} | |
| ${coverageSection} | |
| ${allPassed | |
| ? '### ✅ 所有检查通过,可以合入\n\n> 点击 **Squash and merge** 合并此PR' | |
| : '### ❌ 存在未通过的检查,请修复后重新提交'} | |
| `; | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const botComment = comments.find(comment => | |
| comment.user.type === 'Bot' && | |
| comment.body.includes('🔒 门禁检查结果') | |
| ); | |
| if (botComment) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: botComment.id, | |
| body: body | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: body | |
| }); | |
| } | |
| # ==================== 发布构建验证 ==================== | |
| release-build: | |
| name: 发布构建验证 | |
| runs-on: macos-latest | |
| needs: [gate-result] | |
| if: needs.gate-result.result == 'success' | |
| timeout-minutes: 30 | |
| steps: | |
| - name: 检出代码 | |
| uses: actions/checkout@v4 | |
| - name: 安装 Rust ${{ env.RUST_VERSION }} | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: ${{ env.RUST_VERSION }} | |
| - name: 设置 Rust 缓存 | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| cache-on-failure: true | |
| - name: 构建 CLI 二进制 (release) | |
| run: cargo build -p cli-box-cli --release | |
| - name: 整理构建产物 | |
| run: | | |
| mkdir -p release | |
| cp target/release/cli-box release/ | |
| echo "## 构建产物" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| ls -lh release/ | tail -n +2 | awk '{printf "| %s | %s |\n", $NF, $5}' >> "$GITHUB_STEP_SUMMARY" | |
| - name: 上传构建产物 | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: release-artifacts | |
| path: release/ | |
| retention-days: 7 |