openlark 0.15.0

飞书开放平台 Rust SDK - 企业级高覆盖率 API 客户端,极简依赖一条命令
Documentation
name: Coverage

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always
  CARGO_TOOLCHAIN: stable
  CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
  # 必须禁用增量编译才能正确收集覆盖率数据
  CARGO_INCREMENTAL: "0"
  MIN_COVERAGE: "40.0"

jobs:
  coverage:
    runs-on: ubuntu-latest
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@v1
        with:
          toolchain: ${{ env.CARGO_TOOLCHAIN }}
          components: llvm-tools-preview

      - name: Install cargo-llvm-cov
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-llvm-cov

      - name: Show cargo-llvm-cov version
        run: cargo llvm-cov --version

      # 注意:rust-cache 与覆盖率收集不兼容,禁用缓存
      # - uses: Swatinem/rust-cache@v2
      - name: Install Protoc
        uses: arduino/setup-protoc@v3
        with:
          version: 23.4
          repo-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Run coverage tests
        run: |
          set -euo pipefail
          cargo llvm-cov clean --workspace
          mkdir -p target/llvm-cov
          # 使用 --no-report 只收集覆盖率数据,不生成报告
          cargo llvm-cov --no-report --workspace --all-features --lib --tests || {
            echo "Tests failed" >&2
            exit 1
          }
      - name: Diagnose coverage generation
        if: failure()
        run: |
          echo "=== Checking LLVM tools ==="
          llvm-cov --version || echo "llvm-cov not found"
          llvm-profdata --version || echo "llvm-profdata not found"
          echo ""
          echo "=== Checking target directory ==="
          ls -la target/llvm-cov/ || echo "target/llvm-cov not found"
          echo ""
          echo "=== Checking for .profraw files ==="
          find target -name '*.profraw' -type f | head -20 || echo "No .profraw files found"
          echo ""
          echo "=== Cargo version ==="
          cargo --version
          rustc --version
      - name: Collect coverage summary
        id: coverage
        run: |
          set -euo pipefail
          echo "## 📊 Coverage Report" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY

          # 使用 --no-run 跳过重复测试,--workspace 覆盖全部 crate
          cargo llvm-cov --no-run --workspace --all-features --json --summary-only --output-path target/llvm-cov/summary.json

          cov=$(python3 -c 'import json; d=json.load(open("target/llvm-cov/summary.json","r",encoding="utf-8")); print("{:.2f}".format(d["data"][0]["totals"]["lines"]["percent"]))')

          if [ -z "$cov" ]; then
            echo "Failed to parse coverage result" >&2
            exit 1
          fi

          printf '**Global Line coverage: %s%%**\n' "$cov" | tee -a "$GITHUB_STEP_SUMMARY"
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "value=$cov" >> $GITHUB_OUTPUT

      - name: Generate per-crate coverage reports
        run: |
          set -euo pipefail
          echo "### Per-Crate Coverage" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "| Crate | Line Coverage |" >> $GITHUB_STEP_SUMMARY
          echo "|-------|---------------|" >> $GITHUB_STEP_SUMMARY
          
          # 核心模块
          for crate in openlark-core openlark-client openlark-auth; do
            echo "Generating coverage for $crate..."
            mkdir -p "target/llvm-cov/crates/$crate"
            cargo llvm-cov -p "$crate" --html --output-dir "target/llvm-cov/crates/$crate" || true
            cargo llvm-cov -p "$crate" --json --summary-only --output-path "target/llvm-cov/crates/$crate/summary.json" || true
            
            if [ -f "target/llvm-cov/crates/$crate/summary.json" ]; then
              crate_cov=$(python3 -c "import json; d=json.load(open('target/llvm-cov/crates/$crate/summary.json','r',encoding='utf-8')); print('{:.2f}'.format(d['data'][0]['totals']['lines']['percent']))" 2>/dev/null || echo "N/A")
              printf '| %s | %s%% |\n' "$crate" "$crate_cov" >> "$GITHUB_STEP_SUMMARY"
            else
              printf '| %s | N/A |\n' "$crate" >> "$GITHUB_STEP_SUMMARY"
            fi
          done
          
          # 业务模块
          for crate in openlark-communication openlark-docs openlark-hr; do
            echo "Generating coverage for $crate..."
            mkdir -p "target/llvm-cov/crates/$crate"
            cargo llvm-cov -p "$crate" --html --output-dir "target/llvm-cov/crates/$crate" || true
            cargo llvm-cov -p "$crate" --json --summary-only --output-path "target/llvm-cov/crates/$crate/summary.json" || true
            
            if [ -f "target/llvm-cov/crates/$crate/summary.json" ]; then
              crate_cov=$(python3 -c "import json; d=json.load(open('target/llvm-cov/crates/$crate/summary.json','r',encoding='utf-8')); print('{:.2f}'.format(d['data'][0]['totals']['lines']['percent']))" 2>/dev/null || echo "N/A")
              printf '| %s | %s%% |\n' "$crate" "$crate_cov" >> "$GITHUB_STEP_SUMMARY"
            else
              printf '| %s | N/A |\n' "$crate" >> "$GITHUB_STEP_SUMMARY"
            fi
          done
          
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "_Per-crate HTML reports are available in the coverage artifact._" >> "$GITHUB_STEP_SUMMARY"

      - name: Generate LCOV report
        run: |
          cargo llvm-cov --no-run --workspace --all-features --lcov --output-path target/llvm-cov/lcov.info

      - name: Generate missing lines report
        run: |
          echo "### Missing Lines Report" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "_Top 50 uncovered lines in core crates:_" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          cargo llvm-cov --workspace --show-missing-lines 2>/dev/null | head -100 >> "$GITHUB_STEP_SUMMARY" || echo "_Report generation failed_" >> "$GITHUB_STEP_SUMMARY"

      - name: Generate HTML report
        run: |
          cargo llvm-cov --no-run --workspace --all-features --html --output-dir target/llvm-cov/html

      # 注意:Codecov 上传需要 CODECOV_TOKEN,暂时禁用
      # - name: Upload coverage to Codecov
      #   if: github.event_name == 'push' && github.ref == 'refs/heads/main'
      #   continue-on-error: true
      #   uses: codecov/codecov-action@v4
      #   with:
      #     files: target/llvm-cov/lcov.info
      #     flags: unittests
      #     name: codecov-umbrella
      #     fail_ci_if_error: false

      - name: Upload HTML coverage report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage-report
          path: |
            target/llvm-cov/html/
            target/llvm-cov/crates/
          retention-days: 30

      - name: Enforce coverage threshold on main
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
        run: |
          set -euo pipefail
          cov=${{ steps.coverage.outputs.value }}
          
          # 全局门禁
          echo "=== Global Coverage Check ==="
          awk -v cov="$cov" -v min="$MIN_COVERAGE" 'BEGIN { 
            if (cov+0 < min+0) { 
              printf("❌ Global coverage %.2f%% < threshold %.2f%%\n", cov, min); 
              exit 1 
            } else { 
              printf("✅ Global coverage %.2f%% >= threshold %.2f%%\n", cov, min); 
            } 
          }'
          
          # Per-crate 门禁(仅警告,不阻塞)
          echo ""
          echo "=== Per-Crate Coverage Checks ==="
          
          check_crate() {
            local crate=$1
            local min=$2
            local summary_file="target/llvm-cov/crates/$crate/summary.json"
            
            if [ -f "$summary_file" ]; then
              crate_cov=$(python3 -c "import json; d=json.load(open('$summary_file','r',encoding='utf-8')); print('{:.2f}'.format(d['data'][0]['totals']['lines']['percent']))" 2>/dev/null || echo "0")
              
              if awk -v cov="$crate_cov" -v min="$min" 'BEGIN { exit !(cov+0 >= min+0) }'; then
                echo "✅ $crate: ${crate_cov}% >= ${min}%"
              else
                echo "⚠️  $crate: ${crate_cov}% < ${min}% (warning only)"
              fi
            fi
          }
          
          # 核心模块基线
          check_crate "openlark-core" 40
          check_crate "openlark-client" 35
          check_crate "openlark-auth" 50
          
          # 业务模块基线
          check_crate "openlark-communication" 40
          check_crate "openlark-docs" 35
          check_crate "openlark-hr" 30

      - name: Report coverage threshold (PR)
        if: github.event_name == 'pull_request'
        run: |
          cov=${{ steps.coverage.outputs.value }}
          printf 'Pull request coverage: %s%% (threshold %s%%, informational only)\n' "$cov" "$MIN_COVERAGE"