Skip to content

ClusterFuzzLite cron tasks #107

ClusterFuzzLite cron tasks

ClusterFuzzLite cron tasks #107

Workflow file for this run

name: ClusterFuzzLite cron tasks
on:
push:
branches:
- feat/clusterfuzzlite
schedule:
# Run daily at midnight UTC
- cron: '0 0 * * *'
# Allow manual trigger
workflow_dispatch:
inputs:
task:
description: 'Task to run'
required: true
default: 'all'
type: choice
options:
- all
- prune
- coverage
permissions:
contents: read
# ==============================================================================
# Storage Repository Configuration for Organization
# ==============================================================================
# Required secrets/variables:
# - Secret: CLUSTERFUZZLITE_STORAGE_TOKEN (PAT with repo write access)
# - Variable: CLUSTERFUZZLITE_STORAGE_REPO (e.g., morph-l2/fuzz_corpora)
# ==============================================================================
env:
STORAGE_REPO_URL: ${{ secrets.CLUSTERFUZZLITE_STORAGE_TOKEN && vars.CLUSTERFUZZLITE_STORAGE_REPO && format('https://{0}@github.com/{1}.git', secrets.CLUSTERFUZZLITE_STORAGE_TOKEN, vars.CLUSTERFUZZLITE_STORAGE_REPO) || '' }}
STORAGE_REPO_BRANCH: main
COVERAGE_BRANCH: coverage-reports
jobs:
# ==============================================================================
# Corpus Pruning - Removes redundant test cases from the corpus
# ==============================================================================
Pruning:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' || github.event_name == 'schedule' || github.event.inputs.task == 'all' || github.event.inputs.task == 'prune' }}
steps:
- name: Build Fuzzers
id: build
uses: google/clusterfuzzlite/actions/build_fuzzers@v1
with:
language: go
github-token: ${{ secrets.GITHUB_TOKEN }}
sanitizer: address
storage-repo: ${{ env.STORAGE_REPO_URL }}
storage-repo-branch: ${{ env.STORAGE_REPO_BRANCH }}
- name: Run Pruning
id: run
uses: google/clusterfuzzlite/actions/run_fuzzers@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
fuzz-seconds: 36000
mode: 'prune'
sanitizer: address
storage-repo: ${{ env.STORAGE_REPO_URL }}
storage-repo-branch: ${{ env.STORAGE_REPO_BRANCH }}
# ==============================================================================
# Coverage Reports - Using Go native coverage with corpus conversion
# ==============================================================================
# Process:
# 1. Download libFuzzer-format corpus from storage repo
# 2. Convert to Go native fuzz format (testdata/fuzz directories) inline
# 3. Run Go tests with coverage - fuzz functions will execute corpus as seed inputs
# 4. Generate HTML coverage reports
#
# Key insight: Go 1.18+ automatically runs seed corpus from testdata/fuzz when
# executing fuzz tests, even without -fuzz flag. This gives us corpus coverage.
# ==============================================================================
Coverage:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' || github.event_name == 'schedule' || github.event.inputs.task == 'all' || github.event.inputs.task == 'coverage' }}
needs: Pruning
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Download corpus from storage repo
run: |
echo "=== Cloning corpus repository ==="
git clone --depth 1 --branch ${{ env.STORAGE_REPO_BRANCH }} \
${{ env.STORAGE_REPO_URL }} /tmp/fuzz_corpora || {
echo "Failed to clone storage repo, continuing without corpus"
mkdir -p /tmp/fuzz_corpora/corpus
}
echo "=== Corpus downloaded ==="
ls -la /tmp/fuzz_corpora/corpus/ | head -20
- name: Convert corpus to Go native format
run: |
echo "=== Converting ClusterFuzzLite corpus to Go native format ==="
if [ ! -d "/tmp/fuzz_corpora/corpus" ]; then
echo "No corpus directory found, skipping conversion"
exit 0
fi
# Inline Python script for corpus conversion
python3 << 'PYTHON_EOF'
import os
import sys
from pathlib import Path
# Fuzzer mapping: libFuzzer corpus name -> (Go package path, FuzzFunction name)
FUZZER_MAP = {
"fuzz_abi": ("tests/fuzzers/abi", "FuzzABI"),
"fuzz_bitutil": ("tests/fuzzers/bitutil", "FuzzBitutil"),
"fuzz_blake2b": ("crypto/blake2b", "FuzzBlake2bF"),
"fuzz_difficulty": ("tests/fuzzers/difficulty", "FuzzDifficulty"),
"fuzz_keystore": ("tests/fuzzers/keystore", "FuzzKeystore"),
"fuzz_les": ("tests/fuzzers/les", "FuzzLes"),
"fuzz_rangeproof": ("tests/fuzzers/rangeproof", "FuzzRangeproof"),
"fuzz_rlp": ("tests/fuzzers/rlp", "FuzzRlp"),
"fuzz_runtime": ("tests/fuzzers/runtime", "FuzzRuntime"),
"fuzz_secp256k1": ("tests/fuzzers/secp256k1", "FuzzSecp256k1"),
"fuzz_stacktrie": ("tests/fuzzers/stacktrie", "FuzzStacktrie"),
"fuzz_trie": ("tests/fuzzers/trie", "FuzzTrie"),
"fuzz_txfetcher": ("tests/fuzzers/txfetcher", "FuzzTxfetcher"),
"fuzz_vflux_clientpool": ("tests/fuzzers/vflux", "FuzzVfluxClientPool"),
# bn256
"fuzz_bn256_add": ("tests/fuzzers/bn256", "FuzzBn256Add"),
"fuzz_bn256_mul": ("tests/fuzzers/bn256", "FuzzBn256Mul"),
"fuzz_bn256_pair": ("tests/fuzzers/bn256", "FuzzBn256Pair"),
"fuzz_bn256_unmarshal_g1": ("tests/fuzzers/bn256", "FuzzBn256UnmarshalG1"),
"fuzz_bn256_unmarshal_g2": ("tests/fuzzers/bn256", "FuzzBn256UnmarshalG2"),
# bls12381
"fuzz_bls12381_g1_add": ("tests/fuzzers/bls12381", "FuzzG1Add"),
"fuzz_bls12381_g1_multiexp": ("tests/fuzzers/bls12381", "FuzzG1MultiExp"),
"fuzz_bls12381_g1_subgroup": ("tests/fuzzers/bls12381", "FuzzG1SubgroupChecks"),
"fuzz_bls12381_g2_add": ("tests/fuzzers/bls12381", "FuzzG2Add"),
"fuzz_bls12381_g2_multiexp": ("tests/fuzzers/bls12381", "FuzzG2MultiExp"),
"fuzz_bls12381_g2_subgroup": ("tests/fuzzers/bls12381", "FuzzG2SubgroupChecks"),
"fuzz_bls12381_pairing": ("tests/fuzzers/bls12381", "FuzzPairing"),
"fuzz_bls12381_map_g1": ("tests/fuzzers/bls12381", "FuzzMapG1"),
"fuzz_bls12381_map_g2": ("tests/fuzzers/bls12381", "FuzzMapG2"),
# bls12381 cross
"fuzz_bls12381_cross_g1_add": ("tests/fuzzers/bls12381", "FuzzCrossG1Add"),
"fuzz_bls12381_cross_g1_multiexp": ("tests/fuzzers/bls12381", "FuzzCrossG1MultiExp"),
"fuzz_bls12381_cross_g2_add": ("tests/fuzzers/bls12381", "FuzzCrossG2Add"),
"fuzz_bls12381_cross_g2_multiexp": ("tests/fuzzers/bls12381", "FuzzCrossG2MultiExp"),
"fuzz_bls12381_cross_pairing": ("tests/fuzzers/bls12381", "FuzzCrossPairing"),
}
def is_printable_ascii(data):
"""Check if data is printable ASCII."""
if len(data) == 0:
return True
try:
decoded = data.decode('ascii')
return all(32 <= ord(c) < 127 or c in '\t\n\r' for c in decoded)
except:
return False
def encode_bytes_for_go(data):
"""Encode bytes for Go native corpus format."""
if len(data) == 0:
return '[]byte("")'
if is_printable_ascii(data):
s = data.decode('ascii')
s = s.replace('\\', '\\\\')
s = s.replace('"', '\\"')
s = s.replace('\n', '\\n')
s = s.replace('\r', '\\r')
s = s.replace('\t', '\\t')
return f'[]byte("{s}")'
hex_str = ''.join(f'\\x{b:02x}' for b in data)
return f'[]byte("{hex_str}")'
def convert_corpus_file(source_path, target_path):
"""Convert a single libFuzzer corpus file to Go native format."""
try:
with open(source_path, 'rb') as f:
data = f.read()
target_path.parent.mkdir(parents=True, exist_ok=True)
with open(target_path, 'w', encoding='utf-8') as f:
f.write('go test fuzz v1\n')
f.write(encode_bytes_for_go(data) + '\n')
return True
except Exception as e:
print(f" Error converting {source_path.name}: {e}")
return False
source_base = Path("/tmp/fuzz_corpora/corpus")
target_base = Path(".")
print("=" * 60)
print("Converting ClusterFuzzLite corpus to Go native format")
print("=" * 60)
total_converted = 0
total_fuzzers = 0
for fuzzer_name, (pkg_path, fuzz_func) in sorted(FUZZER_MAP.items()):
corpus_dir = source_base / fuzzer_name
if not corpus_dir.exists():
continue
corpus_files = [f for f in corpus_dir.iterdir() if f.is_file() and not f.name.startswith('.')]
if not corpus_files:
continue
print(f"→ {fuzzer_name} -> {fuzz_func} ({len(corpus_files)} files)")
target_dir = target_base / pkg_path / "testdata" / "fuzz" / fuzz_func
converted = 0
for corpus_file in corpus_files:
target_path = target_dir / corpus_file.name
if convert_corpus_file(corpus_file, target_path):
converted += 1
print(f" ✓ Converted: {converted} files")
total_converted += converted
total_fuzzers += 1
print("=" * 60)
print(f"Total: {total_fuzzers} fuzzers, {total_converted} files converted")
print("=" * 60)
PYTHON_EOF
echo ""
echo "=== Checking generated testdata directories ==="
find tests/fuzzers -type d -name "testdata" -exec sh -c 'echo "{}:"; find {} -type f | wc -l' \;
- name: Run coverage for fuzzer packages
run: |
set -e
mkdir -p coverage-reports
# List of fuzzer packages to run coverage on
FUZZER_PACKAGES=(
"tests/fuzzers/abi"
"tests/fuzzers/bitutil"
"tests/fuzzers/bls12381"
"tests/fuzzers/bn256"
"tests/fuzzers/difficulty"
"tests/fuzzers/keystore"
"tests/fuzzers/les"
"tests/fuzzers/rangeproof"
"tests/fuzzers/rlp"
"tests/fuzzers/runtime"
"tests/fuzzers/secp256k1"
"tests/fuzzers/stacktrie"
"tests/fuzzers/trie"
"tests/fuzzers/txfetcher"
"tests/fuzzers/vflux"
"crypto/blake2b"
)
fuzzer_count=0
for pkg_path in "${FUZZER_PACKAGES[@]}"; do
pkg_name=$(basename "$pkg_path")
echo ""
echo "============================================================"
echo "Processing: $pkg_name ($pkg_path)"
echo "============================================================"
if [ ! -d "$pkg_path" ]; then
echo "Package not found, skipping"
continue
fi
# Check if testdata/fuzz exists (corpus available)
if [ -d "$pkg_path/testdata/fuzz" ]; then
corpus_count=$(find "$pkg_path/testdata/fuzz" -type f ! -name ".*" | wc -l | tr -d ' ')
echo "Found testdata/fuzz with $corpus_count corpus files"
else
echo "No testdata/fuzz directory (will run tests without corpus)"
fi
# Run tests with coverage
# Note: When testdata/fuzz exists, Go will automatically use it as seed corpus
# The fuzz tests will run the corpus through the fuzz target
echo "Running tests with coverage..."
# blake2b needs -tags=gofuzz because of platform-specific assembly
if [ "$pkg_name" = "blake2b" ]; then
TEST_FLAGS="-tags=gofuzz"
else
TEST_FLAGS=""
fi
if go test $TEST_FLAGS -v -cover -coverprofile="coverage-reports/${pkg_name}.out" \
"./${pkg_path}/..." 2>&1 | tee "coverage-reports/${pkg_name}.log"; then
echo "Coverage completed for $pkg_name"
else
echo "Some tests failed for $pkg_name (may be expected)"
fi
# Check if coverage file was generated
if [ -f "coverage-reports/${pkg_name}.out" ] && [ -s "coverage-reports/${pkg_name}.out" ]; then
coverage=$(go tool cover -func="coverage-reports/${pkg_name}.out" 2>/dev/null | \
grep total | awk '{print $3}' | tr -d '%' || echo "0")
echo "Coverage for $pkg_name: ${coverage}%"
((fuzzer_count++)) || true
fi
done
echo ""
echo "============================================================"
echo "Coverage Summary"
echo "============================================================"
echo "Processed $fuzzer_count packages"
# Generate HTML reports
echo ""
echo "Generating HTML reports..."
for coverage_file in coverage-reports/*.out; do
if [ -f "$coverage_file" ] && [ -s "$coverage_file" ]; then
name=$(basename "$coverage_file" .out)
go tool cover -html="$coverage_file" -o="coverage-reports/${name}.html" 2>/dev/null || true
echo "Generated: coverage-reports/${name}.html"
fi
done
# Create summary report
echo ""
echo "Creating summary report..."
# Calculate stats for summary
total_packages=0
total_coverage=0
coverage_data=""
for coverage_file in coverage-reports/*.out; do
if [ -f "$coverage_file" ] && [ -s "$coverage_file" ]; then
name=$(basename "$coverage_file" .out)
cov=$(go tool cover -func="$coverage_file" 2>/dev/null | grep total | awk '{print $3}' | tr -d '%' || echo "0")
corpus_count=0
# Handle different package paths (crypto/blake2b vs tests/fuzzers/xxx)
if [ "$name" = "blake2b" ]; then
pkg_dir="crypto/blake2b"
else
pkg_dir="tests/fuzzers/${name}"
fi
if [ -d "${pkg_dir}/testdata/fuzz" ]; then
corpus_count=$(find "${pkg_dir}/testdata/fuzz" -type f ! -name ".*" 2>/dev/null | wc -l | tr -d ' ')
fi
coverage_data="${coverage_data}${name}|${cov}|${corpus_count}\n"
((total_packages++)) || true
total_coverage=$(echo "$total_coverage + $cov" | bc)
fi
done
# Calculate average
if [ "$total_packages" -gt 0 ]; then
avg_coverage=$(echo "scale=1; $total_coverage / $total_packages" | bc)
else
avg_coverage="0"
fi
cat > coverage-reports/index.html << HTMLEOF
<!DOCTYPE html>
<html>
<head>
<title>Go-Ethereum Fuzzer Coverage Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; border-bottom: 2px solid #4CAF50; padding-bottom: 10px; }
.summary { background: #f5f5f5; padding: 20px; border-radius: 8px; margin: 20px 0; }
.summary h2 { margin-top: 0; color: #333; }
.stats { display: flex; gap: 30px; flex-wrap: wrap; }
.stat-box { background: white; padding: 15px 25px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.stat-value { font-size: 2em; font-weight: bold; color: #4CAF50; }
.stat-label { color: #666; font-size: 0.9em; }
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
th, td { border: 1px solid #ddd; padding: 12px 8px; text-align: left; }
th { background-color: #4CAF50; color: white; }
tr:nth-child(even) { background-color: #f9f9f9; }
tr:hover { background-color: #f1f1f1; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
.coverage-high { color: #2e7d32; font-weight: bold; }
.coverage-medium { color: #f57c00; font-weight: bold; }
.coverage-low { color: #c62828; font-weight: bold; }
.progress-bar { background: #e0e0e0; border-radius: 4px; height: 20px; overflow: hidden; }
.progress-fill { height: 100%; transition: width 0.3s; }
</style>
</head>
<body>
<h1>Go-Ethereum Fuzzer Coverage Report</h1>
<div class="summary">
<h2>Summary</h2>
<div class="stats">
<div class="stat-box">
<div class="stat-value">${avg_coverage}%</div>
<div class="stat-label">Average Coverage</div>
</div>
<div class="stat-box">
<div class="stat-value">${total_packages}</div>
<div class="stat-label">Packages Tested</div>
</div>
<div class="stat-box">
<div class="stat-value">$(date -u +"%Y-%m-%d")</div>
<div class="stat-label">Report Date</div>
</div>
</div>
<p style="margin-top: 15px; color: #666;">
Generated: $(date -u)<br>
Coverage is calculated by running corpus data through each fuzzer package.
</p>
</div>
<h2>Package Details</h2>
<table>
<tr>
<th>Package</th>
<th>Coverage</th>
<th>Progress</th>
<th>Corpus Files</th>
<th>Report</th>
</tr>
HTMLEOF
# Add rows for each package
for coverage_file in coverage-reports/*.out; do
if [ -f "$coverage_file" ] && [ -s "$coverage_file" ]; then
name=$(basename "$coverage_file" .out)
cov=$(go tool cover -func="$coverage_file" 2>/dev/null | grep total | awk '{print $3}' | tr -d '%' || echo "0")
cov_display="${cov}%"
# Determine color class
cov_int=${cov%.*}
if [ "$cov_int" -ge 70 ]; then
cov_class="coverage-high"
bar_color="#4CAF50"
elif [ "$cov_int" -ge 40 ]; then
cov_class="coverage-medium"
bar_color="#ff9800"
else
cov_class="coverage-low"
bar_color="#f44336"
fi
# Count corpus files
corpus_count=0
# Handle different package paths (crypto/blake2b vs tests/fuzzers/xxx)
if [ "$name" = "blake2b" ]; then
pkg_dir="crypto/blake2b"
else
pkg_dir="tests/fuzzers/${name}"
fi
if [ -d "${pkg_dir}/testdata/fuzz" ]; then
corpus_count=$(find "${pkg_dir}/testdata/fuzz" -type f ! -name ".*" 2>/dev/null | wc -l | tr -d ' ')
fi
echo " <tr>" >> coverage-reports/index.html
echo " <td><strong>${name}</strong></td>" >> coverage-reports/index.html
echo " <td class=\"${cov_class}\">${cov_display}</td>" >> coverage-reports/index.html
echo " <td><div class=\"progress-bar\"><div class=\"progress-fill\" style=\"width: ${cov}%; background: ${bar_color};\"></div></div></td>" >> coverage-reports/index.html
echo " <td>${corpus_count}</td>" >> coverage-reports/index.html
echo " <td><a href=\"${name}.html\">View Details</a></td>" >> coverage-reports/index.html
echo " </tr>" >> coverage-reports/index.html
fi
done
cat >> coverage-reports/index.html << 'HTMLEOF2'
</table>
<footer style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 0.9em;">
<p>Generated by ClusterFuzzLite Coverage Workflow</p>
</footer>
</body>
</html>
HTMLEOF2
echo ""
echo "============================================================"
echo "Coverage Summary"
echo "============================================================"
echo "Total packages: $total_packages"
echo "Average coverage: ${avg_coverage}%"
echo ""
echo "=== Final coverage reports ==="
ls -la coverage-reports/
- name: Upload Coverage Report
uses: actions/upload-artifact@v4
if: always()
with:
name: go-coverage-report
path: coverage-reports/
retention-days: 30
- name: Upload coverage to storage repository
if: always() && env.STORAGE_REPO_URL != ''
run: |
if [ ! -d "coverage-reports" ] || [ -z "$(ls -A coverage-reports/*.html 2>/dev/null)" ]; then
echo "No coverage reports to upload"
exit 0
fi
echo "Uploading coverage reports to storage repo..."
# Reuse existing clone or clone fresh
if [ -d "/tmp/fuzz_corpora/.git" ]; then
echo "Using existing corpus repo clone"
cd /tmp/fuzz_corpora && git pull origin ${{ env.STORAGE_REPO_BRANCH }} || true
cd -
else
git clone --depth 1 --branch ${{ env.STORAGE_REPO_BRANCH }} \
${{ env.STORAGE_REPO_URL }} /tmp/fuzz_corpora
fi
# Create coverage directory with date
DATE=$(date +%Y-%m-%d)
COVERAGE_DIR="/tmp/fuzz_corpora/coverage/${DATE}"
mkdir -p "$COVERAGE_DIR"
# Copy coverage reports
cp -r coverage-reports/* "$COVERAGE_DIR/"
# Update latest symlink / copy
rm -rf /tmp/fuzz_corpora/coverage/latest
cp -r "$COVERAGE_DIR" /tmp/fuzz_corpora/coverage/latest
# Create coverage summary markdown
SUMMARY_FILE="/tmp/fuzz_corpora/coverage/SUMMARY.md"
cat > "$SUMMARY_FILE" << 'SUMMARYEOF'
# Fuzzer Coverage Summary
Coverage reports generated by running corpus data through each fuzzer.
SUMMARYEOF
# Add summary stats
echo "## Latest Report (${DATE})" >> "$SUMMARY_FILE"
echo "" >> "$SUMMARY_FILE"
echo "| Package | Coverage | Corpus Files |" >> "$SUMMARY_FILE"
echo "|---------|----------|--------------|" >> "$SUMMARY_FILE"
total_packages=0
total_coverage=0
for coverage_file in coverage-reports/*.out; do
if [ -f "$coverage_file" ] && [ -s "$coverage_file" ]; then
name=$(basename "$coverage_file" .out)
cov=$(go tool cover -func="$coverage_file" 2>/dev/null | grep total | awk '{print $3}' | tr -d '%' || echo "0")
corpus_count=0
# Handle different package paths (crypto/blake2b vs tests/fuzzers/xxx)
if [ "$name" = "blake2b" ]; then
pkg_dir="crypto/blake2b"
else
pkg_dir="tests/fuzzers/${name}"
fi
if [ -d "${pkg_dir}/testdata/fuzz" ]; then
corpus_count=$(find "${pkg_dir}/testdata/fuzz" -type f ! -name ".*" 2>/dev/null | wc -l | tr -d ' ')
fi
echo "| ${name} | ${cov}% | ${corpus_count} |" >> "$SUMMARY_FILE"
((total_packages++)) || true
total_coverage=$(echo "$total_coverage + $cov" | bc)
fi
done
# Calculate and add average
if [ "$total_packages" -gt 0 ]; then
avg_coverage=$(echo "scale=1; $total_coverage / $total_packages" | bc)
else
avg_coverage="0"
fi
echo "" >> "$SUMMARY_FILE"
echo "**Average Coverage: ${avg_coverage}%** | **Packages: ${total_packages}**" >> "$SUMMARY_FILE"
echo "" >> "$SUMMARY_FILE"
# Add links
echo "## View Full Reports" >> "$SUMMARY_FILE"
echo "" >> "$SUMMARY_FILE"
echo "- [Latest HTML Report](./latest/index.html)" >> "$SUMMARY_FILE"
echo "" >> "$SUMMARY_FILE"
echo "## Historical Reports" >> "$SUMMARY_FILE"
echo "" >> "$SUMMARY_FILE"
# List all reports
for dir in $(ls -d /tmp/fuzz_corpora/coverage/20*/ 2>/dev/null | sort -r | head -20); do
date_dir=$(basename "$dir")
echo "- [${date_dir}](./${date_dir}/index.html)" >> "$SUMMARY_FILE"
done
echo "" >> "$SUMMARY_FILE"
echo "---" >> "$SUMMARY_FILE"
echo "*Generated: $(date -u)*" >> "$SUMMARY_FILE"
# Commit and push
cd /tmp/fuzz_corpora
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .
git commit -m "Add coverage report ${DATE} from ${{ github.repository }}" || echo "No changes to commit"
git pull --rebase origin ${{ env.STORAGE_REPO_BRANCH }} || true
git push || echo "Failed to push"
echo "✅ Coverage reports uploaded to storage repository"
echo "📁 Location: coverage/${DATE}/"
# ========================================================================
# Email Notification - Daily Coverage Report
# ========================================================================
- name: Prepare coverage email data
id: coverage_data
if: always()
run: |
# Calculate coverage stats for email
total_packages=0
total_coverage=0
coverage_rows=""
for coverage_file in coverage-reports/*.out; do
if [ -f "$coverage_file" ] && [ -s "$coverage_file" ]; then
name=$(basename "$coverage_file" .out)
cov=$(go tool cover -func="$coverage_file" 2>/dev/null | grep total | awk '{print $3}' | tr -d '%' || echo "0")
corpus_count=0
# Handle different package paths (crypto/blake2b vs tests/fuzzers/xxx)
if [ "$name" = "blake2b" ]; then
pkg_dir="crypto/blake2b"
else
pkg_dir="tests/fuzzers/${name}"
fi
if [ -d "${pkg_dir}/testdata/fuzz" ]; then
corpus_count=$(find "${pkg_dir}/testdata/fuzz" -type f ! -name ".*" 2>/dev/null | wc -l | tr -d ' ')
fi
# Color based on coverage
cov_int=${cov%.*}
if [ "$cov_int" -ge 70 ]; then
color="#2e7d32"
elif [ "$cov_int" -ge 40 ]; then
color="#f57c00"
else
color="#c62828"
fi
coverage_rows="${coverage_rows}<tr><td style='padding:8px;border-bottom:1px solid #ddd;'>${name}</td><td style='padding:8px;border-bottom:1px solid #ddd;color:${color};font-weight:bold;'>${cov}%</td><td style='padding:8px;border-bottom:1px solid #ddd;'>${corpus_count}</td></tr>"
((total_packages++)) || true
total_coverage=$(echo "$total_coverage + $cov" | bc)
fi
done
if [ "$total_packages" -gt 0 ]; then
avg_coverage=$(echo "scale=1; $total_coverage / $total_packages" | bc)
else
avg_coverage="0"
fi
# Output for next step
echo "avg_coverage=${avg_coverage}" >> $GITHUB_OUTPUT
echo "total_packages=${total_packages}" >> $GITHUB_OUTPUT
echo "date=$(date -u +%Y-%m-%d)" >> $GITHUB_OUTPUT
# Save coverage rows to file (too long for env var)
echo "$coverage_rows" > /tmp/coverage_rows.html
- name: Send daily coverage report to Feishu
if: always() && env.STORAGE_REPO_URL != ''
run: |
# Read coverage rows from file
COVERAGE_TABLE=""
if [ -f /tmp/coverage_rows.html ]; then
# Convert HTML table rows to Feishu markdown format
for coverage_file in coverage-reports/*.out; do
if [ -f "$coverage_file" ] && [ -s "$coverage_file" ]; then
name=$(basename "$coverage_file" .out)
cov=$(go tool cover -func="$coverage_file" 2>/dev/null | grep total | awk '{print $3}' || echo "0")
corpus_count=0
if [ "$name" = "blake2b" ]; then
pkg_dir="crypto/blake2b"
else
pkg_dir="tests/fuzzers/${name}"
fi
if [ -d "${pkg_dir}/testdata/fuzz" ]; then
corpus_count=$(find "${pkg_dir}/testdata/fuzz" -type f ! -name ".*" 2>/dev/null | wc -l | tr -d ' ')
fi
COVERAGE_TABLE="${COVERAGE_TABLE}\n**${name}**: ${cov} (${corpus_count} files)"
fi
done
fi
# Generate corpus repository URL for the coverage report
DATE=$(date +%Y-%m-%d)
STORAGE_REPO="${{ vars.CLUSTERFUZZLITE_STORAGE_REPO }}"
COVERAGE_REPORT_URL="https://github.com/${STORAGE_REPO}/blob/main/coverage/SUMMARY.md"
# Send to Feishu
curl -X POST "${{ secrets.FEISHU_WEBHOOK_URL }}" \
-H "Content-Type: application/json" \
-d @- << EOF
{
"msg_type": "interactive",
"card": {
"config": {
"wide_screen_mode": true
},
"header": {
"title": {
"tag": "plain_text",
"content": "📊 Daily Fuzzer Coverage Report"
},
"template": "blue"
},
"elements": [
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": "**Average Coverage:** ${{ steps.coverage_data.outputs.avg_coverage }}%\n**Packages Tested:** ${{ steps.coverage_data.outputs.total_packages }}\n**Report Date:** ${{ steps.coverage_data.outputs.date }}"
}
},
{
"tag": "hr"
},
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": "**Repository:** ${{ github.repository }}\n**Branch:** ${{ github.ref_name }}"
}
},
{
"tag": "action",
"actions": [
{
"tag": "button",
"text": {
"tag": "plain_text",
"content": "View Coverage Report"
},
"type": "primary",
"url": "${COVERAGE_REPORT_URL}"
}
]
},
{
"tag": "note",
"elements": [
{
"tag": "plain_text",
"content": "Coverage calculated by running corpus data through each fuzzer package. Detailed reports available in corpus repository."
}
]
}
]
}
}
EOF
continue-on-error: true