@@ -17,11 +17,13 @@ jobs:
1717 coverage :
1818 name : Test Coverage Report
1919 runs-on : ubuntu-latest
20- timeout-minutes : 10
20+ timeout-minutes : 15
2121
2222 steps :
2323 - name : Checkout repository
2424 uses : actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
25+ with :
26+ fetch-depth : 0
2527
2628 - name : Setup Node.js
2729 uses : actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -35,21 +37,65 @@ jobs:
3537 - name : Build project
3638 run : npm run build
3739
38- - name : Run tests with coverage
40+ - name : Run tests with coverage (PR branch)
3941 run : npm run test:coverage
4042
41- - name : Generate coverage summary
43+ - name : Save PR coverage
44+ run : cp coverage/coverage-summary.json /tmp/pr-coverage-summary.json
45+
46+ - name : Get base branch coverage (PR only)
47+ if : github.event_name == 'pull_request'
48+ id : base_coverage
49+ run : |
50+ # Save the current commit
51+ PR_COMMIT=$(git rev-parse HEAD)
52+
53+ # Checkout base branch
54+ git checkout ${{ github.event.pull_request.base.sha }}
55+
56+ # Install dependencies and build for base branch
57+ npm ci
58+ npm run build
59+
60+ # Run coverage on base branch
61+ npm run test:coverage || true
62+
63+ # Save base coverage
64+ if [ -f coverage/coverage-summary.json ]; then
65+ cp coverage/coverage-summary.json /tmp/base-coverage-summary.json
66+ echo "base_coverage_exists=true" >> $GITHUB_OUTPUT
67+ else
68+ echo "base_coverage_exists=false" >> $GITHUB_OUTPUT
69+ fi
70+
71+ # Checkout back to PR commit
72+ git checkout $PR_COMMIT
73+
74+ # Reinstall PR dependencies
75+ npm ci
76+
77+ - name : Compare coverage (PR only)
78+ if : github.event_name == 'pull_request' && steps.base_coverage.outputs.base_coverage_exists == 'true'
79+ id : compare
80+ run : |
81+ npx tsx scripts/ci/compare-coverage.ts \
82+ /tmp/pr-coverage-summary.json \
83+ /tmp/base-coverage-summary.json
84+ continue-on-error : true
85+
86+ - name : Generate coverage summary (push to main)
87+ if : github.event_name == 'push'
4288 id : coverage
4389 run : |
4490 # Read the coverage summary
4591 COVERAGE_JSON=$(cat coverage/coverage-summary.json)
46-
92+
4793 # Extract metrics using jq
4894 LINES_PCT=$(echo "$COVERAGE_JSON" | jq -r '.total.lines.pct')
4995 STATEMENTS_PCT=$(echo "$COVERAGE_JSON" | jq -r '.total.statements.pct')
5096 FUNCTIONS_PCT=$(echo "$COVERAGE_JSON" | jq -r '.total.functions.pct')
5197 BRANCHES_PCT=$(echo "$COVERAGE_JSON" | jq -r '.total.branches.pct')
52-
98+
5399 LINES_COVERED=$(echo "$COVERAGE_JSON" | jq -r '.total.lines.covered')
54100 LINES_TOTAL=$(echo "$COVERAGE_JSON" | jq -r '.total.lines.total')
55101 STATEMENTS_COVERED=$(echo "$COVERAGE_JSON" | jq -r '.total.statements.covered')
58104 FUNCTIONS_TOTAL=$(echo "$COVERAGE_JSON" | jq -r '.total.functions.total')
59105 BRANCHES_COVERED=$(echo "$COVERAGE_JSON" | jq -r '.total.branches.covered')
60106 BRANCHES_TOTAL=$(echo "$COVERAGE_JSON" | jq -r '.total.branches.total')
61-
107+
62108 # Create summary for GitHub Actions Summary
63109 echo "## Test Coverage Report" >> $GITHUB_STEP_SUMMARY
64110 echo "" >> $GITHUB_STEP_SUMMARY
@@ -69,62 +115,56 @@ jobs:
69115 echo "| **Functions** | ${FUNCTIONS_PCT}% | ${FUNCTIONS_COVERED}/${FUNCTIONS_TOTAL} |" >> $GITHUB_STEP_SUMMARY
70116 echo "| **Branches** | ${BRANCHES_PCT}% | ${BRANCHES_COVERED}/${BRANCHES_TOTAL} |" >> $GITHUB_STEP_SUMMARY
71117 echo "" >> $GITHUB_STEP_SUMMARY
72-
73- # Create PR comment body
74- COMMENT_BODY="## Test Coverage Report
75-
76- | Metric | Coverage | Covered/Total |
77- |--------|----------|---------------|
78- | **Lines** | ${LINES_PCT}% | ${LINES_COVERED}/${LINES_TOTAL} |
79- | **Statements** | ${STATEMENTS_PCT}% | ${STATEMENTS_COVERED}/${STATEMENTS_TOTAL} |
80- | **Functions** | ${FUNCTIONS_PCT}% | ${FUNCTIONS_COVERED}/${FUNCTIONS_TOTAL} |
81- | **Branches** | ${BRANCHES_PCT}% | ${BRANCHES_COVERED}/${BRANCHES_TOTAL} |
82-
83- <details>
84- <summary>Coverage Thresholds</summary>
85-
86- The project has the following coverage thresholds configured:
87- - Lines: 38%
88- - Statements: 38%
89- - Functions: 35%
90- - Branches: 30%
91-
92- </details>
93-
94- ---
95- *Coverage report generated by \\\`npm run test:coverage\\\`*"
96-
97- # Save for next step (escape newlines for GitHub Actions)
98- echo "COMMENT_BODY<<EOF" >> $GITHUB_ENV
99- echo "$COMMENT_BODY" >> $GITHUB_ENV
100- echo "EOF" >> $GITHUB_ENV
101-
118+
102119 # Also save individual metrics as outputs
103120 echo "lines_pct=${LINES_PCT}" >> $GITHUB_OUTPUT
104121 echo "statements_pct=${STATEMENTS_PCT}" >> $GITHUB_OUTPUT
105122 echo "functions_pct=${FUNCTIONS_PCT}" >> $GITHUB_OUTPUT
106123 echo "branches_pct=${BRANCHES_PCT}" >> $GITHUB_OUTPUT
107124
108- - name : Comment PR with coverage report
125+ - name : Comment PR with coverage comparison
109126 if : github.event_name == 'pull_request'
110127 uses : actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
111128 with :
112129 github-token : ${{ secrets.GITHUB_TOKEN }}
113130 script : |
114- const commentBody = process.env.COMMENT_BODY;
115-
131+ const fs = require('fs');
132+
133+ // Try to read the coverage report from compare step
134+ let commentBody = process.env.COVERAGE_REPORT;
135+
136+ // If no comparison report, generate a simple report
137+ if (!commentBody) {
138+ const prCoverage = JSON.parse(fs.readFileSync('/tmp/pr-coverage-summary.json', 'utf8'));
139+ const total = prCoverage.total;
140+
141+ commentBody = `## 📊 Test Coverage Report
142+
143+ | Metric | Coverage |
144+ |--------|----------|
145+ | Lines | ${total.lines.pct.toFixed(2)}% |
146+ | Statements | ${total.statements.pct.toFixed(2)}% |
147+ | Functions | ${total.functions.pct.toFixed(2)}% |
148+ | Branches | ${total.branches.pct.toFixed(2)}% |
149+
150+ > ℹ️ Base branch coverage not available for comparison.
151+
152+ ---
153+ *Coverage report generated by \`npm run test:coverage\`*`;
154+ }
155+
116156 // Find existing coverage comment
117157 const { data: comments } = await github.rest.issues.listComments({
118158 owner: context.repo.owner,
119159 repo: context.repo.repo,
120160 issue_number: context.issue.number,
121161 });
122-
123- const botComment = comments.find(comment =>
124- comment.user.type === 'Bot' &&
125- comment.body.includes('Test Coverage Report')
162+
163+ const botComment = comments.find(comment =>
164+ comment.user.type === 'Bot' &&
165+ ( comment.body.includes('Test Coverage Report') || comment.body.includes('Coverage Check') )
126166 );
127-
167+
128168 if (botComment) {
129169 // Update existing comment
130170 await github.rest.issues.updateComment({
@@ -151,8 +191,11 @@ jobs:
151191 coverage/
152192 retention-days : 30
153193
154- - name : Check coverage thresholds
194+ - name : Fail on coverage regression
195+ if : github.event_name == 'pull_request' && steps.compare.outcome == 'failure'
155196 run : |
156- echo "Checking if coverage meets minimum thresholds..."
157- # Jest will fail if coverage is below thresholds defined in jest.config.js
158- # This step is informational since the test:coverage command already checks
197+ echo "❌ Coverage regression detected!"
198+ echo "This PR decreases overall test coverage. Please add tests to maintain coverage levels."
199+ echo ""
200+ echo "See the PR comment above for detailed coverage comparison."
201+ exit 1
0 commit comments