gha-workflow-testing 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/dist/assertions.d.ts +23 -0
- package/dist/assertions.d.ts.map +1 -0
- package/dist/assertions.js +260 -0
- package/dist/assertions.js.map +1 -0
- package/dist/github-api.d.ts +38 -0
- package/dist/github-api.d.ts.map +1 -0
- package/dist/github-api.js +123 -0
- package/dist/github-api.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/reporter.d.ts +43 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +277 -0
- package/dist/reporter.js.map +1 -0
- package/package.json +52 -0
- package/src/assertions.ts +387 -0
- package/src/github-api.ts +184 -0
- package/src/index.ts +3 -0
- package/src/reporter.ts +344 -0
package/src/reporter.ts
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
|
|
3
|
+
export interface StepResult {
|
|
4
|
+
tested: boolean;
|
|
5
|
+
result: string;
|
|
6
|
+
conclusion?: string;
|
|
7
|
+
expected?: string;
|
|
8
|
+
actual?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface JobResult {
|
|
12
|
+
tested: boolean;
|
|
13
|
+
result: string;
|
|
14
|
+
conclusion?: string;
|
|
15
|
+
expected?: string;
|
|
16
|
+
actual?: string;
|
|
17
|
+
steps: { [stepName: string]: StepResult };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ArtifactResult {
|
|
21
|
+
tested: boolean;
|
|
22
|
+
result: string;
|
|
23
|
+
checks: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class WorkflowTestReporter {
|
|
27
|
+
private workflowName: string;
|
|
28
|
+
private jobs: { [jobName: string]: JobResult } = {};
|
|
29
|
+
private artifacts: { [artifactName: string]: ArtifactResult } = {};
|
|
30
|
+
private runId?: string;
|
|
31
|
+
private allJobsLoaded = false;
|
|
32
|
+
|
|
33
|
+
constructor(workflowName: string) {
|
|
34
|
+
this.workflowName = workflowName;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setRunId(runId: string | number): void {
|
|
38
|
+
this.runId = String(runId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async loadAllJobsAndSteps(api: any): Promise<void> {
|
|
42
|
+
if (this.allJobsLoaded || !this.runId) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// Get all jobs from the run
|
|
48
|
+
const jobsData = await api.getJobs(Number(this.runId));
|
|
49
|
+
|
|
50
|
+
for (const [jobName, jobInfo] of Object.entries(jobsData)) {
|
|
51
|
+
// Add job if not already tracked
|
|
52
|
+
if (!this.jobs[jobName]) {
|
|
53
|
+
this.jobs[jobName] = {
|
|
54
|
+
tested: false,
|
|
55
|
+
result: 'Not Tested',
|
|
56
|
+
conclusion: (jobInfo as any).conclusion || undefined,
|
|
57
|
+
steps: {},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Get all steps for this job
|
|
62
|
+
try {
|
|
63
|
+
const stepsData = await api.getSteps(Number(this.runId), jobName);
|
|
64
|
+
|
|
65
|
+
for (const [stepName, stepInfo] of Object.entries(stepsData)) {
|
|
66
|
+
if (!this.jobs[jobName].steps[stepName]) {
|
|
67
|
+
this.jobs[jobName].steps[stepName] = {
|
|
68
|
+
tested: false,
|
|
69
|
+
result: 'Not Tested',
|
|
70
|
+
conclusion: (stepInfo as any).conclusion || undefined,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.log(`⚠️ Could not load steps for ${jobName}: ${error}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.allJobsLoaded = true;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.log(`⚠️ Could not load all jobs and steps: ${error}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
addJob(jobName: string): void {
|
|
86
|
+
if (!this.jobs[jobName]) {
|
|
87
|
+
this.jobs[jobName] = {
|
|
88
|
+
tested: false,
|
|
89
|
+
result: 'Not Tested',
|
|
90
|
+
steps: {},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
recordJobTest(
|
|
96
|
+
jobName: string,
|
|
97
|
+
passed: boolean,
|
|
98
|
+
expected?: string,
|
|
99
|
+
actual?: string
|
|
100
|
+
): void {
|
|
101
|
+
this.addJob(jobName);
|
|
102
|
+
this.jobs[jobName].tested = true;
|
|
103
|
+
this.jobs[jobName].expected = expected;
|
|
104
|
+
this.jobs[jobName].actual = actual;
|
|
105
|
+
|
|
106
|
+
if (passed) {
|
|
107
|
+
this.jobs[jobName].result = `✅ Passed (expected: ${expected})`;
|
|
108
|
+
} else {
|
|
109
|
+
this.jobs[jobName].result = `❌ Failed (expected: ${expected}, got: ${actual})`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
recordStepTest(
|
|
114
|
+
jobName: string,
|
|
115
|
+
stepName: string,
|
|
116
|
+
passed: boolean,
|
|
117
|
+
expected?: string,
|
|
118
|
+
actual?: string
|
|
119
|
+
): void {
|
|
120
|
+
this.addJob(jobName);
|
|
121
|
+
|
|
122
|
+
if (!this.jobs[jobName].steps[stepName]) {
|
|
123
|
+
this.jobs[jobName].steps[stepName] = {
|
|
124
|
+
tested: false,
|
|
125
|
+
result: 'Not Tested',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.jobs[jobName].steps[stepName].tested = true;
|
|
130
|
+
this.jobs[jobName].steps[stepName].expected = expected;
|
|
131
|
+
this.jobs[jobName].steps[stepName].actual = actual;
|
|
132
|
+
|
|
133
|
+
if (passed) {
|
|
134
|
+
this.jobs[jobName].steps[stepName].result = `✅ Passed (expected: ${expected})`;
|
|
135
|
+
} else {
|
|
136
|
+
this.jobs[jobName].steps[stepName].result = `❌ Failed (expected: ${expected}, got: ${actual})`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
addArtifact(artifactName: string): void {
|
|
141
|
+
if (!this.artifacts[artifactName]) {
|
|
142
|
+
this.artifacts[artifactName] = {
|
|
143
|
+
tested: false,
|
|
144
|
+
result: 'Not Tested',
|
|
145
|
+
checks: [],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
recordArtifactTest(
|
|
151
|
+
artifactName: string,
|
|
152
|
+
checkType: string,
|
|
153
|
+
passed: boolean,
|
|
154
|
+
expected?: string,
|
|
155
|
+
actual?: string
|
|
156
|
+
): void {
|
|
157
|
+
this.addArtifact(artifactName);
|
|
158
|
+
this.artifacts[artifactName].tested = true;
|
|
159
|
+
|
|
160
|
+
let result: string;
|
|
161
|
+
if (passed) {
|
|
162
|
+
result = `✅ ${checkType}`;
|
|
163
|
+
if (expected) {
|
|
164
|
+
result += ` (expected: ${expected})`;
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
result = `❌ ${checkType} failed`;
|
|
168
|
+
if (expected && actual) {
|
|
169
|
+
result += ` (expected: ${expected}, got: ${actual})`;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this.artifacts[artifactName].checks.push(result);
|
|
174
|
+
|
|
175
|
+
// Update overall artifact result
|
|
176
|
+
if (!passed) {
|
|
177
|
+
this.artifacts[artifactName].result = '❌ Failed';
|
|
178
|
+
} else if (this.artifacts[artifactName].result !== '❌ Failed') {
|
|
179
|
+
this.artifacts[artifactName].result = '✅ Passed';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
generateMarkdown(): string {
|
|
184
|
+
const lines: string[] = [];
|
|
185
|
+
|
|
186
|
+
// Header
|
|
187
|
+
lines.push('# 🧪 Workflow Test Report');
|
|
188
|
+
lines.push('');
|
|
189
|
+
lines.push(`**Workflow:** \`${this.workflowName}\``);
|
|
190
|
+
if (this.runId) {
|
|
191
|
+
lines.push(`**Run ID:** \`${this.runId}\``);
|
|
192
|
+
}
|
|
193
|
+
lines.push('');
|
|
194
|
+
|
|
195
|
+
// Summary stats
|
|
196
|
+
const totalJobs = Object.keys(this.jobs).length;
|
|
197
|
+
const testedJobs = Object.values(this.jobs).filter((j) => j.tested).length;
|
|
198
|
+
const passedJobs = Object.values(this.jobs).filter((j) =>
|
|
199
|
+
j.result.startsWith('✅')
|
|
200
|
+
).length;
|
|
201
|
+
|
|
202
|
+
const totalSteps = Object.values(this.jobs).reduce(
|
|
203
|
+
(sum, job) => sum + Object.keys(job.steps).length,
|
|
204
|
+
0
|
|
205
|
+
);
|
|
206
|
+
const testedSteps = Object.values(this.jobs).reduce(
|
|
207
|
+
(sum, job) =>
|
|
208
|
+
sum + Object.values(job.steps).filter((step) => step.tested).length,
|
|
209
|
+
0
|
|
210
|
+
);
|
|
211
|
+
const passedSteps = Object.values(this.jobs).reduce(
|
|
212
|
+
(sum, job) =>
|
|
213
|
+
sum +
|
|
214
|
+
Object.values(job.steps).filter((step) => step.result.startsWith('✅'))
|
|
215
|
+
.length,
|
|
216
|
+
0
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const totalArtifacts = Object.keys(this.artifacts).length;
|
|
220
|
+
const testedArtifacts = Object.values(this.artifacts).filter(
|
|
221
|
+
(a) => a.tested
|
|
222
|
+
).length;
|
|
223
|
+
const passedArtifacts = Object.values(this.artifacts).filter((a) =>
|
|
224
|
+
a.result.startsWith('✅')
|
|
225
|
+
).length;
|
|
226
|
+
|
|
227
|
+
lines.push('## 📊 Summary');
|
|
228
|
+
lines.push('');
|
|
229
|
+
lines.push('| Metric | Count |');
|
|
230
|
+
lines.push('|--------|-------|');
|
|
231
|
+
lines.push(`| Total Jobs | ${totalJobs} |`);
|
|
232
|
+
lines.push(`| Jobs Tested | ${testedJobs} |`);
|
|
233
|
+
lines.push(`| Jobs Passed | ${passedJobs} |`);
|
|
234
|
+
lines.push(`| Total Steps | ${totalSteps} |`);
|
|
235
|
+
lines.push(`| Steps Tested | ${testedSteps} |`);
|
|
236
|
+
lines.push(`| Steps Passed | ${passedSteps} |`);
|
|
237
|
+
lines.push(`| Total Artifacts | ${totalArtifacts} |`);
|
|
238
|
+
lines.push(`| Artifacts Tested | ${testedArtifacts} |`);
|
|
239
|
+
lines.push(`| Artifacts Passed | ${passedArtifacts} |`);
|
|
240
|
+
lines.push('');
|
|
241
|
+
|
|
242
|
+
// Job details
|
|
243
|
+
lines.push('## 📋 Detailed Test Results');
|
|
244
|
+
lines.push('');
|
|
245
|
+
|
|
246
|
+
for (const [jobName, jobInfo] of Object.entries(this.jobs)) {
|
|
247
|
+
const testedIcon = jobInfo.tested ? '✅' : '⬜';
|
|
248
|
+
|
|
249
|
+
lines.push(`### ${testedIcon} ${jobName}`);
|
|
250
|
+
lines.push('');
|
|
251
|
+
|
|
252
|
+
// Job-level table
|
|
253
|
+
lines.push('| Type | Name | Tested | Result |');
|
|
254
|
+
lines.push('|------|------|--------|--------|');
|
|
255
|
+
lines.push(
|
|
256
|
+
`| **Job** | ${jobName} | ${jobInfo.tested ? '✅ Yes' : '❌ No'} | ${jobInfo.result} |`
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// Steps table
|
|
260
|
+
if (Object.keys(jobInfo.steps).length > 0) {
|
|
261
|
+
for (const [stepName, stepInfo] of Object.entries(jobInfo.steps)) {
|
|
262
|
+
const tested = stepInfo.tested ? '✅ Yes' : '❌ No';
|
|
263
|
+
lines.push(`| Step | ${stepName} | ${tested} | ${stepInfo.result} |`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
lines.push('');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Artifacts section
|
|
271
|
+
if (Object.keys(this.artifacts).length > 0) {
|
|
272
|
+
lines.push('## 📦 Artifacts');
|
|
273
|
+
lines.push('');
|
|
274
|
+
lines.push('| Artifact | Tested | Overall Result | Checks |');
|
|
275
|
+
lines.push('|----------|--------|----------------|--------|');
|
|
276
|
+
|
|
277
|
+
for (const [artifactName, artifactInfo] of Object.entries(this.artifacts)) {
|
|
278
|
+
const tested = artifactInfo.tested ? '✅ Yes' : '❌ No';
|
|
279
|
+
const result = artifactInfo.result;
|
|
280
|
+
|
|
281
|
+
// Combine all checks into one cell
|
|
282
|
+
const checks =
|
|
283
|
+
artifactInfo.checks.length > 0
|
|
284
|
+
? artifactInfo.checks.join('<br>')
|
|
285
|
+
: 'No checks performed';
|
|
286
|
+
|
|
287
|
+
lines.push(`| ${artifactName} | ${tested} | ${result} | ${checks} |`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
lines.push('');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return lines.join('\n');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
writeToGithubSummary(): void {
|
|
297
|
+
console.log('\n🔍 Attempting to write test report...');
|
|
298
|
+
|
|
299
|
+
const summaryFile = process.env.GITHUB_STEP_SUMMARY;
|
|
300
|
+
|
|
301
|
+
if (!summaryFile) {
|
|
302
|
+
console.log('⚠️ GITHUB_STEP_SUMMARY environment variable not set');
|
|
303
|
+
console.log(' Report will only be printed to console');
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
console.log(`📝 Writing report to: ${summaryFile}`);
|
|
308
|
+
|
|
309
|
+
const markdown = this.generateMarkdown();
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
fs.appendFileSync(summaryFile, markdown);
|
|
313
|
+
console.log('✅ Test report successfully written to GitHub Actions summary');
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.log(`❌ Failed to write report: ${error}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
printReport(): void {
|
|
320
|
+
console.log('\n' + '='.repeat(80));
|
|
321
|
+
console.log(this.generateMarkdown());
|
|
322
|
+
console.log('='.repeat(80) + '\n');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Global reporter instance
|
|
327
|
+
let globalReporter: WorkflowTestReporter | null = null;
|
|
328
|
+
|
|
329
|
+
export function getReporter(workflowName?: string): WorkflowTestReporter {
|
|
330
|
+
if (!globalReporter) {
|
|
331
|
+
if (!workflowName) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
'Reporter not initialized. Call getReporter(workflowName) first.'
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
globalReporter = new WorkflowTestReporter(workflowName);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return globalReporter;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function resetReporter(): void {
|
|
343
|
+
globalReporter = null;
|
|
344
|
+
}
|