Skip to content

Commit ce12e3b

Browse files
authored
Merge pull request #351 from DialmasterOrg/347-fix-github-workflows-for-contributors
fix(ci): move coverage comment to separate workflow for fork PR support
2 parents be3f89e + 04658f5 commit ce12e3b

File tree

2 files changed

+141
-147
lines changed

2 files changed

+141
-147
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ on:
66

77
permissions:
88
contents: read
9-
pull-requests: write
10-
checks: write
9+
actions: write
1110

1211
jobs:
1312
lint:
@@ -179,150 +178,6 @@ jobs:
179178
client/coverage/coverage-summary.json
180179
client/coverage/lcov.info
181180
182-
coverage-comment:
183-
name: Post Coverage Comment
184-
runs-on: ubuntu-latest
185-
needs: [test-backend, test-frontend]
186-
if: github.event_name == 'pull_request' && always()
187-
188-
steps:
189-
- name: Checkout code
190-
uses: actions/checkout@v3
191-
192-
- name: Download backend coverage
193-
uses: actions/download-artifact@v4
194-
with:
195-
name: backend-coverage
196-
path: backend-coverage
197-
continue-on-error: true
198-
199-
- name: Download frontend coverage
200-
uses: actions/download-artifact@v4
201-
with:
202-
name: frontend-coverage
203-
path: frontend-coverage
204-
continue-on-error: true
205-
206-
- name: Generate Coverage Report Comment
207-
id: coverage
208-
uses: actions/github-script@v6
209-
with:
210-
github-token: ${{ secrets.GITHUB_TOKEN }}
211-
script: |
212-
const fs = require('fs');
213-
214-
// Read coverage summaries
215-
let backendCoverage = { total: { lines: { pct: 0 }, statements: { pct: 0 }, functions: { pct: 0 }, branches: { pct: 0 } } };
216-
let frontendCoverage = { total: { lines: { pct: 0 }, statements: { pct: 0 }, functions: { pct: 0 }, branches: { pct: 0 } } };
217-
let backendAvailable = false;
218-
let frontendAvailable = false;
219-
220-
try {
221-
const backendSummary = fs.readFileSync('backend-coverage/coverage-summary.json', 'utf8');
222-
backendCoverage = JSON.parse(backendSummary);
223-
backendAvailable = true;
224-
} catch (e) {
225-
console.log('Could not read backend coverage:', e);
226-
}
227-
228-
try {
229-
const frontendSummary = fs.readFileSync('frontend-coverage/coverage-summary.json', 'utf8');
230-
frontendCoverage = JSON.parse(frontendSummary);
231-
frontendAvailable = true;
232-
} catch (e) {
233-
console.log('Could not read frontend coverage:', e);
234-
}
235-
236-
// Format coverage badge color
237-
function getBadgeColor(percentage) {
238-
if (percentage >= 80) return '🟢';
239-
if (percentage >= 70) return '🟡';
240-
return '🔴';
241-
}
242-
243-
// Format percentage
244-
function formatPct(value) {
245-
return typeof value === 'number' ? value.toFixed(2) : '0.00';
246-
}
247-
248-
// Create comment body
249-
const backendTotal = backendCoverage.total || backendCoverage;
250-
const frontendTotal = frontendCoverage.total || frontendCoverage;
251-
252-
let comment = `## 📊 Test Coverage Report\n\n`;
253-
254-
if (backendAvailable) {
255-
comment += `### Backend Coverage
256-
| Type | Coverage | Status |
257-
|------|----------|--------|
258-
| Lines | ${formatPct(backendTotal.lines.pct)}% | ${getBadgeColor(backendTotal.lines.pct)} |
259-
| Statements | ${formatPct(backendTotal.statements.pct)}% | ${getBadgeColor(backendTotal.statements.pct)} |
260-
| Functions | ${formatPct(backendTotal.functions.pct)}% | ${getBadgeColor(backendTotal.functions.pct)} |
261-
| Branches | ${formatPct(backendTotal.branches.pct)}% | ${getBadgeColor(backendTotal.branches.pct)} |
262-
263-
`;
264-
} else {
265-
comment += `### Backend Coverage\n⚠️ Coverage data not available (tests may have failed)\n\n`;
266-
}
267-
268-
if (frontendAvailable) {
269-
comment += `### Frontend Coverage
270-
| Type | Coverage | Status |
271-
|------|----------|--------|
272-
| Lines | ${formatPct(frontendTotal.lines.pct)}% | ${getBadgeColor(frontendTotal.lines.pct)} |
273-
| Statements | ${formatPct(frontendTotal.statements.pct)}% | ${getBadgeColor(frontendTotal.statements.pct)} |
274-
| Functions | ${formatPct(frontendTotal.functions.pct)}% | ${getBadgeColor(frontendTotal.functions.pct)} |
275-
| Branches | ${formatPct(frontendTotal.branches.pct)}% | ${getBadgeColor(frontendTotal.branches.pct)} |
276-
277-
`;
278-
} else {
279-
comment += `### Frontend Coverage\n⚠️ Coverage data not available (tests may have failed)\n\n`;
280-
}
281-
282-
// Check if thresholds are met
283-
const backendMeetsThreshold = backendAvailable && backendTotal.lines.pct >= 70;
284-
const frontendMeetsThreshold = frontendAvailable && frontendTotal.lines.pct >= 70;
285-
286-
comment += `### Coverage Requirements\n`;
287-
comment += `- **Minimum threshold:** 70% line coverage\n`;
288-
comment += `- **Backend:** ${backendMeetsThreshold ? '✅ Passes' : backendAvailable ? '❌ Fails (below 70%)' : '⚠️ Not available'}\n`;
289-
comment += `- **Frontend:** ${frontendMeetsThreshold ? '✅ Passes' : frontendAvailable ? '❌ Fails (below 70%)' : '⚠️ Not available'}\n\n`;
290-
291-
if (!backendMeetsThreshold || !frontendMeetsThreshold) {
292-
comment += `> ⚠️ **This PR cannot be merged until both frontend and backend coverage meet the 70% threshold.**\n\n`;
293-
}
294-
295-
comment += `---\n*Coverage report generated for commit ${context.sha.substring(0, 7)}*`;
296-
297-
// Find existing comment
298-
const { data: comments } = await github.rest.issues.listComments({
299-
owner: context.repo.owner,
300-
repo: context.repo.repo,
301-
issue_number: context.issue.number,
302-
});
303-
304-
const botComment = comments.find(comment =>
305-
comment.user.type === 'Bot' &&
306-
comment.body.includes('Test Coverage Report')
307-
);
308-
309-
// Update or create comment
310-
if (botComment) {
311-
await github.rest.issues.updateComment({
312-
owner: context.repo.owner,
313-
repo: context.repo.repo,
314-
comment_id: botComment.id,
315-
body: comment
316-
});
317-
} else {
318-
await github.rest.issues.createComment({
319-
owner: context.repo.owner,
320-
repo: context.repo.repo,
321-
issue_number: context.issue.number,
322-
body: comment
323-
});
324-
}
325-
326181
# This job is required for branch protection rules
327182
# It will only succeed if all tests and linting pass
328183
check-all:
@@ -352,4 +207,4 @@ jobs:
352207
fi
353208
354209
echo ""
355-
echo "✅ All checks passed successfully!"
210+
echo "✅ All checks passed successfully!"
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
name: Coverage Comment
2+
3+
on:
4+
workflow_run:
5+
workflows:
6+
- CI - Lint and Test
7+
types:
8+
- completed
9+
10+
permissions:
11+
actions: read
12+
contents: read
13+
pull-requests: write
14+
issues: write
15+
16+
jobs:
17+
post-coverage-comment:
18+
name: Post Coverage Comment
19+
runs-on: ubuntu-latest
20+
if: ${{ github.event.workflow_run.event == 'pull_request' }}
21+
22+
steps:
23+
- name: Download backend coverage
24+
uses: actions/download-artifact@v4
25+
with:
26+
name: backend-coverage
27+
path: backend-coverage
28+
run-id: ${{ github.event.workflow_run.id }}
29+
github-token: ${{ secrets.GITHUB_TOKEN }}
30+
continue-on-error: true
31+
32+
- name: Download frontend coverage
33+
uses: actions/download-artifact@v4
34+
with:
35+
name: frontend-coverage
36+
path: frontend-coverage
37+
run-id: ${{ github.event.workflow_run.id }}
38+
github-token: ${{ secrets.GITHUB_TOKEN }}
39+
continue-on-error: true
40+
41+
- name: Generate coverage report comment
42+
uses: actions/github-script@v7
43+
with:
44+
github-token: ${{ secrets.GITHUB_TOKEN }}
45+
script: |
46+
const fs = require('fs');
47+
48+
const pullRequests = context.payload.workflow_run.pull_requests || [];
49+
50+
if (!pullRequests.length) {
51+
core.info('No pull request found for this workflow run, skipping coverage comment.');
52+
return;
53+
}
54+
55+
const issueNumber = pullRequests[0].number;
56+
const { owner, repo } = context.repo;
57+
const headSha = context.payload.workflow_run.head_sha || '';
58+
const shortSha = headSha ? headSha.slice(0, 7) : context.payload.workflow_run.id;
59+
60+
const readCoverage = (filePath) => {
61+
try {
62+
const raw = fs.readFileSync(filePath, 'utf8');
63+
const parsed = JSON.parse(raw);
64+
const total = parsed.total || parsed;
65+
return { available: true, total };
66+
} catch (error) {
67+
core.info(`Coverage not available at ${filePath}: ${error.message}`);
68+
return {
69+
available: false,
70+
total: { lines: { pct: 0 }, statements: { pct: 0 }, functions: { pct: 0 }, branches: { pct: 0 } }
71+
};
72+
}
73+
};
74+
75+
const backendCoverage = readCoverage('backend-coverage/coverage-summary.json');
76+
const frontendCoverage = readCoverage('frontend-coverage/coverage-summary.json');
77+
78+
const getBadge = (percentage) => {
79+
if (percentage >= 80) return '🟢';
80+
if (percentage >= 70) return '🟡';
81+
return '🔴';
82+
};
83+
84+
const formatPct = (value) => (typeof value === 'number' ? value.toFixed(2) : '0.00');
85+
86+
const formatSection = (label, coverage) => {
87+
if (!coverage.available) {
88+
return `### ${label}\n⚠️ Coverage data not available (tests may have failed)\n\n`;
89+
}
90+
91+
const total = coverage.total;
92+
return `### ${label}\n| Type | Coverage | Status |\n|------|----------|--------|\n| Lines | ${formatPct(total.lines.pct)}% | ${getBadge(total.lines.pct)} |\n| Statements | ${formatPct(total.statements.pct)}% | ${getBadge(total.statements.pct)} |\n| Functions | ${formatPct(total.functions.pct)}% | ${getBadge(total.functions.pct)} |\n| Branches | ${formatPct(total.branches.pct)}% | ${getBadge(total.branches.pct)} |\n\n`;
93+
};
94+
95+
const backendSection = formatSection('Backend Coverage', backendCoverage);
96+
const frontendSection = formatSection('Frontend Coverage', frontendCoverage);
97+
98+
const backendPass = backendCoverage.available && backendCoverage.total.lines.pct >= 70;
99+
const frontendPass = frontendCoverage.available && frontendCoverage.total.lines.pct >= 70;
100+
101+
let comment = '## 📊 Test Coverage Report\n\n';
102+
comment += backendSection;
103+
comment += frontendSection;
104+
comment += '### Coverage Requirements\n';
105+
comment += '- **Minimum threshold:** 70% line coverage\n';
106+
comment += `- **Backend:** ${backendPass ? '✅ Passes' : backendCoverage.available ? '❌ Fails (below 70%)' : '⚠️ Not available'}\n`;
107+
comment += `- **Frontend:** ${frontendPass ? '✅ Passes' : frontendCoverage.available ? '❌ Fails (below 70%)' : '⚠️ Not available'}\n\n`;
108+
109+
if (!backendPass || !frontendPass) {
110+
comment += '> ⚠️ Coverage thresholds are not met.\n\n';
111+
}
112+
113+
comment += `---\n*Coverage report generated for commit ${shortSha}*`;
114+
115+
const { data: comments } = await github.rest.issues.listComments({
116+
owner,
117+
repo,
118+
issue_number: issueNumber,
119+
});
120+
121+
const existing = comments.find((comment) =>
122+
comment.user.type === 'Bot' && comment.body.includes('Test Coverage Report')
123+
);
124+
125+
if (existing) {
126+
await github.rest.issues.updateComment({
127+
owner,
128+
repo,
129+
comment_id: existing.id,
130+
body: comment,
131+
});
132+
} else {
133+
await github.rest.issues.createComment({
134+
owner,
135+
repo,
136+
issue_number: issueNumber,
137+
body: comment,
138+
});
139+
}

0 commit comments

Comments
 (0)