-
-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathaction.yml
More file actions
462 lines (433 loc) · 18.8 KB
/
action.yml
File metadata and controls
462 lines (433 loc) · 18.8 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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
---
# yamllint disable rule:line-length
name: Code Coverage Report Action
author: Inisghts Engineering
description: Action that converts a Cobertura XML report into a markdown report.
inputs:
token:
description: Github token to use to publish the check.
required: false
default: ${{ github.token }}
path:
description: Path to the Cobertura coverage XML report.
required: false
default: coverage.xml
threshold:
description: The minimum allowed coverage percentage, as a real number.
required: false
default: 0
fail:
description: Fail the action when the minimum coverage was not met.
required: false
default: true
publish:
description: Publish the coverage report as an issue comment.
required: false
default: false
diff:
description: Create a diff of the coverage report.
required: false
default: false
diff-branch:
description: Branch to diff against.
required: false
default: main
storage-subdirectory:
description: Subdirectory in the diff-storage branch where the XML reports will be stored.
required: false
default: "."
diff-storage:
description: Branch where coverage reports are stored for diff purposes.
required: false
default: _xml_coverage_reports
coverage-summary-title:
description: Title for the code coverage summary in the Pull Request comment.
required: false
default: "Code Coverage Summary"
uncovered-statements-increase-failure:
description: |
Fail the action if any changed file has an increase in uncovered lines compared to the `diff-branch`.
This corresponds to pycobertura exit code 2, which indicates that at least one changed file has
more uncovered lines than before (Miss > 0). Note that this is different from coverage rate
reduction - it specifically checks for increases in the absolute number of uncovered lines.
required: false
default: false
new-uncovered-statements-failure:
description: |
Fail the action if new uncovered statements are introduced AND overall coverage improved
(total uncovered lines decreased) compared to the `diff-branch`.
This corresponds to pycobertura exit code 3, which only occurs when total uncovered lines
decreased (Miss <= 0) but there are still new uncovered statements (Missing != []).
To fail on ALL new uncovered statements regardless of overall coverage improvement,
use this flag together with `coverage-reduction-failure: true`.
required: false
default: false
coverage-rate-reduction-failure:
description: |
Fail the action if the overall coverage percentage (rate) decreases compared to the `diff-branch`.
This is different from `uncovered-statements-increase-failure` which checks for absolute
increases in uncovered lines. This flag specifically looks at the coverage percentage
and fails if it goes down, regardless of whether uncovered lines increased or decreased.
This is a more forgiving approach that focuses on the relative coverage rate rather than
absolute uncovered line counts.
required: false
default: false
pycobertura-exception-failure:
description: Fail the action in case of a `Pycobertura` exception.
required: false
default: true
togglable-report:
description: Make the code coverage report togglable.
required: false
default: false
report-low-coverage-only:
description: |
When true, the detailed coverage report will only include files where
coverage is less than 100%. The TOTAL summary line is always included.
required: false
default: false
exclude-detailed-coverage:
description: |
Whether a detailed coverage report should be excluded from the PR comment.
The detailed coverage report contains the following information per file:
number of code statements, number of statements not covered by any test,
coverage percentage, and line numbers not covered by any test.
required: false
default: false
outputs:
summary:
description: Summary of coverage report
value: ${{ steps.create-output.outputs.summary }}
branding: # https://feathericons.com/
icon: "umbrella"
color: "red"
runs:
using: composite
steps:
- name: Setup Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: '3.11'
- name: Install pycobertura
uses: insightsengineering/pip-action@905c7a9be173f62965b791e91d0079355d99f19b # v2.0.2
with:
packages: pycobertura==3.0.0
- name: Get branch names
id: branch-names
uses: tj-actions/branch-names@6c999acf206f5561e19f46301bb310e9e70d8815 # v7.0.7
- name: Generate text report
run: |
mkdir -p coverage-action
cp ${{ inputs.path }} coverage-action/
pycobertura show ${{ inputs.path }} --output .coverage-output
if [[ "${{ inputs.report-low-coverage-only }}" == "true" ]]
then {
# Keep header lines, the TOTAL line, and any file with missed lines (Miss != 0)
awk 'NR<=2 || /^TOTAL / || $3 != 0' .coverage-output > .coverage-output.filtered
mv .coverage-output.filtered .coverage-output
}
fi
cat .coverage-output
shell: bash
- name: Fetch report from ${{ inputs.diff-storage }}
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
path: ${{ inputs.diff-storage }}
fetch-depth: 0
token: ${{ inputs.token }}
- name: Get token identity
id: identity
uses: octokit/graphql-action@f7836e89a7e5bac63911bbe9653c21147b3d9bc3
with:
query: |
query {
viewer {
databaseId
login
}
}
env:
GITHUB_TOKEN: ${{ inputs.token }}
- name: Configure git
run: |
name="${{ fromJSON(steps.identity.outputs.data).viewer.login }}"
email="${{ format('{0}+{1}@users.noreply.github.com', fromJSON(steps.identity.outputs.data).viewer.databaseId, fromJSON(steps.identity.outputs.data).viewer.login) }}"
cat >> "$GITHUB_ENV" << EOF
GIT_AUTHOR_NAME=$name
GIT_AUTHOR_EMAIL=$email
GIT_COMMITTER_NAME=$name
GIT_COMMITTER_EMAIL=$email
EOF
shell: bash
- name: Initialize storage branch
working-directory: ${{ inputs.diff-storage }}
run: |
# Switch to the branch if it already exists
git switch ${{ inputs.diff-storage }} || true
git pull origin ${{ inputs.diff-storage }} || true
# Create the branch if it doesn't exist yet
git checkout --orphan ${{ inputs.diff-storage }} || true
# Ensure that the bare minimum components exist in the branch
mkdir -p data
touch README.md data/.gitkeep
# Copy necessary files and folders to a temporary location
mkdir -p /tmp/${{ github.sha }}
echo "Copying data to /tmp/${{ github.sha }}"
cp -r .git README.md data /tmp/${{ github.sha }}
# Remove everything else
# Attribution: https://unix.stackexchange.com/a/77313
rm -rf ..?* .[!.]* *
# Restore files from the temporary location
echo "Copying data from /tmp/${{ github.sha }}"
cp -r /tmp/${{ github.sha }}/.git /tmp/${{ github.sha }}/README.md /tmp/${{ github.sha }}/data .
rm -rf /tmp/${{ github.sha }}
git add --all -f
git commit -m "Update storage branch: $(date)" || true
shell: bash
- name: Push storage branch
uses: ad-m/github-push-action@4cc74773234f74829a8c21bc4d69dd4be9cfa599
with:
github_token: ${{ inputs.token }}
branch: ${{ inputs.diff-storage }}
directory: ${{ inputs.diff-storage }}
force: true
- name: Generate diff against ${{ inputs.diff-branch }}
if: contains(inputs.diff, 'true')
run: |
echo "storage_subdirectory = '${{ inputs.storage-subdirectory }}'"
pushd ${{ inputs.diff-storage }}
git checkout ${{ inputs.diff-storage }} || touch ${{ inputs.diff-storage }}-not-found
popd
if [[ -f "${{ inputs.diff-storage }}/data/${{ inputs.diff-branch }}/${{ inputs.storage-subdirectory }}/coverage.xml" && (! -f ${{ inputs.diff-storage }}-not-found) ]]
then {
pycobertura diff --no-color --no-source ${{ inputs.diff-storage }}/data/${{ inputs.diff-branch }}/${{ inputs.storage-subdirectory }}/coverage.xml \
${{ inputs.path }} \
--output .coverage-output.diff && pycobertura_status=$? || pycobertura_status=$?
# Save status both in case of success and failure.
echo "pycobertura_status=$pycobertura_status" >> $GITHUB_ENV
cat .coverage-output.diff
} else {
echo "${{ inputs.diff-storage }}/data/${{ inputs.diff-branch }}/${{ inputs.storage-subdirectory }}/coverage.xml not found! Not diffing."
}
fi
shell: bash
- name: Extract diff-branch coverage percentage
if: contains(inputs.diff, 'true') && contains(inputs.coverage-rate-reduction-failure, 'true')
run: |
if [[ -f "${{ inputs.diff-storage }}/data/${{ inputs.diff-branch }}/${{ inputs.storage-subdirectory }}/coverage.xml" ]]
then {
# Generate a text report for the diff-branch coverage
pycobertura show ${{ inputs.diff-storage }}/data/${{ inputs.diff-branch }}/${{ inputs.storage-subdirectory }}/coverage.xml --output .coverage-output.diff-branch
# Extract the total coverage percentage
grep -E "^TOTAL " .coverage-output.diff-branch | \
awk '{printf "%.8f", (1 - $3/$2) * 100}' > .coverage-total.diff-branch
echo "Diff-branch coverage: $(cat .coverage-total.diff-branch)%"
} else {
echo "Diff-branch coverage file not found, cannot compare coverage rates."
echo "0" > .coverage-total.diff-branch
}
fi
shell: bash
- name: Get total
run: |
grep -E "^TOTAL " .coverage-output | \
awk '{printf "%.8f", (1 - $3/$2) * 100}' > .coverage-total
shell: bash
- name: Store coverage percent
id: coverage_percent
run: |
echo "coverage_total=$(cat .coverage-total)" >> $GITHUB_OUTPUT
BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
echo "diff_storage_branch=$BRANCH" >> $GITHUB_ENV
mkdir -p ${{ inputs.diff-storage }}/data/${BRANCH}/${{ inputs.storage-subdirectory }}
shell: bash
# Use the output from the `coverage_percent` step
- name: Generate the badge SVG image
uses: emibcn/badge-action@808173dd03e2f30c980d03ee49e181626088eee8 # v2.0.3
id: badge
with:
label: 'Test Coverage'
status: "${{ steps.coverage_percent.outputs.coverage_total }}%"
color: ${{
steps.coverage_percent.outputs.coverage_total > 90 && 'green' ||
steps.coverage_percent.outputs.coverage_total > 80 && 'yellow,green' ||
steps.coverage_percent.outputs.coverage_total > 70 && 'yellow' ||
steps.coverage_percent.outputs.coverage_total > 60 && 'orange,yellow' ||
steps.coverage_percent.outputs.coverage_total > 50 && 'orange' ||
steps.coverage_percent.outputs.coverage_total > 40 && 'red,orange' ||
steps.coverage_percent.outputs.coverage_total > 30 && 'red,red,orange' ||
steps.coverage_percent.outputs.coverage_total > 20 && 'red,red,red,orange' ||
'red' }}
path: ${{ inputs.diff-storage }}/data/${{ env.diff_storage_branch }}/${{ inputs.storage-subdirectory }}/badge.svg
- name: Commit badge
working-directory: ${{ inputs.diff-storage }}/data
run: |
git switch ${{ inputs.diff-storage }} || true
git pull origin ${{ inputs.diff-storage }}
git add "${{ env.diff_storage_branch }}/${{ inputs.storage-subdirectory }}/badge.svg"
git commit -m "Add/Update badge: ${{ github.sha }}" || true
shell: bash
# Badge has to be committed and pushed to be used in comment
- name: Push badges
uses: ad-m/github-push-action@4cc74773234f74829a8c21bc4d69dd4be9cfa599
with:
github_token: ${{ inputs.token }}
branch: ${{ inputs.diff-storage }}
directory: ${{ inputs.diff-storage }}/data
- name: Determine repository visibility
if: contains(inputs.publish, 'true')
id: repository-visibility
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const result = await github.rest.repos.get({
owner: "${{ github.repository_owner }}",
repo: "${{ github.repository }}".split("/")[1]
});
return result.data.visibility;
result-encoding: string
- name: Generate issue comment body
if: contains(inputs.publish, 'true')
run: |
BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
if [[ "${{ steps.repository-visibility.outputs.result }}" == "public" ]]
then {
# URL encoding for branch name
URL_ENCODED_BRANCH=$(python3 -c 'import urllib.parse; print(urllib.parse.quote_plus("${{ env.diff_storage_branch }}"))')
echo -e "\n\n" > .coverage-output.final
}
else {
echo -e "🧪 Test coverage: ${{ steps.coverage_percent.outputs.coverage_total }}%\n\n" > .coverage-output.final
}
fi
echo -e "## ${{ inputs.coverage-summary-title }}\n" >> .coverage-output.final
if [[ "${{ inputs.exclude-detailed-coverage }}" == "false" ]]
then {
if [[ "${{ inputs.togglable-report }}" == "true" ]]
then {
echo -e "<details>\n\n" >> .coverage-output.final
}
fi
echo -e "\`\`\`" >> .coverage-output.final
cat .coverage-output >> .coverage-output.final
echo -e "\n\`\`\`\n" >> .coverage-output.final
if [[ "${{ inputs.togglable-report }}" == "true" ]]
then {
echo -e "</details>\n\n" >> .coverage-output.final
}
fi
}
else {
echo -e "\n" >> .coverage-output.final
}
fi
if [[ "${{ inputs.diff }}" == "true" && -f .coverage-output.diff ]]
then {
echo -e "### Diff against ${{ inputs.diff-branch }}\n" >> .coverage-output.final
echo -e "\`\`\`" >> .coverage-output.final
cat .coverage-output.diff >> .coverage-output.final
echo -e "\n\`\`\`\n" >> .coverage-output.final
}
fi
COMMIT_SHA="${{ github.sha }}"
if [ "${{ github.event_name }}" == "pull_request" ]
then {
COMMIT_SHA="${{ github.event.pull_request.head.sha }}"
}
fi
echo -e "\nResults for commit: $COMMIT_SHA\n" >> .coverage-output.final
echo -e "\n_Minimum allowed coverage is \`${{ inputs.threshold }}%\`_\n" >> .coverage-output.final
if [[ "${{ inputs.publish }}" == "true" ]]
then {
echo -e "\n:recycle: This comment has been updated with latest results\n" >> .coverage-output.final
}
fi
shell: bash
- name: Post as comment
if: contains(inputs.publish, 'true')
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
with:
GITHUB_TOKEN: ${{ inputs.token }}
header: ${{ inputs.path }}
path: .coverage-output.final
- name: Set as output
id: create-output
run: |
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
echo "summary<<$EOF" >> $GITHUB_OUTPUT
echo "$(cat .coverage-output.final)" >> $GITHUB_OUTPUT
echo "$EOF" >> $GITHUB_OUTPUT
shell: bash
- name: Commit XML report to ${{ inputs.diff-storage }}
if: >
contains(inputs.diff, 'true')
working-directory: ${{ inputs.diff-storage }}
run: |
git switch ${{ inputs.diff-storage }}
git pull origin ${{ inputs.diff-storage }}
filename=$(basename ${{ inputs.path }})
mv ../coverage-action/${filename} ./data/${{ env.diff_storage_branch }}/${{ inputs.storage-subdirectory }}/coverage.xml
git add -f "./data/${{ env.diff_storage_branch }}/${{ inputs.storage-subdirectory }}/coverage.xml"
git commit -m "Coverage report for ${{ github.sha }}" || true
shell: bash
- name: Push XML report to ${{ inputs.diff-storage }}
if: >
contains(inputs.diff, 'true')
uses: ad-m/github-push-action@4cc74773234f74829a8c21bc4d69dd4be9cfa599
with:
github_token: ${{ inputs.token }}
branch: ${{ inputs.diff-storage }}
directory: ${{ inputs.diff-storage }}/data
- name: Check threshold
if: contains(inputs.fail, 'true')
run: |
with open('.coverage-total', 'r') as t:
total = float(t.read().rstrip())
min = float('${{ inputs.threshold }}')
if total < min:
raise SystemExit(
f"Total Coverage of {total}% falls below minimum threshold of {min}%."
)
shell: python
- name: Fail if coverage worsened
if: contains(inputs.diff, 'true')
run: |
if [[ "${{ env.pycobertura_status }}" == "2" && "${{ inputs.uncovered-statements-increase-failure }}" == "true" ]]
then {
echo "Code changes increased the number of uncovered lines in at least one file."
exit 1
}
fi
if [[ "${{ env.pycobertura_status }}" == "3" && "${{ inputs.new-uncovered-statements-failure }}" == "true" ]]
then {
echo "Code changes introduced new uncovered statements despite overall coverage improvement."
exit 1
}
fi
if [[ "${{ env.pycobertura_status }}" == "1" && "${{ inputs.pycobertura-exception-failure }}" == "true" ]]
then {
echo "Pycobertura exception occurred."
exit 1
}
fi
if [[ "${{ inputs.coverage-rate-reduction-failure }}" == "true" && -f .coverage-total.diff-branch ]]
then {
current_coverage=$(cat .coverage-total)
diff_branch_coverage=$(cat .coverage-total.diff-branch)
if (( $(echo "$current_coverage < $diff_branch_coverage" | bc -l) ))
then {
echo "Coverage rate decreased from ${diff_branch_coverage}% to ${current_coverage}%."
exit 1
}
fi
}
fi
shell: bash
- name: Clean up intermediate files
if: always()
run: |
rm -rf coverage-action .coverage-output.final .coverage-output \
.coverage-total .coverage-output.diff \
.coverage-output.diff-branch .coverage-total.diff-branch \
${{ inputs.diff-storage }}-not-found
shell: bash