Created
December 18, 2025 17:34
-
-
Save vivekhaldar/004d476406e1f8e9cd83e10003dc3f05 to your computer and use it in GitHub Desktop.
Continuous Test Coverage Setup Guide for Monorepos
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
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Continuous Test Coverage for Monorepos</title> | |
| <style> | |
| * { box-sizing: border-box; } | |
| body { | |
| font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| max-width: 900px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| line-height: 1.7; | |
| color: #1a1a1a; | |
| background: #fafafa; | |
| } | |
| h1 { | |
| color: #0066cc; | |
| border-bottom: 3px solid #0066cc; | |
| padding-bottom: 10px; | |
| } | |
| h2 { | |
| color: #333; | |
| margin-top: 40px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| h3 { | |
| color: #555; | |
| margin-top: 25px; | |
| } | |
| .subtitle { | |
| color: #666; | |
| font-size: 1.1em; | |
| margin-top: -10px; | |
| } | |
| .architecture { | |
| background: #1e1e1e; | |
| color: #d4d4d4; | |
| padding: 20px; | |
| border-radius: 8px; | |
| font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | |
| font-size: 13px; | |
| overflow-x: auto; | |
| white-space: pre; | |
| line-height: 1.4; | |
| } | |
| pre { | |
| background: #282c34; | |
| color: #abb2bf; | |
| padding: 16px; | |
| border-radius: 8px; | |
| overflow-x: auto; | |
| font-size: 13px; | |
| line-height: 1.5; | |
| } | |
| code { | |
| background: #e8e8e8; | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-size: 0.9em; | |
| } | |
| pre code { | |
| background: none; | |
| padding: 0; | |
| } | |
| .option-card { | |
| background: white; | |
| border: 1px solid #ddd; | |
| border-radius: 12px; | |
| padding: 25px; | |
| margin: 20px 0; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.05); | |
| } | |
| .option-card h3 { | |
| margin-top: 0; | |
| color: #0066cc; | |
| } | |
| .badge { | |
| display: inline-block; | |
| padding: 4px 12px; | |
| border-radius: 20px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| } | |
| .badge-simple { background: #d4edda; color: #155724; } | |
| .badge-detailed { background: #cce5ff; color: #004085; } | |
| .pros-cons { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 15px; | |
| margin: 15px 0; | |
| } | |
| .pros, .cons { | |
| padding: 15px; | |
| border-radius: 8px; | |
| } | |
| .pros { background: #e8f5e9; } | |
| .cons { background: #fff3e0; } | |
| .pros h4 { color: #2e7d32; margin-top: 0; } | |
| .cons h4 { color: #e65100; margin-top: 0; } | |
| .pros ul, .cons ul { | |
| margin: 0; | |
| padding-left: 20px; | |
| } | |
| .step { | |
| display: flex; | |
| gap: 15px; | |
| margin: 20px 0; | |
| } | |
| .step-number { | |
| background: #0066cc; | |
| color: white; | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: bold; | |
| flex-shrink: 0; | |
| } | |
| .step-content { | |
| flex: 1; | |
| } | |
| .step-content h4 { | |
| margin: 0 0 8px 0; | |
| color: #333; | |
| } | |
| .step-content p { | |
| margin: 0; | |
| color: #666; | |
| } | |
| .comparison-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin: 20px 0; | |
| background: white; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.05); | |
| } | |
| .comparison-table th { | |
| background: #0066cc; | |
| color: white; | |
| padding: 12px 15px; | |
| text-align: left; | |
| } | |
| .comparison-table td { | |
| padding: 12px 15px; | |
| border-bottom: 1px solid #eee; | |
| } | |
| .comparison-table tr:last-child td { | |
| border-bottom: none; | |
| } | |
| .comparison-table tr:hover td { | |
| background: #f8f9fa; | |
| } | |
| .highlight-box { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 20px 25px; | |
| border-radius: 12px; | |
| margin: 30px 0; | |
| } | |
| .highlight-box h3 { | |
| color: white; | |
| margin-top: 0; | |
| } | |
| .file-label { | |
| background: #6c757d; | |
| color: white; | |
| padding: 4px 10px; | |
| border-radius: 4px 4px 0 0; | |
| font-size: 12px; | |
| font-family: monospace; | |
| display: inline-block; | |
| margin-bottom: -8px; | |
| } | |
| .copy-btn { | |
| float: right; | |
| background: #495057; | |
| color: white; | |
| border: none; | |
| padding: 4px 10px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 12px; | |
| } | |
| .copy-btn:hover { | |
| background: #343a40; | |
| } | |
| footer { | |
| margin-top: 50px; | |
| padding-top: 20px; | |
| border-top: 1px solid #ddd; | |
| color: #666; | |
| font-size: 14px; | |
| text-align: center; | |
| } | |
| @media (max-width: 600px) { | |
| .pros-cons { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>📊 Continuous Test Coverage for Monorepos</h1> | |
| <p class="subtitle">A self-hosted approach using GitHub Actions + Gist (no external services required)</p> | |
| <h2>🏗️ The Architecture</h2> | |
| <div class="architecture">┌─────────────────────────────────────────────────────────┐ | |
| │ GitHub Actions │ | |
| │ ┌─────────────────┐ ┌─────────────────┐ │ | |
| │ │ JS Tests │ │ Python Tests │ │ | |
| │ │ (vitest/jest) │ │ (pytest-cov) │ │ | |
| │ │ → coverage.xml │ │ → coverage.xml │ │ | |
| │ └────────┬────────┘ └────────┬────────┘ │ | |
| │ │ │ │ | |
| │ └──────────┬───────────────┘ │ | |
| │ ▼ │ | |
| │ Extract % → Update Gist/Pages │ | |
| └─────────────────────────────────────────────────────────┘ | |
| │ | |
| ┌──────────────┴──────────────┐ | |
| ▼ ▼ | |
| ┌───────────────┐ ┌─────────────────┐ | |
| │ Gist Badge │ │ GitHub Pages │ | |
| │ (shields.io) │ │ (HTML reports) │ | |
| └───────────────┘ └─────────────────┘</div> | |
| <h2>🎯 Two Options</h2> | |
| <div class="option-card"> | |
| <span class="badge badge-simple">Simplest</span> | |
| <h3>Option 1: Gist + Shields.io Badge</h3> | |
| <p>Push coverage percentages to a gist, display via shields.io dynamic badge. Great when you just need a quick overview.</p> | |
| <div class="pros-cons"> | |
| <div class="pros"> | |
| <h4>✓ Pros</h4> | |
| <ul> | |
| <li>Minimal setup</li> | |
| <li>Nice badge for README</li> | |
| <li>No extra repos/branches</li> | |
| </ul> | |
| </div> | |
| <div class="cons"> | |
| <h4>✗ Cons</h4> | |
| <ul> | |
| <li>Just percentages, no details</li> | |
| <li>Can't drill into uncovered lines</li> | |
| </ul> | |
| </div> | |
| </div> | |
| <h4>Setup Steps</h4> | |
| <div class="step"> | |
| <div class="step-number">1</div> | |
| <div class="step-content"> | |
| <h4>Create a Gist</h4> | |
| <p>Go to <a href="https://gist.github.com">gist.github.com</a> and create a new gist (can be empty). Note the gist ID from the URL.</p> | |
| </div> | |
| </div> | |
| <div class="step"> | |
| <div class="step-number">2</div> | |
| <div class="step-content"> | |
| <h4>Create a Personal Access Token</h4> | |
| <p>GitHub Settings → Developer Settings → Personal Access Tokens → Generate with <code>gist</code> scope.</p> | |
| </div> | |
| </div> | |
| <div class="step"> | |
| <div class="step-number">3</div> | |
| <div class="step-content"> | |
| <h4>Add Secret to Repo</h4> | |
| <p>Repo Settings → Secrets → Actions → New secret named <code>GIST_TOKEN</code>.</p> | |
| </div> | |
| </div> | |
| <div class="step"> | |
| <div class="step-number">4</div> | |
| <div class="step-content"> | |
| <h4>Add Workflow</h4> | |
| <p>Create <code>.github/workflows/coverage.yml</code> (see below).</p> | |
| </div> | |
| </div> | |
| <div class="file-label">.github/workflows/coverage.yml</div> | |
| <pre><code>name: Coverage | |
| on: | |
| push: | |
| branches: [main] | |
| jobs: | |
| coverage: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # Backend (Python) | |
| - name: Backend coverage | |
| working-directory: ./backend | |
| run: | | |
| pip install pytest pytest-cov | |
| pytest --cov=src --cov-report=json | |
| # Frontend (JavaScript) | |
| - name: Frontend coverage | |
| working-directory: ./frontend | |
| run: | | |
| npm ci | |
| npm run test:coverage | |
| # Update Gist | |
| - name: Update coverage gist | |
| env: | |
| GH_TOKEN: ${{ secrets.GIST_TOKEN }} | |
| run: | | |
| # Extract coverage percentages | |
| PY_COV=$(jq '.totals.percent_covered | floor' backend/coverage.json) | |
| JS_COV=$(jq '.total.lines.pct | floor' frontend/coverage/coverage-summary.json) | |
| # Create shields.io endpoint JSON | |
| cat > coverage.json << EOF | |
| { | |
| "schemaVersion": 1, | |
| "label": "coverage", | |
| "message": "py:${PY_COV}% js:${JS_COV}%", | |
| "color": "$([ $PY_COV -ge 80 ] && [ $JS_COV -ge 80 ] && echo 'green' || echo 'yellow')" | |
| } | |
| EOF | |
| # Update gist | |
| gh gist edit YOUR_GIST_ID --add coverage.json</code></pre> | |
| <h4>README Badge</h4> | |
| <pre><code></code></pre> | |
| </div> | |
| <div class="option-card"> | |
| <span class="badge badge-detailed">More Detailed</span> | |
| <h3>Option 2: GitHub Pages (Full HTML Reports)</h3> | |
| <p>Push full HTML coverage reports to GitHub Pages. Browse line-by-line coverage directly in your browser.</p> | |
| <div class="pros-cons"> | |
| <div class="pros"> | |
| <h4>✓ Pros</h4> | |
| <ul> | |
| <li>Full detailed reports</li> | |
| <li>See exactly which lines are covered</li> | |
| <li>All within your repo (no external deps)</li> | |
| </ul> | |
| </div> | |
| <div class="cons"> | |
| <h4>✗ Cons</h4> | |
| <ul> | |
| <li>Uses gh-pages branch</li> | |
| <li>Slightly more complex workflow</li> | |
| </ul> | |
| </div> | |
| </div> | |
| <div class="file-label">.github/workflows/coverage.yml</div> | |
| <pre><code>name: Coverage | |
| on: | |
| push: | |
| branches: [main] | |
| jobs: | |
| coverage: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Backend coverage | |
| working-directory: ./backend | |
| run: | | |
| pip install pytest pytest-cov | |
| pytest --cov=src --cov-report=html:coverage-html | |
| - name: Frontend coverage | |
| working-directory: ./frontend | |
| run: | | |
| npm ci | |
| npm run test:coverage | |
| - name: Prepare pages | |
| run: | | |
| mkdir -p public | |
| cp -r backend/coverage-html public/backend | |
| cp -r frontend/coverage/lcov-report public/frontend | |
| cat > public/index.html << 'EOF' | |
| <!DOCTYPE html> | |
| <html> | |
| <head><title>Coverage Reports</title></head> | |
| <body> | |
| <h1>Coverage Reports</h1> | |
| <ul> | |
| <li><a href="backend/">Backend (Python)</a></li> | |
| <li><a href="frontend/">Frontend (JS)</a></li> | |
| </ul> | |
| </body> | |
| </html> | |
| EOF | |
| - name: Deploy to GitHub Pages | |
| uses: peaceiris/actions-gh-pages@v4 | |
| with: | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| publish_dir: ./public</code></pre> | |
| <p>Enable GitHub Pages in repo settings (source: <code>gh-pages</code> branch). Reports live at:</p> | |
| <pre><code>https://YOUR_ORG.github.io/YOUR_REPO/</code></pre> | |
| </div> | |
| <h2>📋 Quick Comparison</h2> | |
| <table class="comparison-table"> | |
| <tr> | |
| <th>Aspect</th> | |
| <th>Gist + Badge</th> | |
| <th>GitHub Pages</th> | |
| </tr> | |
| <tr> | |
| <td>Setup complexity</td> | |
| <td>⭐ Simple</td> | |
| <td>⭐⭐ Medium</td> | |
| </tr> | |
| <tr> | |
| <td>Detail level</td> | |
| <td>Percentages only</td> | |
| <td>Line-by-line</td> | |
| </tr> | |
| <tr> | |
| <td>External dependencies</td> | |
| <td>shields.io (for badge)</td> | |
| <td>None</td> | |
| </tr> | |
| <tr> | |
| <td>Best for</td> | |
| <td>Quick status check</td> | |
| <td>Investigating gaps</td> | |
| </tr> | |
| <tr> | |
| <td>Storage</td> | |
| <td>~1KB gist</td> | |
| <td>~1-5MB per update</td> | |
| </tr> | |
| </table> | |
| <div class="highlight-box"> | |
| <h3>💡 Recommendation</h3> | |
| <p>Use <strong>GitHub Pages</strong> if you actually want to investigate coverage gaps—the HTML reports are genuinely useful for that.</p> | |
| <p>Use <strong>Gist + Badge</strong> if you just want a quick visual indicator in your README and don't need details.</p> | |
| <p>You can also combine both: use GitHub Pages for reports and add a badge that links to them!</p> | |
| </div> | |
| <h2>🔧 Coverage Tool Setup</h2> | |
| <h3>Python (pytest-cov)</h3> | |
| <pre><code># Install | |
| pip install pytest-cov | |
| # Run with JSON output (for badge) | |
| pytest --cov=your_package --cov-report=json | |
| # Run with HTML output (for pages) | |
| pytest --cov=your_package --cov-report=html</code></pre> | |
| <h3>JavaScript (Jest)</h3> | |
| <pre><code>// package.json | |
| { | |
| "scripts": { | |
| "test:coverage": "jest --coverage --coverageReporters=json-summary --coverageReporters=lcov" | |
| } | |
| } | |
| // jest.config.js | |
| module.exports = { | |
| collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'], | |
| coverageDirectory: 'coverage' | |
| };</code></pre> | |
| <h3>JavaScript (Vitest)</h3> | |
| <pre><code>// vite.config.js | |
| export default { | |
| test: { | |
| coverage: { | |
| reporter: ['json-summary', 'lcov', 'html'], | |
| reportsDirectory: './coverage' | |
| } | |
| } | |
| }</code></pre> | |
| <footer> | |
| <p>Generated by Claude • No external services required • Just GitHub!</p> | |
| </footer> | |
| <script> | |
| // Add copy functionality to code blocks | |
| document.querySelectorAll('pre').forEach(pre => { | |
| const btn = document.createElement('button'); | |
| btn.className = 'copy-btn'; | |
| btn.textContent = 'Copy'; | |
| btn.onclick = async () => { | |
| const code = pre.querySelector('code')?.textContent || pre.textContent; | |
| await navigator.clipboard.writeText(code); | |
| btn.textContent = 'Copied!'; | |
| setTimeout(() => btn.textContent = 'Copy', 2000); | |
| }; | |
| pre.style.position = 'relative'; | |
| pre.insertBefore(btn, pre.firstChild); | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment