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
- 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
- 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"