feat(rebuild): simplify architecture with native Terminal sandbox #45
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
| # system-test-sandbox - 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 sandbox-core -p sandbox-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 sandbox-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 sandbox-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 | |
| # ==================== 安全检查 ==================== | |
| 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::未发现敏感文件" | |
| # ==================== 门禁结果汇总 ==================== | |
| gate-result: | |
| name: 门禁结果 | |
| runs-on: ubuntu-latest | |
| needs: [rust-fmt, rust-clippy, rust-test, security] | |
| 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: 检查门禁结果 | |
| run: | | |
| echo "============================================" | |
| echo " system-test-sandbox CI 门禁结果" | |
| echo "============================================" | |
| echo "" | |
| RUST_FMT="${{ needs.rust-fmt.result }}" | |
| RUST_CLIPPY="${{ needs.rust-clippy.result }}" | |
| RUST_TEST="${{ needs.rust-test.result }}" | |
| SECURITY_RESULT="${{ needs.security.result }}" | |
| echo "| 检查项 | 状态 |" | |
| echo "|-------|------|" | |
| echo "| Rust 格式化 | $RUST_FMT |" | |
| echo "| Rust Clippy | $RUST_CLIPPY |" | |
| echo "| Rust 测试 | $RUST_TEST |" | |
| echo "| 安全检查 | $SECURITY_RESULT |" | |
| echo "" | |
| if [ -f coverage-artifacts/rust-coverage-summary.md ]; then | |
| echo "---" | |
| cat coverage-artifacts/rust-coverage-summary.md | |
| echo "" | |
| fi | |
| if [[ "$RUST_FMT" == "success" && \ | |
| "$RUST_CLIPPY" == "success" && \ | |
| "$RUST_TEST" == "success" && \ | |
| "$SECURITY_RESULT" == "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.security.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) {} | |
| 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: github.event_name == 'push' && github.ref == 'refs/heads/main' && 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 sandbox-cli --release | |
| - name: 整理构建产物 | |
| run: | | |
| mkdir -p release | |
| cp target/release/sandbox 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 |