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.
@@ -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
+ }