-
Notifications
You must be signed in to change notification settings - Fork 42
152 lines (131 loc) · 5.57 KB
/
coverage-comment.yml
File metadata and controls
152 lines (131 loc) · 5.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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@v7
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@v8
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
});
}