Skip to content

Coverage Comment

Coverage Comment #36

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
});
}