Coverage Comment #36
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 | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| # For fork PRs, pull_requests array is empty in workflow_run event | |
| # Use gh pr view to find the PR number | |
| # See: https://github.com/orgs/community/discussions/25220 | |
| HEAD_REPO="${{ github.event.workflow_run.head_repository.full_name }}" | |
| HEAD_BRANCH="${{ github.event.workflow_run.head_branch }}" | |
| TARGET_REPO="${{ github.repository }}" | |
| echo "Looking for PR from ${HEAD_REPO} branch ${HEAD_BRANCH}" | |
| # Determine branch query format: | |
| # - Fork PRs: use "owner:branch" format | |
| # - Same-repo PRs: use just "branch" format | |
| if [ "$HEAD_REPO" = "$TARGET_REPO" ]; then | |
| BRANCH_QUERY="${HEAD_BRANCH}" | |
| else | |
| BRANCH_QUERY="${HEAD_REPO%%/*}:${HEAD_BRANCH}" | |
| fi | |
| echo "Using branch query: ${BRANCH_QUERY}" | |
| PR_NUMBER=$(gh pr view \ | |
| --repo "$TARGET_REPO" \ | |
| "$BRANCH_QUERY" \ | |
| --json number --jq .number 2>/dev/null || echo "") | |
| if [ -z "$PR_NUMBER" ]; then | |
| echo "::error::Could not find PR for ${BRANCH_QUERY} in ${TARGET_REPO}" | |
| exit 1 | |
| fi | |
| echo "Found PR #${PR_NUMBER}" | |
| echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT" | |
| - 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 | |
| }); | |
| } |