fix(desktop): macOS system proxy support and missing native dependency #1142
Workflow file for this run
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
| # Defense-in-Depth CI Pipeline | |
| # | |
| # 摒弃传统针对 DX 的"宽容度",用硬性约束封锁 AI 的乱写空间。 | |
| # 通过持续演进的架构约束实现深度防御: | |
| # - 控制爆炸半径:通过权限和行为约束限定 AI 的操作范围 | |
| # - 反熵增机制:设立质量门槛与技术债检查,将 AI 的解空间限制在安全边界内 | |
| # | |
| # Hard Gates(任一失败直接阻断): | |
| # - ts_test_pass: TypeScript 测试必须 100% 通过 | |
| # - rust_test_pass: Rust 测试必须 100% 通过 | |
| # - api_contract_parity: API 契约必须一致 | |
| # - lint_pass: Lint 必须 0 错误 | |
| # - no_critical_vulnerabilities: 无严重安全漏洞 | |
| name: 'Defense' | |
| on: | |
| pull_request: | |
| paths-ignore: | |
| - '*.md' | |
| - 'docs/**' | |
| - '.github/ISSUE_TEMPLATE/**' | |
| push: | |
| branches: [main] | |
| paths-ignore: | |
| - '*.md' | |
| - 'docs/**' | |
| schedule: | |
| # 每周一 UTC 00:00 运行 security 维度 | |
| - cron: '0 0 * * 1' | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| security-events: write | |
| env: | |
| CARGO_TERM_COLOR: always | |
| NODE_ENV: test | |
| jobs: | |
| # ══════════════════════════════════════════════════════════════ | |
| # COMMIT SAFETY VALIDATION: Block test credentials & mass deletions | |
| # ══════════════════════════════════════════════════════════════ | |
| validate-commit-safety: | |
| name: 'Gate: Commit Safety' | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' || github.event_name == 'push' | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 # Full history needed to scan commits | |
| - name: Check for mass file deletions | |
| run: | | |
| # Determine range to check | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| BASE_SHA="${{ github.event.pull_request.base.sha }}" | |
| HEAD_SHA="${{ github.event.pull_request.head.sha }}" | |
| RANGE="$BASE_SHA..$HEAD_SHA" | |
| echo "📋 Checking PR commits: $RANGE" | |
| else | |
| # For push events, check the pushed commits | |
| RANGE="${{ github.event.before }}..${{ github.event.after }}" | |
| echo "📋 Checking pushed commits: $RANGE" | |
| fi | |
| echo "" | |
| echo "🔍 Scanning for mass file deletions..." | |
| HAS_MASS_DELETE=0 | |
| for commit in $(git rev-list $RANGE 2>/dev/null); do | |
| DELETED=$(git show --diff-filter=D --name-only --format="" $commit 2>/dev/null | wc -l | tr -d ' ') | |
| if [ "$DELETED" -ge 200 ]; then | |
| HAS_MASS_DELETE=1 | |
| SHORT_HASH=$(echo $commit | cut -c1-8) | |
| SUBJECT=$(git log -1 --format="%s" $commit) | |
| echo "" | |
| echo "❌ MASS DELETION DETECTED" | |
| echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| echo " Commit: $SHORT_HASH" | |
| echo " Subject: $SUBJECT" | |
| echo " Deleted files: $DELETED (threshold: 200)" | |
| echo "" | |
| # Show first 20 deleted files as sample | |
| echo " Sample of deleted files:" | |
| git show --diff-filter=D --name-only --format="" $commit | head -20 | sed 's/^/ - /' | |
| if [ "$DELETED" -gt 20 ]; then | |
| echo " ... and $((DELETED - 20)) more files" | |
| fi | |
| echo "" | |
| echo "::error::Commit $SHORT_HASH deletes $DELETED files (threshold: 200)" | |
| fi | |
| done | |
| if [ "$HAS_MASS_DELETE" = "1" ]; then | |
| echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| echo "" | |
| echo "This protection prevents accidental mass deletions." | |
| echo "" | |
| echo "If this deletion is intentional:" | |
| echo " 1. Add clear justification in commit message" | |
| echo " 2. Split into smaller logical commits (recommended)" | |
| echo " 3. Document impact and reason for mass deletion" | |
| echo "" | |
| exit 1 | |
| fi | |
| echo "✅ No mass deletions detected" | |
| - name: Validate commit authors | |
| run: | | |
| # Determine range to check | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| BASE_SHA="${{ github.event.pull_request.base.sha }}" | |
| HEAD_SHA="${{ github.event.pull_request.head.sha }}" | |
| RANGE="$BASE_SHA..$HEAD_SHA" | |
| echo "📋 Checking PR commits: $RANGE" | |
| else | |
| # For push events, check the pushed commits | |
| RANGE="${{ github.event.before }}..${{ github.event.after }}" | |
| echo "📋 Checking pushed commits: $RANGE" | |
| fi | |
| # Scan for test credentials | |
| echo "" | |
| echo "🔍 Scanning for test credentials..." | |
| SUSPICIOUS=$(git log "$RANGE" --format="%H|%ae|%an" 2>/dev/null | \ | |
| grep -iE "(test@example\.com|routa test|placeholder@|noreply@test)" || true) | |
| if [ -n "$SUSPICIOUS" ]; then | |
| echo "" | |
| echo "❌ TEST CREDENTIALS DETECTED IN COMMITS" | |
| echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| echo "" | |
| echo "$SUSPICIOUS" | while IFS='|' read hash email name; do | |
| SHORT_HASH=$(echo $hash | cut -c1-8) | |
| echo " Commit: $SHORT_HASH" | |
| echo " Author: $name <$email>" | |
| git log -1 --format=" Subject: %s" $hash | |
| echo "" | |
| done | |
| echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| echo "" | |
| echo "::error::Commits with test credentials detected" | |
| echo "::error::These commits must be amended with real author information" | |
| echo "" | |
| echo "To fix:" | |
| echo " 1. Use interactive rebase: git rebase -i origin/main" | |
| echo " 2. Mark commits for 'edit'" | |
| echo " 3. Amend each commit: git commit --amend --author=\"Your Name <your@email.com>\" --no-edit" | |
| echo " 4. Continue: git rebase --continue" | |
| echo "" | |
| exit 1 | |
| fi | |
| echo "✅ All commits have valid author metadata" | |
| - name: Validate email formats | |
| run: | | |
| # Check that all emails are valid format | |
| INVALID=$(git log "$RANGE" --format="%ae" 2>/dev/null | \ | |
| grep -vE "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" || true) | |
| if [ -n "$INVALID" ]; then | |
| echo "⚠️ Warning: Invalid email formats detected:" | |
| echo "$INVALID" | |
| # Don't fail, just warn | |
| fi | |
| # ══════════════════════════════════════════════════════════════ | |
| # FITNESS DIMENSIONS: 按 docs/fitness 维度拆分执行 | |
| # ══════════════════════════════════════════════════════════════ | |
| fitness-dimensions: | |
| name: 'Gate: ${{ matrix.label }}' | |
| runs-on: ubuntu-latest | |
| if: github.event_name != 'schedule' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - dimension: code_quality | |
| label: Code Quality | |
| - dimension: engineering_governance | |
| label: Engineering Governance | |
| - dimension: testability | |
| label: Testability | |
| - dimension: security | |
| label: Security | |
| - dimension: api_contract | |
| label: API Contract | |
| - dimension: design_system | |
| label: Design System | |
| - dimension: evolvability | |
| label: Evolvability | |
| - dimension: ui_consistency | |
| label: UI Consistency | |
| - dimension: observability | |
| label: Observability | |
| - dimension: performance | |
| label: Performance | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| submodules: recursive | |
| - name: Install Rust system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| pkg-config \ | |
| libglib2.0-dev \ | |
| libgtk-3-dev \ | |
| libwebkit2gtk-4.1-dev \ | |
| libayatana-appindicator3-dev \ | |
| librsvg2-dev \ | |
| libsoup-3.0-dev \ | |
| libjavascriptcoregtk-4.1-dev \ | |
| patchelf | |
| - name: Setup Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.11' | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: lts/* | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: clippy | |
| - name: Rust cache | |
| uses: swatinem/rust-cache@v2 | |
| - name: Install entrix | |
| run: pip install entrix | |
| - name: Run fitness dimension | |
| id: fitness | |
| run: | | |
| mkdir -p .artifacts | |
| entrix run \ | |
| --parallel \ | |
| --tier normal \ | |
| --scope ci \ | |
| --min-score 0 \ | |
| --dimension "${{ matrix.dimension }}" \ | |
| --output ".artifacts/fitness-report-${{ matrix.dimension }}.json" | |
| - name: Summary | |
| if: always() | |
| run: | | |
| { | |
| echo "## Fitness Dimension Report" | |
| echo "" | |
| echo "| Gate | Status |" | |
| echo "|------|--------|" | |
| echo "| ${{ matrix.label }} | ${{ steps.fitness.outcome == 'success' && '✅' || '❌' }} |" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| if [ -f ".artifacts/fitness-report-${{ matrix.dimension }}.json" ]; then | |
| python3 -c 'import json; from pathlib import Path; payload=json.loads(Path(".artifacts/fitness-report-${{ matrix.dimension }}.json").read_text(encoding="utf-8")); print(""); print("- Dimension: {}".format("${{ matrix.dimension }}")); print("- Final score: {:.1f}".format(payload.get("final_score", 0))); print("- Hard gate blocked: {}".format(payload.get("hard_gate_blocked", False))); print("- Score blocked: {}".format(payload.get("score_blocked", False))); [print("- {}: score={:.1f} passed={}/{}".format(dimension["name"], dimension["score"], dimension["passed"], dimension["total"])) for dimension in payload.get("dimensions", [])]' >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Upload fitness artifact | |
| if: always() && hashFiles(format('.artifacts/fitness-report-{0}.json', matrix.dimension)) != '' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: fitness-report-${{ matrix.dimension }} | |
| path: .artifacts/fitness-report-${{ matrix.dimension }}.json | |
| fitness-summary: | |
| name: 'Fitness Summary' | |
| runs-on: ubuntu-latest | |
| needs: [fitness-dimensions] | |
| if: always() && github.event_name != 'schedule' | |
| steps: | |
| - name: Download fitness artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: fitness-report-* | |
| path: .artifacts | |
| merge-multiple: true | |
| - name: Summary | |
| run: | | |
| python3 -c 'import json; from pathlib import Path; files=sorted(Path(".artifacts").glob("fitness-report-*.json")); print("## Fitness Summary"); print(""); print("| Dimension | Final score | Hard gate blocked | Score blocked |"); print("|-----------|-------------|-------------------|---------------|"); [print("| {} | {:.1f} | {} | {} |".format(((payload.get("dimensions") or [{}])[0].get("name") or file_path.stem.removeprefix("fitness-report-")), payload.get("final_score", 0), payload.get("hard_gate_blocked", False), payload.get("score_blocked", False))) for file_path in files for payload in [json.loads(file_path.read_text(encoding="utf-8"))]] or print("| none | n/a | n/a | n/a |")' >> "$GITHUB_STEP_SUMMARY" | |
| # ══════════════════════════════════════════════════════════════ | |
| # SECURITY DIMENSION: 依赖漏洞扫描 | |
| # ══════════════════════════════════════════════════════════════ | |
| dependency-scan: | |
| name: 'Security: Dependency Scan' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: lts/* | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: npm audit (critical) | |
| run: npm audit --audit-level=critical | |
| continue-on-error: false | |
| - name: npm audit (high) - report only | |
| run: npm audit --audit-level=high || true | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Install cargo-audit | |
| run: cargo install cargo-audit | |
| - name: cargo audit | |
| run: cargo audit | |
| continue-on-error: true | |
| # ══════════════════════════════════════════════════════════════ | |
| # SECURITY DIMENSION: Semgrep SAST 扫描 | |
| # ══════════════════════════════════════════════════════════════ | |
| semgrep: | |
| name: 'Security: Semgrep SAST' | |
| runs-on: ubuntu-latest | |
| container: | |
| image: semgrep/semgrep | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Semgrep scan (security-audit + OWASP) | |
| run: | | |
| semgrep scan \ | |
| --config=p/security-audit \ | |
| --config=p/owasp-top-ten \ | |
| --config=p/typescript \ | |
| --config=p/javascript \ | |
| --sarif \ | |
| --output=semgrep-results.sarif \ | |
| . | |
| continue-on-error: true | |
| - name: Upload SARIF to GitHub Security | |
| uses: github/codeql-action/upload-sarif@v4 | |
| if: always() | |
| with: | |
| sarif_file: semgrep-results.sarif | |
| - name: Upload Semgrep SARIF artifact | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: semgrep-results-sarif | |
| path: semgrep-results.sarif | |
| if-no-files-found: warn | |
| - name: Semgrep strict (ERROR only, fail on match) | |
| run: | | |
| semgrep scan \ | |
| --config=p/security-audit \ | |
| --config=p/owasp-top-ten \ | |
| --severity=ERROR \ | |
| --error \ | |
| . | |
| # ══════════════════════════════════════════════════════════════ | |
| # SECURITY DIMENSION: Trivy 文件系统扫描 | |
| # ══════════════════════════════════════════════════════════════ | |
| trivy: | |
| name: 'Security: Trivy Scan' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Run Trivy vulnerability scanner | |
| uses: aquasecurity/trivy-action@master | |
| with: | |
| scan-type: 'fs' | |
| scan-ref: '.' | |
| severity: 'CRITICAL,HIGH' | |
| format: 'sarif' | |
| output: 'trivy-results.sarif' | |
| - name: Upload Trivy SARIF to GitHub Security | |
| uses: github/codeql-action/upload-sarif@v4 | |
| if: always() | |
| with: | |
| sarif_file: trivy-results.sarif | |
| - name: Upload Trivy SARIF artifact | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: trivy-results-sarif | |
| path: trivy-results.sarif | |
| if-no-files-found: warn | |
| # ══════════════════════════════════════════════════════════════ | |
| # SECURITY DIMENSION: Dockerfile 检查 | |
| # ══════════════════════════════════════════════════════════════ | |
| hadolint: | |
| name: 'Security: Hadolint' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Detect Dockerfile | |
| id: dockerfile | |
| run: | | |
| if [ -f Dockerfile ]; then | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Lint Dockerfile | |
| if: steps.dockerfile.outputs.exists == 'true' | |
| uses: hadolint/hadolint-action@v3.3.0 | |
| with: | |
| dockerfile: Dockerfile | |
| failure-threshold: error | |
| # ══════════════════════════════════════════════════════════════ | |
| # SECURITY DIMENSION: 汇总 | |
| # ══════════════════════════════════════════════════════════════ | |
| security-summary: | |
| name: 'Security Summary' | |
| runs-on: ubuntu-latest | |
| needs: [dependency-scan, semgrep, trivy, hadolint] | |
| if: always() | |
| steps: | |
| - name: Summary | |
| run: | | |
| { | |
| echo "## Defense / Security Summary" | |
| echo "" | |
| echo "| Check | Status |" | |
| echo "|-------|--------|" | |
| echo "| Dependency Scan | ${{ needs.dependency-scan.result == 'success' && '✅' || '❌' }} |" | |
| echo "| Semgrep SAST | ${{ needs.semgrep.result == 'success' && '✅' || '❌' }} |" | |
| echo "| Trivy | ${{ needs.trivy.result == 'success' && '✅' || '❌' }} |" | |
| echo "| Hadolint | ${{ needs.hadolint.result == 'success' && '✅' || '❌' }} |" | |
| } >> "$GITHUB_STEP_SUMMARY" |