-
Notifications
You must be signed in to change notification settings - Fork 0
229 lines (199 loc) · 8.74 KB
/
Copy pathreport.yml
File metadata and controls
229 lines (199 loc) · 8.74 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
# =============================================================================
# Test Report Workflow - Publish test results to PR
# Trigger: After CI workflow completes
# Features:
# 1. Download pytest artifacts from CI run
# 2. Publish test report as GitHub Check Run (visible in PR Checks tab)
# 3. Post/update a sticky comment on the PR with detailed test results
# Strategy: PR reports Python 3.8 only, main push reports full matrix
# Security:
# - workflow_run always runs in base repository context
# - Checkout uses base repo only (never fork code) to prevent code injection
# - All untrusted inputs passed via env vars (never ${{ }} in run scripts)
# - Permissions scoped to minimum required
# =============================================================================
name: Test Report
on:
workflow_run:
workflows: ["CI"]
types:
- completed
permissions:
checks: write # Create/update Check Run
pull-requests: write # Post sticky comment on PR
actions: read # Read workflow run info and download artifacts
contents: read # Checkout base repository code
jobs:
report:
name: Publish Test Results
runs-on: ubuntu-latest
if: |
github.event.workflow_run.conclusion == 'success' ||
github.event.workflow_run.conclusion == 'failure'
strategy:
fail-fast: false
matrix:
# PR: report 3.8 only; main push: full matrix
python-version: ${{ github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' && fromJSON('["3.8", "3.9", "3.10", "3.11", "3.12"]') || fromJSON('["3.8"]') }}
steps:
# Checkout BASE repo only (not fork) - dorny/test-reporter needs a git
# repo for path resolution, but does not need the actual PR code.
# SECURITY: Never checkout fork code in a privileged workflow_run context.
- name: Checkout base repository
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 1
- name: Download test results
uses: actions/download-artifact@v7
with:
name: pytest-results-${{ matrix.python-version }}
path: test-results
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
- name: Publish Test Report (Check Run)
uses: dorny/test-reporter@v2
if: always()
with:
name: PyTest Results (Python ${{ matrix.python-version }})
path: test-results/pytest-report.xml
reporter: java-junit
fail-on-error: false
fail-on-empty: true
list-suites: all
list-tests: all
max-annotations: 10
# Sticky comment with actual test results embedded in PR
comment:
name: Update PR Comment
runs-on: ubuntu-latest
needs: report
if: |
always() &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.pull_requests[0]
steps:
- name: Download test results
uses: actions/download-artifact@v7
with:
name: pytest-results-3.8
path: test-results
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
# SECURITY: All workflow_run event data is passed via env vars,
# never interpolated with ${{ }} inside run scripts to prevent
# code injection via attacker-controlled branch names etc.
- name: Parse test results and build comment
id: parse
env:
WORKFLOW_CONCLUSION: ${{ github.event.workflow_run.conclusion }}
WORKFLOW_SHA: ${{ github.event.workflow_run.head_sha }}
WORKFLOW_BRANCH: ${{ github.event.workflow_run.head_branch }}
WORKFLOW_RUN_URL: ${{ github.event.workflow_run.html_url }}
run: |
python3 << 'PYEOF'
import xml.etree.ElementTree as ET
import os
xml_path = "test-results/pytest-report.xml"
conclusion = os.environ["WORKFLOW_CONCLUSION"]
sha = os.environ["WORKFLOW_SHA"]
branch = os.environ["WORKFLOW_BRANCH"]
run_url = os.environ["WORKFLOW_RUN_URL"]
icon = "✅" if conclusion == "success" else "❌"
# Parse JUnit XML
tree = ET.parse(xml_path)
root = tree.getroot()
# Collect suite-level stats
total_tests = 0
total_failures = 0
total_errors = 0
total_skipped = 0
total_time = 0.0
# Collect individual test cases
failed_cases = []
passed_cases = []
skipped_cases = []
errored_cases = []
for suite in root.iter("testsuite"):
total_tests += int(suite.get("tests", 0))
total_failures += int(suite.get("failures", 0))
total_errors += int(suite.get("errors", 0))
total_skipped += int(suite.get("skipped", 0))
total_time += float(suite.get("time", 0))
for tc in root.iter("testcase"):
name = tc.get("name", "unknown")
classname = tc.get("classname", "")
time_s = float(tc.get("time", 0))
display = f"{classname}::{name}" if classname else name
failure = tc.find("failure")
error = tc.find("error")
skip = tc.find("skipped")
if failure is not None:
msg = (failure.get("message") or failure.text or "")[:120].replace("\n", " ")
failed_cases.append((display, time_s, msg))
elif error is not None:
msg = (error.get("message") or error.text or "")[:120].replace("\n", " ")
errored_cases.append((display, time_s, msg))
elif skip is not None:
skipped_cases.append((display, time_s))
else:
passed_cases.append((display, time_s))
total_passed = total_tests - total_failures - total_errors - total_skipped
# Build markdown comment
lines = []
lines.append(f"## {icon} CI Test Results — Python 3.8\n")
# Summary table
pass_icon = "✅" if total_passed == total_tests else "⚠️"
lines.append("| Metric | Value |")
lines.append("|--------|-------|")
lines.append(f"| **Tests** | {total_tests} |")
lines.append(f"| **Passed** | {pass_icon} {total_passed} |")
if total_failures > 0:
lines.append(f"| **Failed** | ❌ {total_failures} |")
if total_errors > 0:
lines.append(f"| **Errors** | ❌ {total_errors} |")
if total_skipped > 0:
lines.append(f"| **Skipped** | ⏭️ {total_skipped} |")
lines.append(f"| **Duration** | {total_time:.2f}s |")
lines.append(f"| **Commit** | `{sha[:7]}` |")
lines.append(f"| **Branch** | `{branch}` |")
lines.append("")
# Failed tests detail
if failed_cases:
lines.append("<details><summary>❌ Failed Tests</summary>\n")
lines.append("| Test | Time | Message |")
lines.append("|------|------|---------|")
for name, t, msg in failed_cases:
lines.append(f"| `{name}` | {t:.3f}s | {msg} |")
lines.append("\n</details>\n")
# Error tests detail
if errored_cases:
lines.append("<details><summary>💥 Errored Tests</summary>\n")
lines.append("| Test | Time | Message |")
lines.append("|------|------|---------|")
for name, t, msg in errored_cases:
lines.append(f"| `{name}` | {t:.3f}s | {msg} |")
lines.append("\n</details>\n")
# Passed tests (collapsed)
if passed_cases:
lines.append(f"<details><summary>✅ Passed Tests ({len(passed_cases)})</summary>\n")
lines.append("| Test | Time |")
lines.append("|------|------|")
for name, t in passed_cases:
lines.append(f"| `{name}` | {t:.3f}s |")
lines.append("\n</details>\n")
lines.append(f"[View full CI run]({run_url})")
lines.append("")
lines.append("---")
lines.append("_🤖 This comment is automatically updated on each push._")
body = "\n".join(lines)
# Write to GitHub Actions output (multiline)
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
f.write(f"comment<<ENDOFCOMMENT\n{body}\nENDOFCOMMENT\n")
PYEOF
- name: Post sticky PR comment
uses: marocchino/sticky-pull-request-comment@v2
with:
number: ${{ github.event.workflow_run.pull_requests[0].number }}
header: ci-test-summary
message: ${{ steps.parse.outputs.comment }}