Coverage Comment #14
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: Coverage Comment | |
| # This workflow runs after CI completes and posts coverage comments to PRs. | |
| # It uses workflow_run to run in the upstream repo context with write permissions, | |
| # enabling coverage comments on fork PRs that would otherwise fail due to | |
| # insufficient permissions (see: https://github.com/anthropics/claude-code-action/issues/339) | |
| on: | |
| workflow_run: | |
| workflows: ["CI (Tests & Quality)"] | |
| types: [completed] | |
| jobs: | |
| post-comment: | |
| name: Post Coverage Comment | |
| runs-on: ubuntu-latest | |
| if: > | |
| github.event.workflow_run.event == 'pull_request' && | |
| github.event.workflow_run.conclusion == 'success' | |
| permissions: | |
| pull-requests: write | |
| actions: read | |
| steps: | |
| - name: Get PR number from workflow run | |
| id: pr_info | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| // Get the PR associated with the workflow run (trusted source) | |
| const workflowRun = await github.rest.actions.getWorkflowRun({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: context.payload.workflow_run.id | |
| }); | |
| // Extract PR number from the workflow run's pull_requests array | |
| const pullRequests = workflowRun.data.pull_requests; | |
| if (!pullRequests || pullRequests.length === 0) { | |
| core.setFailed('No pull request associated with this workflow run'); | |
| return; | |
| } | |
| const prNumber = pullRequests[0].number; | |
| core.setOutput('pr_number', prNumber); | |
| console.log(`PR number from workflow run: ${prNumber}`); | |
| - name: Download coverage data | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: coverage-data | |
| path: coverage-data | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Read coverage data | |
| id: coverage | |
| env: | |
| TRUSTED_PR_NUMBER: ${{ steps.pr_info.outputs.pr_number }} | |
| run: | | |
| if [ -f coverage-data/coverage.json ]; then | |
| PR_COVERAGE=$(jq -r '.pr_coverage' coverage-data/coverage.json) | |
| MAIN_COVERAGE=$(jq -r '.main_coverage' coverage-data/coverage.json) | |
| DIFF=$(jq -r '.diff' coverage-data/coverage.json) | |
| ARTIFACT_PR_NUMBER=$(jq -r '.pr_number' coverage-data/coverage.json) | |
| # Security: Validate PR number matches (prevent cross-PR comment injection) | |
| if [ "$ARTIFACT_PR_NUMBER" != "$TRUSTED_PR_NUMBER" ]; then | |
| echo "::error::PR number mismatch: artifact=$ARTIFACT_PR_NUMBER, expected=$TRUSTED_PR_NUMBER" | |
| exit 1 | |
| fi | |
| { | |
| echo "pr_coverage=$PR_COVERAGE" | |
| echo "main_coverage=$MAIN_COVERAGE" | |
| echo "diff=$DIFF" | |
| echo "pr_number=$TRUSTED_PR_NUMBER" | |
| } >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Coverage data not found" | |
| exit 1 | |
| fi | |
| - name: Post coverage comment | |
| uses: actions/github-script@v7 | |
| env: | |
| PR_COVERAGE: ${{ steps.coverage.outputs.pr_coverage }} | |
| MAIN_COVERAGE: ${{ steps.coverage.outputs.main_coverage }} | |
| DIFF: ${{ steps.coverage.outputs.diff }} | |
| PR_NUMBER: ${{ steps.coverage.outputs.pr_number }} | |
| with: | |
| script: | | |
| const prCoverage = process.env.PR_COVERAGE; | |
| const mainCoverage = process.env.MAIN_COVERAGE; | |
| const diff = parseFloat(process.env.DIFF); | |
| const prNumber = parseInt(process.env.PR_NUMBER); | |
| const emoji = diff >= 0 ? '📈' : '📉'; | |
| const diffText = diff >= 0 ? `+${diff}%` : `${diff}%`; | |
| const status = diff >= 0 ? '✅' : '⚠️'; | |
| const body = `## ${emoji} Test Coverage Report\n\n` + | |
| `| Branch | Coverage |\n` + | |
| `|--------|----------|\n` + | |
| `| **This PR** | ${prCoverage}% |\n` + | |
| `| Main | ${mainCoverage}% |\n` + | |
| `| **Diff** | ${status} ${diffText} |\n\n` + | |
| `---\n\n` + | |
| `*Coverage calculated from unit tests only*`; | |
| const comments = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| }); | |
| const existingComment = comments.data.find(comment => | |
| comment.user.login === 'github-actions[bot]' && | |
| comment.body.includes('Test Coverage Report') | |
| ); | |
| if (existingComment) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existingComment.id, | |
| body: body | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: body | |
| }); | |
| } |