-
Notifications
You must be signed in to change notification settings - Fork 74
248 lines (220 loc) · 10.4 KB
/
Copy pathcoverage-report.yml
File metadata and controls
248 lines (220 loc) · 10.4 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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
name: Coverage Report
on:
workflow_run:
workflows: ["Labs Prototypes CI"]
types:
- completed
# Note: workflow_run ALWAYS runs in the context of the default branch (main),
# not the PR branch. This is a GitHub security feature that ensures:
# 1. The workflow code being executed is from main (trusted code)
# 2. Secrets are available (they're not available to fork PRs in pull_request trigger)
# 3. Write permissions can be safely granted
jobs:
report:
runs-on: ubuntu-latest
# Job-level permissions (more explicit than workflow-level)
permissions:
contents: read # To checkout main branch
pull-requests: write # To post comments on the PR
actions: read # To download artifacts from the triggering workflow
checks: write # To create a coverage status check on the PR
if: >
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
steps:
# Checkout main FIRST to ensure all scripts and package.json come from
# trusted code, not the PR artifact
- uses: actions/checkout@v6
with:
ref: main
# Fetch enough history so we can checkout the base SHA commit.
# If the base SHA is too old, the baseline step will fall back
# gracefully via continue-on-error.
fetch-depth: 100
- name: Use Node.js 24.x
uses: actions/setup-node@v4
with:
node-version: 24.x
cache: "npm"
# Look up the PR using the pulls list API filtered by head branch.
# This works for ALL PR types including fork PRs and survives squash
# merges (unlike /commits/{sha}/pulls, which fails when the original
# head SHA isn't in the default branch after squash).
#
# Why not use github.event.workflow_run.pull_requests[0]?
# That field is EMPTY for fork PRs (cross-repo PRs) as a GitHub
# security measure. Since the team primarily works from personal
# forks, we query the pulls API with the fork owner + branch instead.
- name: Get PR info from branch
id: pr-info
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_EVENT_WORKFLOW_RUN_HEAD_REPOSITORY_OWNER_LOGIN: ${{ github.event.workflow_run.head_repository.owner.login }}
GITHUB_EVENT_WORKFLOW_RUN_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
FORK_OWNER="${GITHUB_EVENT_WORKFLOW_RUN_HEAD_REPOSITORY_OWNER_LOGIN}"
FORK_BRANCH="${GITHUB_EVENT_WORKFLOW_RUN_HEAD_BRANCH}"
API_URL="repos/${{ github.repository }}/pulls?state=all&head=${FORK_OWNER}:${FORK_BRANCH}&sort=updated&direction=desc&per_page=1"
PR_NUMBER=$(gh api "$API_URL" --jq '.[0].number // empty')
if [ -n "$PR_NUMBER" ]; then
echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT
BASE_SHA=$(gh api "$API_URL" --jq '.[0].base.sha // empty')
HEAD_SHA=$(gh api "$API_URL" --jq '.[0].head.sha // empty')
if [[ "$BASE_SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo "sha=$BASE_SHA" >> $GITHUB_OUTPUT
else
echo "::warning::Invalid base SHA format, using HEAD of main"
echo "sha=" >> $GITHUB_OUTPUT
fi
if [[ "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo "head_sha=$HEAD_SHA" >> $GITHUB_OUTPUT
else
echo "::warning::Could not determine PR head SHA for check"
echo "head_sha=" >> $GITHUB_OUTPUT
fi
else
echo "::warning::No PR found for ${FORK_OWNER}:${FORK_BRANCH}"
echo "number=" >> $GITHUB_OUTPUT
echo "sha=" >> $GITHUB_OUTPUT
echo "head_sha=" >> $GITHUB_OUTPUT
fi
# If we have a base SHA, checkout that commit for the baseline.
# This avoids the automerge race where main already contains the PR
# changes by the time this workflow runs.
- name: Checkout baseline commit
if: steps.pr-info.outputs.sha != ''
run: git checkout ${STEPS_PR_INFO_OUTPUTS_SHA}
env:
STEPS_PR_INFO_OUTPUTS_SHA: ${{ steps.pr-info.outputs.sha }}
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Build and generate baseline coverage
id: baseline
run: |
npm run build
npm run coverage
continue-on-error: true
# Restore main's scripts for the comparison step. Coverage data in
# gitignored directories (packages/*/coverage/) survives this checkout.
- name: Restore trusted scripts
if: steps.pr-info.outputs.sha != ''
run: git checkout main -- scripts/
# Download artifacts AFTER the baseline build to keep them isolated
# from the trusted codebase. The artifact only contains JSON data files.
- name: Download coverage artifact
id: download
uses: actions/download-artifact@v7
with:
name: coverage-report
path: pr-artifact
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: Check artifact download
if: steps.download.outcome == 'failure'
run: |
echo "::warning::Coverage artifact not found. Skipping coverage report."
exit 0
- name: Skip if no PR number
if: steps.download.outcome == 'success' && steps.pr-info.outputs.number == ''
run: exit 0
# upload-artifact flattens directory structure when uploading a single
# file vs multiple files, so we locate it dynamically to be safe.
- name: Locate PR coverage file
if: steps.download.outcome == 'success' && steps.pr-info.outputs.number != ''
id: pr-coverage
run: |
PR_COV=$(find pr-artifact -name coverage-summary.json -type f | head -1)
if [ -n "$PR_COV" ]; then
echo "path=$PR_COV" >> $GITHUB_OUTPUT
else
echo "::warning::coverage-summary.json not found in artifact"
echo "path=" >> $GITHUB_OUTPUT
fi
- name: Generate coverage comparison
if: steps.download.outcome == 'success' && steps.pr-info.outputs.number != '' && steps.baseline.outcome == 'success' && steps.pr-coverage.outputs.path != ''
id: coverage
# compare-coverage.js only parses JSON data, it does not execute artifact contents
run: node scripts/compare-coverage.js ${STEPS_PR_COVERAGE_OUTPUTS_PATH} packages/visual-editor/coverage/coverage-summary.json
continue-on-error: true
env:
STEPS_PR_COVERAGE_OUTPUTS_PATH: ${{ steps.pr-coverage.outputs.path }}
- name: Generate fallback comment on comparison failure
if: steps.download.outcome == 'success' && steps.pr-info.outputs.number != '' && (steps.baseline.outcome == 'failure' || steps.coverage.outcome == 'failure')
run: |
echo "## 📊 Coverage Report" > coverage-comment.md
echo "" >> coverage-comment.md
echo "⚠️ Unable to generate coverage comparison. This may be due to:" >> coverage-comment.md
echo "- Build failure on main branch" >> coverage-comment.md
echo "- Missing coverage data" >> coverage-comment.md
echo "" >> coverage-comment.md
if [ -n "${STEPS_PR_COVERAGE_OUTPUTS_PATH}" ]; then
echo "**PR Coverage data was uploaded successfully.**" >> coverage-comment.md
fi
env:
STEPS_PR_COVERAGE_OUTPUTS_PATH: ${{ steps.pr-coverage.outputs.path }}
- name: Post coverage comment
if: steps.download.outcome == 'success' && steps.pr-info.outputs.number != ''
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
if (!fs.existsSync('coverage-comment.md')) {
console.log('No coverage comment file found. Skipping.');
return;
}
const marker = '<!-- breadboard-coverage-report -->';
const commentBody = fs.readFileSync('coverage-comment.md', 'utf8');
const body = marker + '\n' + commentBody;
const prNumber = ${{ steps.pr-info.outputs.number }};
// Find existing coverage comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
console.log(`Updating existing comment ${existing.id}`);
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: body
});
} else {
console.log('Creating new coverage comment');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: body
});
}
- name: Create coverage check
if: steps.download.outcome == 'success' && steps.pr-info.outputs.number != '' && steps.pr-info.outputs.head_sha != ''
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
let conclusion = 'neutral';
let title = 'Coverage data unavailable';
let summary = 'Could not determine coverage status.';
if (fs.existsSync('coverage-status.json')) {
const status = JSON.parse(fs.readFileSync('coverage-status.json', 'utf8'));
conclusion = status.failed ? 'failure' : 'success';
title = status.failed ? 'Coverage dropped' : 'Coverage OK';
summary = status.details;
}
// Use the full markdown table as the summary if available.
if (fs.existsSync('coverage-comment.md')) {
summary = fs.readFileSync('coverage-comment.md', 'utf8');
}
await github.rest.checks.create({
owner: context.repo.owner,
repo: context.repo.repo,
name: 'Coverage',
head_sha: '${{ steps.pr-info.outputs.head_sha }}',
conclusion,
output: { title, summary }
});