PR Coverage Reporter #2131
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: 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'); |