Skip to content

Coverage Comment

Coverage Comment #14

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