📊 Repository Health Monitor #82
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
name: 📊 Repository Health Monitor | |
on: | |
schedule: | |
# Run daily at 6 AM UTC | |
- cron: "0 6 * * *" | |
workflow_dispatch: | |
push: | |
branches: [main] | |
paths: | |
- "package.json" | |
- "package-lock.json" | |
- ".github/workflows/**" | |
env: | |
NODE_VERSION: "20" | |
jobs: | |
# Health check and metrics collection | |
health-check: | |
name: 🏥 Repository Health Check | |
runs-on: ubuntu-latest | |
outputs: | |
health-score: ${{ steps.health.outputs.score }} | |
metrics: ${{ steps.metrics.outputs.data }} | |
steps: | |
- name: Checkout code | |
uses: actions/checkout@v4 | |
with: | |
fetch-depth: 0 | |
- name: Setup Node.js | |
uses: actions/setup-node@v4 | |
with: | |
node-version: ${{ env.NODE_VERSION }} | |
cache: "npm" | |
- name: Install dependencies | |
run: | | |
# Clean install to prevent rollup optional dependency issues | |
rm -rf node_modules package-lock.json | |
npm install | |
- name: Collect repository metrics | |
id: metrics | |
run: | | |
echo "📊 Collecting repository metrics..." | |
# Code metrics | |
total_files=$(find frontend/src/ -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" 2>/dev/null | wc -l || echo 0) | |
if command -v wc >/dev/null; then | |
# Use find with wc to count lines across all files | |
total_lines=$(find frontend/src/ -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" -exec cat {} + 2>/dev/null | wc -l || echo 0) | |
else | |
# Fallback method | |
total_lines=$(find src/ -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" -exec wc -l {} + 2>/dev/null | tail -1 | awk '{print $1}' || echo 0) | |
fi | |
# Git metrics | |
commits_last_week=$(git log --since="1 week ago" --oneline | wc -l) | |
contributors=$(git log --format='%an' | sort -u | wc -l) | |
# Build metrics | |
npm run build > /dev/null 2>&1 | |
build_size=$(du -sh build/ | cut -f1) | |
# Test metrics | |
test_output=$(npm test --silent 2>&1 || echo "Tests failed") | |
# Package metrics | |
total_deps=$(cat package.json | jq '.dependencies | length') | |
total_dev_deps=$(cat package.json | jq '.devDependencies | length') | |
# Security metrics | |
audit_output=$(npm audit --json 2>/dev/null || echo '{"metadata":{"vulnerabilities":{}}}') | |
# Check if audit shows actual vulnerabilities by counting those with severity | |
vulnerabilities=$(echo "$audit_output" | jq '[.metadata.vulnerabilities | to_entries[] | select(.value > 0)] | length' 2>/dev/null || echo 0) | |
# Ensure all variables have safe defaults | |
total_files=${total_files:-0} | |
total_lines=${total_lines:-0} | |
commits_last_week=${commits_last_week:-0} | |
contributors=${contributors:-0} | |
build_size=${build_size:-"0K"} | |
total_deps=${total_deps:-0} | |
total_dev_deps=${total_dev_deps:-0} | |
vulnerabilities=${vulnerabilities:-0} | |
# Create metrics JSON using jq for robust single-line output | |
metrics_json=$(jq -n -c \ | |
--arg total_files "$total_files" \ | |
--arg total_lines "$total_lines" \ | |
--arg commits_last_week "$commits_last_week" \ | |
--arg contributors "$contributors" \ | |
--arg build_size "$build_size" \ | |
--arg total_deps "$total_deps" \ | |
--arg total_dev_deps "$total_dev_deps" \ | |
--arg vulnerabilities "$vulnerabilities" \ | |
'{ | |
code: { | |
total_files: ($total_files | tonumber), | |
total_lines: ($total_lines | tonumber) | |
}, | |
git: { | |
commits_last_week: ($commits_last_week | tonumber), | |
contributors: ($contributors | tonumber) | |
}, | |
build: { | |
size: $build_size, | |
status: "success" | |
}, | |
dependencies: { | |
total: ($total_deps | tonumber), | |
dev: ($total_dev_deps | tonumber) | |
}, | |
security: { | |
vulnerabilities: ($vulnerabilities | tonumber) | |
} | |
}' | |
) | |
# Use GitHub Actions multiline output format | |
{ | |
echo "data<<EOF" | |
echo "$metrics_json" | |
echo "EOF" | |
} >> $GITHUB_OUTPUT | |
echo "📈 Repository Metrics:" | |
echo "- Files: $total_files" | |
echo "- Lines: $total_lines" | |
echo "- Build Size: $build_size" | |
echo "- Dependencies: $total_deps" | |
echo "- Vulnerabilities: $vulnerabilities" | |
- name: Calculate health score | |
id: health | |
run: | | |
echo "🏥 Calculating repository health score..." | |
score=100 | |
# Deduct points for security issues | |
vulnerabilities=$(echo '${{ steps.metrics.outputs.data }}' | jq '.security.vulnerabilities // 0') | |
if [[ $vulnerabilities =~ ^[0-9]+$ ]] && [[ $vulnerabilities -gt 0 ]]; then | |
score=$((score - (vulnerabilities * 10))) | |
echo "⚠️ Deducted points for $vulnerabilities vulnerabilities" | |
fi | |
# Check if tests are passing | |
if ! npm test -- --watchAll=false --silent > /dev/null 2>&1; then | |
score=$((score - 20)) | |
echo "⚠️ Deducted points for failing tests" | |
fi | |
# Check if build is successful | |
if ! npm run build > /dev/null 2>&1; then | |
score=$((score - 30)) | |
echo "⚠️ Deducted points for build failures" | |
fi | |
# Check for recent activity | |
commits_last_week=$(echo '${{ steps.metrics.outputs.data }}' | jq '.git.commits_last_week') | |
if [[ $commits_last_week -eq 0 ]]; then | |
score=$((score - 5)) | |
echo "⚠️ Deducted points for no recent activity" | |
fi | |
# Ensure score doesn't go below 0 | |
if [[ $score -lt 0 ]]; then | |
score=0 | |
fi | |
echo "score=$score" >> $GITHUB_OUTPUT | |
echo "🏥 Repository Health Score: $score/100" | |
# Performance monitoring | |
performance-monitor: | |
name: ⚡ Performance Monitor | |
runs-on: ubuntu-latest | |
outputs: | |
performance-score: ${{ steps.perf.outputs.score }} | |
metrics: ${{ steps.perf.outputs.metrics }} | |
steps: | |
- name: Checkout code | |
uses: actions/checkout@v4 | |
- name: Setup Node.js | |
uses: actions/setup-node@v4 | |
with: | |
node-version: ${{ env.NODE_VERSION }} | |
cache: "npm" | |
- name: Install dependencies | |
run: | | |
# Clean install to prevent rollup optional dependency issues | |
rm -rf node_modules package-lock.json | |
npm install | |
- name: Build application | |
run: npm run build | |
- name: Analyze bundle size | |
id: perf | |
run: | | |
echo "⚡ Analyzing performance metrics..." | |
# Bundle analysis - Check both build/assets and build/static paths | |
if [[ -d "build/assets" ]]; then | |
echo "📁 Using build/assets directory structure" | |
# Use du -b for Linux (GitHub Actions) and stat for cross-platform compatibility | |
if command -v stat >/dev/null && stat --version 2>&1 | grep -q GNU; then | |
# GNU stat (Linux) | |
total_js_size=$(find build/assets -name "*.js" -exec stat -c%s {} + 2>/dev/null | awk '{sum += $1} END {print sum}' || echo 0) | |
total_css_size=$(find build/assets -name "*.css" -exec stat -c%s {} + 2>/dev/null | awk '{sum += $1} END {print sum}' || echo 0) | |
else | |
# Use du -b as fallback for GitHub Actions | |
total_js_size=$(find build/assets -name "*.js" -exec du -b {} + 2>/dev/null | awk '{sum += $1} END {print sum}' || echo 0) | |
total_css_size=$(find build/assets -name "*.css" -exec du -b {} + 2>/dev/null | awk '{sum += $1} END {print sum}' || echo 0) | |
fi | |
js_files=$(find build/assets -name "*.js" | wc -l) | |
elif [[ -d "build/static" ]]; then | |
echo "📁 Using build/static directory structure" | |
if command -v stat >/dev/null && stat --version 2>&1 | grep -q GNU; then | |
# GNU stat (Linux) | |
total_js_size=$(find build/static/js -name "*.js" -exec stat -c%s {} + 2>/dev/null | awk '{sum += $1} END {print sum}' || echo 0) | |
total_css_size=$(find build/static/css -name "*.css" -exec stat -c%s {} + 2>/dev/null | awk '{sum += $1} END {print sum}' || echo 0) | |
else | |
# Use du -b as fallback | |
total_js_size=$(find build/static/js -name "*.js" -exec du -b {} + 2>/dev/null | awk '{sum += $1} END {print sum}' || echo 0) | |
total_css_size=$(find build/static/css -name "*.css" -exec du -b {} + 2>/dev/null | awk '{sum += $1} END {print sum}' || echo 0) | |
fi | |
js_files=$(find build/static/js -name "*.js" | wc -l) | |
else | |
echo "⚠️ No build directory found" | |
total_js_size=0 | |
total_css_size=0 | |
js_files=0 | |
fi | |
# Ensure we have valid numbers | |
total_js_size=${total_js_size:-0} | |
total_css_size=${total_css_size:-0} | |
js_files=${js_files:-0} | |
# Convert to KB (handle case where size is 0) | |
if [[ $total_js_size -gt 0 ]]; then | |
js_size_kb=$((total_js_size / 1024)) | |
else | |
js_size_kb=0 | |
fi | |
if [[ $total_css_size -gt 0 ]]; then | |
css_size_kb=$((total_css_size / 1024)) | |
else | |
css_size_kb=0 | |
fi | |
total_size_kb=$((js_size_kb + css_size_kb)) | |
# Performance score calculation | |
perf_score=100 | |
# Deduct points based on bundle size | |
if [[ $js_size_kb -gt 1000 ]]; then | |
perf_score=$((perf_score - 20)) | |
echo "⚠️ Large JavaScript bundle: ${js_size_kb}KB" | |
fi | |
if [[ $css_size_kb -gt 100 ]]; then | |
perf_score=$((perf_score - 10)) | |
echo "⚠️ Large CSS bundle: ${css_size_kb}KB" | |
fi | |
# Check for code splitting (adjust threshold for Vite builds) | |
if [[ $js_files -lt 5 ]]; then | |
perf_score=$((perf_score - 10)) | |
echo "⚠️ Could benefit from more code splitting (${js_files} JS files)" | |
fi | |
echo "score=$perf_score" >> $GITHUB_OUTPUT | |
# Create metrics JSON (single line for GitHub Actions output) | |
metrics_json=$(jq -nc \ | |
--argjson js_size_kb "$js_size_kb" \ | |
--argjson css_size_kb "$css_size_kb" \ | |
--argjson total_size_kb "$total_size_kb" \ | |
--argjson js_files "$js_files" \ | |
--argjson score "$perf_score" \ | |
'{"bundle": {"js_size_kb": $js_size_kb, "css_size_kb": $css_size_kb, "total_size_kb": $total_size_kb, "js_files": $js_files}, "score": $score}' | |
) | |
echo "metrics=$metrics_json" >> $GITHUB_OUTPUT | |
echo "📊 Performance Metrics:" | |
echo "- JavaScript: ${js_size_kb}KB" | |
echo "- CSS: ${css_size_kb}KB" | |
echo "- Total: ${total_size_kb}KB" | |
echo "- Performance Score: $perf_score/100" | |
# Dependency health check | |
dependency-health: | |
name: 📦 Dependency Health | |
runs-on: ubuntu-latest | |
outputs: | |
health-status: ${{ steps.deps.outputs.status }} | |
outdated-count: ${{ steps.deps.outputs.outdated }} | |
steps: | |
- name: Checkout code | |
uses: actions/checkout@v4 | |
- name: Setup Node.js | |
uses: actions/setup-node@v4 | |
with: | |
node-version: ${{ env.NODE_VERSION }} | |
cache: "npm" | |
- name: Check dependency health | |
id: deps | |
run: | | |
echo "📦 Checking dependency health..." | |
# Install dependencies | |
# Clean install to prevent rollup optional dependency issues | |
rm -rf node_modules package-lock.json | |
npm install | |
# Check for outdated packages | |
outdated_output=$(npm outdated --json 2>/dev/null || echo "{}") | |
outdated_count=$(echo "$outdated_output" | jq 'length // 0') | |
# Validate counts are numbers | |
if ! [[ $outdated_count =~ ^[0-9]+$ ]]; then | |
outdated_count=0 | |
fi | |
# Check for security vulnerabilities | |
audit_output=$(npm audit --json 2>/dev/null || echo '{"metadata":{"vulnerabilities":{}}}') | |
high_vulns=$(echo "$audit_output" | jq '.metadata.vulnerabilities.high // 0') | |
critical_vulns=$(echo "$audit_output" | jq '.metadata.vulnerabilities.critical // 0') | |
# Validate vulnerability counts are numbers | |
if ! [[ $high_vulns =~ ^[0-9]+$ ]]; then | |
high_vulns=0 | |
fi | |
if ! [[ $critical_vulns =~ ^[0-9]+$ ]]; then | |
critical_vulns=0 | |
fi | |
# Determine health status | |
if [[ $critical_vulns -gt 0 ]]; then | |
status="critical" | |
elif [[ $high_vulns -gt 0 ]]; then | |
status="warning" | |
elif [[ $outdated_count -gt 10 ]]; then | |
status="warning" | |
else | |
status="healthy" | |
fi | |
echo "status=$status" >> $GITHUB_OUTPUT | |
echo "outdated=$outdated_count" >> $GITHUB_OUTPUT | |
echo "📊 Dependency Health:" | |
echo "- Status: $status" | |
echo "- Outdated packages: $outdated_count" | |
echo "- High vulnerabilities: $high_vulns" | |
echo "- Critical vulnerabilities: $critical_vulns" | |
# Documentation health | |
documentation-health: | |
name: 📚 Documentation Health | |
runs-on: ubuntu-latest | |
outputs: | |
coverage-score: ${{ steps.docs.outputs.score }} | |
missing-docs: ${{ steps.docs.outputs.missing }} | |
steps: | |
- name: Checkout code | |
uses: actions/checkout@v4 | |
- name: Check documentation health | |
id: docs | |
run: | | |
echo "📚 Checking documentation health..." | |
# Required documentation files | |
required_docs=( | |
"README.md" | |
"CHANGELOG.md" | |
"docs/website-overview.md" | |
"docs/security-architecture.md" | |
"docs/design-system.md" | |
"docs/brand-guidelines.md" | |
"docs/company-info.md" | |
) | |
missing_docs=() | |
present_docs=0 | |
for doc in "${required_docs[@]}"; do | |
if [[ -f "$doc" ]]; then | |
present_docs=$((present_docs + 1)) | |
else | |
missing_docs+=("$doc") | |
fi | |
done | |
# Calculate coverage score | |
total_docs=${#required_docs[@]} | |
coverage_score=$(( (present_docs * 100) / total_docs )) | |
# Check README quality | |
readme_size=$(wc -c < README.md) | |
if [[ $readme_size -lt 1000 ]]; then | |
coverage_score=$((coverage_score - 10)) | |
echo "⚠️ README is too short" | |
fi | |
echo "score=$coverage_score" >> $GITHUB_OUTPUT | |
echo "missing=${missing_docs[*]}" >> $GITHUB_OUTPUT | |
echo "📊 Documentation Health:" | |
echo "- Coverage Score: $coverage_score/100" | |
echo "- Present: $present_docs/$total_docs" | |
echo "- Missing: ${missing_docs[*]}" | |
# Update repository badges and status | |
update-status: | |
name: 🏷️ Update Repository Status | |
runs-on: ubuntu-latest | |
needs: | |
[ | |
health-check, | |
performance-monitor, | |
dependency-health, | |
documentation-health, | |
] | |
if: always() && github.ref == 'refs/heads/main' | |
steps: | |
- name: Checkout code | |
uses: actions/checkout@v4 | |
with: | |
token: ${{ secrets.GITHUB_TOKEN }} | |
- name: Update status badges | |
run: | | |
echo "🏷️ Updating repository status badges..." | |
# Get status values | |
health_score="${{ needs.health-check.outputs.health-score }}" | |
perf_score="${{ needs.performance-monitor.outputs.performance-score }}" | |
deps_status="${{ needs.dependency-health.outputs.health-status }}" | |
docs_score="${{ needs.documentation-health.outputs.coverage-score }}" | |
# Determine colors based on scores/status | |
get_color() { | |
case "$1" in | |
healthy|passed) echo "brightgreen" ;; | |
warning) echo "yellow" ;; | |
critical|failed) echo "red" ;; | |
*) | |
if [[ "$1" =~ ^[0-9]+$ ]]; then | |
if [[ $1 -ge 80 ]]; then echo "brightgreen" | |
elif [[ $1 -ge 60 ]]; then echo "yellow" | |
else echo "red" | |
fi | |
else | |
echo "lightgrey" | |
fi | |
;; | |
esac | |
} | |
health_color=$(get_color "$health_score") | |
perf_color=$(get_color "$perf_score") | |
deps_color=$(get_color "$deps_status") | |
docs_color=$(get_color "$docs_score") | |
# Create new badge section | |
cat > temp_status_badges.md << EOF | |
### 📊 Repository Status | |
[](https://github.com/${{ github.repository }}/actions) | |
[](https://github.com/${{ github.repository }}/actions) | |
[](https://github.com/${{ github.repository }}/actions) | |
[](./docs/) | |
EOF | |
# Note: README.md updates disabled to prevent conflicts with quality-security-checks workflow | |
# Repository status badges are now managed in quality-security-checks.yml | |
# rm temp_status_badges.md | |
- name: Generate repository health report | |
run: | | |
echo "📊 Generating repository health report..." | |
mkdir -p reports/automated | |
# Create health report | |
cat > reports/automated/health-report.md << EOF | |
# 📊 Repository Health Report | |
**Generated:** $(date -u) | |
**Repository:** ${{ github.repository }} | |
## 🏥 Overall Health Score: ${{ needs.health-check.outputs.health-score }}/100 | |
### 📈 Metrics Summary | |
| Category | Score/Status | Details | | |
|----------|--------------|---------| | |
| **Repository Health** | ${{ needs.health-check.outputs.health-score }}/100 | Overall codebase health | | |
| **Performance** | ${{ needs.performance-monitor.outputs.performance-score }}/100 | Bundle size and optimization | | |
| **Dependencies** | ${{ needs.dependency-health.outputs.health-status }} | Package health and security | | |
| **Documentation** | ${{ needs.documentation-health.outputs.coverage-score }}/100 | Documentation coverage | | |
### 🔍 Detailed Metrics | |
**Repository Metrics:** | |
\`\`\`json | |
${{ needs.health-check.outputs.metrics }} | |
\`\`\` | |
**Performance Metrics:** | |
\`\`\`json | |
${{ needs.performance-monitor.outputs.metrics }} | |
\`\`\` | |
### 📋 Action Items | |
EOF | |
# Add action items based on status | |
if [[ "${{ needs.dependency-health.outputs.health-status }}" != "healthy" ]]; then | |
echo "- 📦 Address dependency issues (outdated: ${{ needs.dependency-health.outputs.outdated-count }})" >> reports/automated/health-report.md | |
fi | |
if [[ "${{ needs.documentation-health.outputs.coverage-score }}" -lt 90 ]]; then | |
echo "- 📚 Improve documentation coverage (missing: ${{ needs.documentation-health.outputs.missing-docs }})" >> reports/automated/health-report.md | |
fi | |
if [[ "${{ needs.performance-monitor.outputs.performance-score }}" -lt 80 ]]; then | |
echo "- ⚡ Optimize performance (bundle size, code splitting)" >> reports/automated/health-report.md | |
fi | |
echo "" >> reports/automated/health-report.md | |
echo "### 🔗 Quick Links" >> reports/automated/health-report.md | |
echo "- [Security Architecture](./docs/security-architecture.md)" >> reports/automated/health-report.md | |
echo "- [Website Overview](./docs/website-overview.md)" >> reports/automated/health-report.md | |
echo "- [GitHub Actions](https://github.com/${{ github.repository }}/actions)" >> reports/automated/health-report.md | |
- name: Commit status updates | |
run: | | |
git config --local user.email "[email protected]" | |
git config --local user.name "GitHub Action" | |
if git diff --quiet; then | |
echo "No status updates needed" | |
else | |
# Check if there was already a health update today | |
last_health_commit=$(git log --since="24 hours ago" --grep="Update repository health status" --oneline | head -1) | |
if [ -z "$last_health_commit" ]; then | |
echo "Creating daily health report update" | |
git add reports/ | |
git add -f reports/automated/health-report.md | |
git commit -m "📊 Update repository health status and metrics [skip ci]" | |
git push | |
else | |
echo "Health report already updated today, skipping commit to reduce git history pollution" | |
fi | |
fi | |
- name: Create summary | |
run: | | |
echo "## 📊 Repository Health Monitor Summary" >> $GITHUB_STEP_SUMMARY | |
echo "" >> $GITHUB_STEP_SUMMARY | |
echo "### 🏥 Health Scores" >> $GITHUB_STEP_SUMMARY | |
echo "| Metric | Score | Status |" >> $GITHUB_STEP_SUMMARY | |
echo "|--------|-------|--------|" >> $GITHUB_STEP_SUMMARY | |
echo "| Repository Health | ${{ needs.health-check.outputs.health-score }}/100 | $([ ${{ needs.health-check.outputs.health-score }} -ge 80 ] && echo '✅ Good' || echo '⚠️ Needs Attention') |" >> $GITHUB_STEP_SUMMARY | |
echo "| Performance | ${{ needs.performance-monitor.outputs.performance-score }}/100 | $([ ${{ needs.performance-monitor.outputs.performance-score }} -ge 80 ] && echo '✅ Good' || echo '⚠️ Needs Attention') |" >> $GITHUB_STEP_SUMMARY | |
echo "| Dependencies | ${{ needs.dependency-health.outputs.health-status }} | $([ '${{ needs.dependency-health.outputs.health-status }}' = 'healthy' ] && echo '✅ Healthy' || echo '⚠️ Issues Found') |" >> $GITHUB_STEP_SUMMARY | |
echo "| Documentation | ${{ needs.documentation-health.outputs.coverage-score }}/100 | $([ ${{ needs.documentation-health.outputs.coverage-score }} -ge 90 ] && echo '✅ Complete' || echo '⚠️ Incomplete') |" >> $GITHUB_STEP_SUMMARY | |
echo "" >> $GITHUB_STEP_SUMMARY | |
echo "### 🔗 Resources" >> $GITHUB_STEP_SUMMARY | |
echo "- [Health Report](./reports/automated/health-report.md)" >> $GITHUB_STEP_SUMMARY | |
echo "- [Live Website](https://www.thinkred.tech)" >> $GITHUB_STEP_SUMMARY | |
echo "- [Documentation](./docs/)" >> $GITHUB_STEP_SUMMARY |