Skip to content

PR Coverage Reporter #2131

PR Coverage Reporter

PR Coverage Reporter #2131

name: PR Coverage Reporter
on:
workflow_run:
workflows: ["PR check"]
types: [completed]
# Only one coverage report per PR at a time
concurrency:
group: coverage-${{ github.event.workflow_run.head_sha }}
cancel-in-progress: true
jobs:
report-coverage:
if: github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
pull-requests: write
actions: read
steps:
- name: Get PR info
id: pr
uses: actions/github-script@v8
with:
script: |
const prs = context.payload.workflow_run.pull_requests;
if (prs && prs.length > 0) {
core.setOutput('number', prs[0].number);
core.setOutput('base_ref', prs[0].base.ref);
return;
}
// Fallback: search by head branch
const { data: pulls } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
state: 'open'
});
if (pulls.length > 0) {
core.setOutput('number', pulls[0].number);
core.setOutput('base_ref', pulls[0].base.ref);
return;
}
core.setFailed('Could not find PR');
- uses: actions/checkout@v6
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0
- name: Fetch base branch
run: git fetch origin "${{ steps.pr.outputs.base_ref }}"
- name: Download client coverage artifact
uses: actions/download-artifact@v4
with:
name: coverage-report-client
path: build/test-results/vitest/coverage/
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: Download server coverage artifact
uses: actions/download-artifact@v4
with:
name: coverage-report-server
path: build/reports/jacoco/test/
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
- name: Generate coverage table
id: coverage
run: |
BASE_BRANCH="origin/${{ steps.pr.outputs.base_ref }}"
COVERAGE_OUTPUT=$(node supporting_scripts/local-pr-coverage/local-pr-coverage.mjs \
--skip-tests --print --base-branch "$BASE_BRANCH" --verbose 2>&1) || true
echo "$COVERAGE_OUTPUT"
# Extract the coverage table (between separator lines)
COVERAGE_TABLE=$(echo "$COVERAGE_OUTPUT" | sed -n '/^─/,/^─/p' | sed '1d;$d')
if [ -z "$COVERAGE_TABLE" ]; then
echo "has_table=false" >> $GITHUB_OUTPUT
else
echo "has_table=true" >> $GITHUB_OUTPUT
{
echo 'table<<EOF'
echo "$COVERAGE_TABLE"
echo 'EOF'
} >> $GITHUB_OUTPUT
fi
- name: Update PR description
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = parseInt('${{ steps.pr.outputs.number }}');
const owner = context.repo.owner;
const repo = context.repo.repo;
const author = context.payload.workflow_run.head_commit?.author?.username || '';
const prCheckConclusion = context.payload.workflow_run.conclusion;
const hasTable = '${{ steps.coverage.outputs.has_table }}' === 'true';
const coverageTable = `${{ steps.coverage.outputs.table }}`;
const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${context.payload.workflow_run.id}`;
const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber
});
let body = pr.body || '';
const coverageSection = '### Test Coverage';
const coverageIndex = body.indexOf(coverageSection);
if (coverageIndex === -1) {
console.log('Test Coverage section not found in PR description');
return;
}
const afterCoverage = body.substring(coverageIndex + coverageSection.length);
const nextSectionMatch = afterCoverage.match(/\n###\s/);
const nextSectionIndex = nextSectionMatch
? coverageIndex + coverageSection.length + nextSectionMatch.index
: body.length;
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
const lines = [
coverageSection,
'<!-- Please add the test coverages for all changed files modified in this PR here. You can generate the coverage table using one of these options: -->',
'<!-- 1. Run `npm run coverage:pr` to generate coverage locally by running only the affected module tests (see supporting_scripts/local-pr-coverage/README.md) -->',
'<!-- The line coverage must be above 90% for changed files, and you must use extensive and useful assertions for server tests and expect statements for client tests. -->',
'<!-- Note: Confirm in the last column that you have implemented extensive assertions for server tests and expect statements for client tests. -->',
'<!-- Remove rows with only trivial changes from the table. -->',
''
];
if (prCheckConclusion === 'failure') {
lines.push(`**Warning:** Some tests failed in the PR check. Coverage may be incomplete. Please check the [workflow logs](${runUrl}).`, '');
}
if (hasTable && coverageTable && coverageTable.trim()) {
lines.push(coverageTable, '');
} else if (prCheckConclusion === 'success') {
lines.push('**No code changes detected** - test coverage not required for this PR.', '');
}
lines.push(`_Last updated: ${timestamp}_`, '', '');
const newCoverageContent = lines.join('\n');
const newBody = body.substring(0, coverageIndex) + newCoverageContent + body.substring(nextSectionIndex);
await github.rest.pulls.update({
owner,
repo,
pull_number: prNumber,
body: newBody
});
console.log('Updated PR description with coverage results');