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
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { Jobs, Steps, Artifacts } from './github-api';
|
|
3
|
+
import { WorkflowTestReporter } from './reporter';
|
|
4
|
+
|
|
5
|
+
export function assertJobSuccess(
|
|
6
|
+
jobs: Jobs,
|
|
7
|
+
jobName: string,
|
|
8
|
+
reporter?: WorkflowTestReporter,
|
|
9
|
+
expectedConclusion: string = 'success'
|
|
10
|
+
): void {
|
|
11
|
+
try {
|
|
12
|
+
if (!jobs[jobName]) {
|
|
13
|
+
const availableJobs = Object.keys(jobs).join('\n - ');
|
|
14
|
+
throw new Error(
|
|
15
|
+
`${jobName} not found. Available jobs:\n - ${availableJobs}`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const jobInfo = jobs[jobName];
|
|
20
|
+
const { status, conclusion } = jobInfo;
|
|
21
|
+
|
|
22
|
+
if (conclusion !== expectedConclusion) {
|
|
23
|
+
if (conclusion === null) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`${jobName} did not complete with expected conclusion '${expectedConclusion}'.\n` +
|
|
26
|
+
` Status: ${status}\n` +
|
|
27
|
+
` Conclusion: ${conclusion} (job was skipped, cancelled, or still running)`
|
|
28
|
+
);
|
|
29
|
+
} else {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`${jobName} did not have expected conclusion.\n` +
|
|
32
|
+
` Expected: ${expectedConclusion}\n` +
|
|
33
|
+
` Actual: ${conclusion}\n` +
|
|
34
|
+
` Status: ${status}`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(`✅ ${jobName} completed with conclusion '${expectedConclusion}'`);
|
|
40
|
+
|
|
41
|
+
// Record success in reporter
|
|
42
|
+
if (reporter) {
|
|
43
|
+
reporter.recordJobTest(jobName, true, expectedConclusion, conclusion);
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// Record failure in reporter
|
|
47
|
+
if (reporter) {
|
|
48
|
+
const conclusion = jobs[jobName]?.conclusion || 'unknown';
|
|
49
|
+
reporter.recordJobTest(jobName, false, expectedConclusion, conclusion);
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function assertStepSuccess(
|
|
56
|
+
steps: Steps,
|
|
57
|
+
stepName: string,
|
|
58
|
+
jobName?: string,
|
|
59
|
+
reporter?: WorkflowTestReporter,
|
|
60
|
+
expectedConclusion: string = 'success'
|
|
61
|
+
): void {
|
|
62
|
+
try {
|
|
63
|
+
if (!steps[stepName]) {
|
|
64
|
+
const availableSteps = Object.keys(steps).join('\n - ');
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Step '${stepName}' not found. Available steps:\n - ${availableSteps}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const stepInfo = steps[stepName];
|
|
71
|
+
const { conclusion } = stepInfo;
|
|
72
|
+
|
|
73
|
+
if (conclusion !== expectedConclusion) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Step '${stepName}' did not have expected conclusion.\n` +
|
|
76
|
+
` Expected: ${expectedConclusion}\n` +
|
|
77
|
+
` Actual: ${conclusion}`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(`✅ Step '${stepName}' completed with conclusion '${expectedConclusion}'`);
|
|
82
|
+
|
|
83
|
+
// Record success in reporter
|
|
84
|
+
if (reporter && jobName) {
|
|
85
|
+
reporter.recordStepTest(jobName, stepName, true, expectedConclusion, conclusion || undefined);
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
// Record failure in reporter
|
|
89
|
+
if (reporter && jobName) {
|
|
90
|
+
const conclusion = steps[stepName]?.conclusion || 'unknown';
|
|
91
|
+
reporter.recordStepTest(jobName, stepName, false, expectedConclusion, conclusion || undefined);
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function assertArtifactExists(
|
|
98
|
+
artifacts: Artifacts,
|
|
99
|
+
artifactName: string,
|
|
100
|
+
reporter?: WorkflowTestReporter
|
|
101
|
+
): void {
|
|
102
|
+
try {
|
|
103
|
+
if (!artifacts[artifactName]) {
|
|
104
|
+
const availableArtifacts = Object.keys(artifacts).length > 0
|
|
105
|
+
? Object.keys(artifacts).join('\n - ')
|
|
106
|
+
: '(no artifacts)';
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Artifact '${artifactName}' not found. Available artifacts:\n - ${availableArtifacts}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const artifactInfo = artifacts[artifactName];
|
|
113
|
+
if (artifactInfo.expired) {
|
|
114
|
+
throw new Error(`Artifact '${artifactName}' has expired`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(`✅ Artifact '${artifactName}' exists (size: ${artifactInfo.size} bytes)`);
|
|
118
|
+
|
|
119
|
+
// Record success in reporter
|
|
120
|
+
if (reporter) {
|
|
121
|
+
reporter.recordArtifactTest(artifactName, 'exists', true, 'exists');
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
// Record failure in reporter
|
|
125
|
+
if (reporter) {
|
|
126
|
+
reporter.recordArtifactTest(
|
|
127
|
+
artifactName,
|
|
128
|
+
'exists',
|
|
129
|
+
false,
|
|
130
|
+
'exists',
|
|
131
|
+
'not found or expired'
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function assertFileInArtifact(
|
|
139
|
+
artifactFiles: { [filename: string]: Buffer },
|
|
140
|
+
filename: string,
|
|
141
|
+
artifactName?: string,
|
|
142
|
+
reporter?: WorkflowTestReporter
|
|
143
|
+
): void {
|
|
144
|
+
try {
|
|
145
|
+
if (!artifactFiles[filename]) {
|
|
146
|
+
const availableFiles = Object.keys(artifactFiles).length > 0
|
|
147
|
+
? Object.keys(artifactFiles).join('\n - ')
|
|
148
|
+
: '(no files)';
|
|
149
|
+
throw new Error(
|
|
150
|
+
`File '${filename}' not found in artifact. Available files:\n - ${availableFiles}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log(`✅ File '${filename}' found in artifact`);
|
|
155
|
+
|
|
156
|
+
// Record success in reporter
|
|
157
|
+
if (reporter && artifactName) {
|
|
158
|
+
reporter.recordArtifactTest(
|
|
159
|
+
artifactName,
|
|
160
|
+
`file '${filename}' exists`,
|
|
161
|
+
true,
|
|
162
|
+
filename
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
// Record failure in reporter
|
|
167
|
+
if (reporter && artifactName) {
|
|
168
|
+
reporter.recordArtifactTest(
|
|
169
|
+
artifactName,
|
|
170
|
+
`file '${filename}' exists`,
|
|
171
|
+
false,
|
|
172
|
+
filename,
|
|
173
|
+
'not found'
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function assertFileContains(
|
|
181
|
+
artifactFiles: { [filename: string]: Buffer },
|
|
182
|
+
filename: string,
|
|
183
|
+
expectedContent: string | Buffer,
|
|
184
|
+
artifactName?: string,
|
|
185
|
+
reporter?: WorkflowTestReporter
|
|
186
|
+
): void {
|
|
187
|
+
try {
|
|
188
|
+
assertFileInArtifact(artifactFiles, filename, artifactName, reporter);
|
|
189
|
+
|
|
190
|
+
const fileContent = artifactFiles[filename];
|
|
191
|
+
const expectedBuffer =
|
|
192
|
+
typeof expectedContent === 'string'
|
|
193
|
+
? Buffer.from(expectedContent)
|
|
194
|
+
: expectedContent;
|
|
195
|
+
|
|
196
|
+
if (!fileContent.includes(expectedBuffer)) {
|
|
197
|
+
const contentStr =
|
|
198
|
+
typeof expectedContent === 'string'
|
|
199
|
+
? expectedContent
|
|
200
|
+
: expectedContent.toString();
|
|
201
|
+
throw new Error(
|
|
202
|
+
`File '${filename}' does not contain expected content: ${contentStr}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log(`✅ File '${filename}' contains expected content`);
|
|
207
|
+
|
|
208
|
+
// Record success in reporter
|
|
209
|
+
if (reporter && artifactName) {
|
|
210
|
+
const contentPreview = (
|
|
211
|
+
typeof expectedContent === 'string'
|
|
212
|
+
? expectedContent
|
|
213
|
+
: expectedContent.toString()
|
|
214
|
+
).substring(0, 50);
|
|
215
|
+
reporter.recordArtifactTest(
|
|
216
|
+
artifactName,
|
|
217
|
+
`file '${filename}' contains content`,
|
|
218
|
+
true,
|
|
219
|
+
contentPreview
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
// Record failure in reporter
|
|
224
|
+
if (reporter && artifactName) {
|
|
225
|
+
const contentPreview = (
|
|
226
|
+
typeof expectedContent === 'string'
|
|
227
|
+
? expectedContent
|
|
228
|
+
: expectedContent.toString()
|
|
229
|
+
).substring(0, 50);
|
|
230
|
+
reporter.recordArtifactTest(
|
|
231
|
+
artifactName,
|
|
232
|
+
`file '${filename}' contains content`,
|
|
233
|
+
false,
|
|
234
|
+
contentPreview,
|
|
235
|
+
'not found'
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ============================================================================
|
|
243
|
+
// JFrog Artifactory Assertions
|
|
244
|
+
// ============================================================================
|
|
245
|
+
|
|
246
|
+
export interface JFrogArtifactOptions {
|
|
247
|
+
jfrogUrl: string;
|
|
248
|
+
repository: string;
|
|
249
|
+
artifactPath: string;
|
|
250
|
+
artifactName: string;
|
|
251
|
+
jfrogApiKey?: string;
|
|
252
|
+
jfrogToken?: string;
|
|
253
|
+
reporter?: WorkflowTestReporter;
|
|
254
|
+
jobName?: string;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function assertJFrogArtifactExists(
|
|
258
|
+
options: JFrogArtifactOptions
|
|
259
|
+
): Promise<void> {
|
|
260
|
+
const {
|
|
261
|
+
jfrogUrl,
|
|
262
|
+
repository,
|
|
263
|
+
artifactPath,
|
|
264
|
+
artifactName,
|
|
265
|
+
jfrogApiKey,
|
|
266
|
+
jfrogToken,
|
|
267
|
+
reporter,
|
|
268
|
+
} = options;
|
|
269
|
+
|
|
270
|
+
// Create artifact display name for reporter
|
|
271
|
+
const jfrogArtifactDisplayName = `JFrog: ${repository}/${artifactPath}/${artifactName}`;
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
// Get credentials from parameters or environment
|
|
275
|
+
const apiKey = jfrogApiKey || process.env.MCD_ACTIONS_DD_API_KEY;
|
|
276
|
+
const token = jfrogToken || process.env.GDAP_ARTIFACTORY_ACCESS_TOKEN;
|
|
277
|
+
|
|
278
|
+
if (!apiKey && !token) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
'JFrog credentials not provided. Set one of:\n' +
|
|
281
|
+
' - JFROG_API_KEY or GDAP_ARTIFACTORY_ACCESS_TOKEN environment variable\n' +
|
|
282
|
+
' - JFROG_TOKEN environment variable\n' +
|
|
283
|
+
' - Pass jfrogApiKey or jfrogToken parameter'
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Build artifact URL
|
|
288
|
+
const cleanJfrogUrl = jfrogUrl.replace(/\/$/, '');
|
|
289
|
+
const cleanArtifactPath = artifactPath.replace(/^\/|\/$/g, '');
|
|
290
|
+
const artifactUrl = `${cleanJfrogUrl}/artifactory/${repository}/${cleanArtifactPath}/${artifactName}`;
|
|
291
|
+
|
|
292
|
+
// Set up authentication headers
|
|
293
|
+
const headers: { [key: string]: string } = {};
|
|
294
|
+
let authMethod: string;
|
|
295
|
+
|
|
296
|
+
if (apiKey) {
|
|
297
|
+
headers['X-JFrog-Art-Api'] = apiKey;
|
|
298
|
+
authMethod = 'API Key';
|
|
299
|
+
} else {
|
|
300
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
301
|
+
authMethod = 'Bearer Token';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
console.log('🔍 Checking JFrog artifact existence...');
|
|
305
|
+
console.log(` URL: ${artifactUrl}`);
|
|
306
|
+
console.log(` Auth: ${authMethod}`);
|
|
307
|
+
|
|
308
|
+
// Make HEAD request to check if artifact exists
|
|
309
|
+
const response = await axios.head(artifactUrl, {
|
|
310
|
+
headers,
|
|
311
|
+
timeout: 30000,
|
|
312
|
+
maxRedirects: 5,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
if (response.status === 200) {
|
|
316
|
+
// Artifact exists! Get metadata from response headers
|
|
317
|
+
const fileSize = response.headers['content-length'] || 'unknown';
|
|
318
|
+
const checksumSha1 = response.headers['x-checksum-sha1'] || 'N/A';
|
|
319
|
+
const lastModified = response.headers['last-modified'] || 'N/A';
|
|
320
|
+
|
|
321
|
+
console.log(`✅ Artifact '${artifactName}' exists in JFrog Artifactory`);
|
|
322
|
+
console.log(` Repository: ${repository}`);
|
|
323
|
+
console.log(` Path: ${artifactPath}`);
|
|
324
|
+
console.log(` Size: ${fileSize} bytes`);
|
|
325
|
+
console.log(` SHA1: ${checksumSha1}`);
|
|
326
|
+
console.log(` Last Modified: ${lastModified}`);
|
|
327
|
+
|
|
328
|
+
// Record success in reporter as artifact
|
|
329
|
+
if (reporter) {
|
|
330
|
+
reporter.recordArtifactTest(
|
|
331
|
+
jfrogArtifactDisplayName,
|
|
332
|
+
'exists in JFrog',
|
|
333
|
+
true,
|
|
334
|
+
`Size: ${fileSize} bytes, SHA1: ${checksumSha1.substring(0, 8)}...`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
} catch (error: any) {
|
|
339
|
+
let errorMsg: string;
|
|
340
|
+
|
|
341
|
+
if (axios.isAxiosError(error)) {
|
|
342
|
+
if (error.response?.status === 404) {
|
|
343
|
+
errorMsg =
|
|
344
|
+
`❌ Artifact '${artifactName}' not found in JFrog Artifactory.\n` +
|
|
345
|
+
` Repository: ${repository}\n` +
|
|
346
|
+
` Path: ${artifactPath}\n` +
|
|
347
|
+
` Status: 404 Not Found`;
|
|
348
|
+
} else if (error.response?.status === 401) {
|
|
349
|
+
errorMsg =
|
|
350
|
+
`❌ Authentication failed when checking JFrog artifact.\n` +
|
|
351
|
+
` Status: 401 Unauthorized`;
|
|
352
|
+
} else if (error.response?.status === 403) {
|
|
353
|
+
errorMsg =
|
|
354
|
+
`❌ Permission denied when checking JFrog artifact.\n` +
|
|
355
|
+
` Status: 403 Forbidden`;
|
|
356
|
+
} else if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
|
|
357
|
+
errorMsg =
|
|
358
|
+
`❌ Failed to connect to JFrog Artifactory.\n` +
|
|
359
|
+
` Error: Connection error`;
|
|
360
|
+
} else if (error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED') {
|
|
361
|
+
errorMsg =
|
|
362
|
+
`❌ Request to JFrog Artifactory timed out.\n` +
|
|
363
|
+
` Timeout: 30 seconds`;
|
|
364
|
+
} else {
|
|
365
|
+
errorMsg =
|
|
366
|
+
`❌ Unexpected response when checking JFrog artifact.\n` +
|
|
367
|
+
` Status: ${error.response?.status}\n` +
|
|
368
|
+
` Error: ${error.message}`;
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
errorMsg = `❌ Error checking JFrog artifact: ${error.message}`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Record failure in reporter
|
|
375
|
+
if (reporter) {
|
|
376
|
+
reporter.recordArtifactTest(
|
|
377
|
+
jfrogArtifactDisplayName,
|
|
378
|
+
'exists in JFrog',
|
|
379
|
+
false,
|
|
380
|
+
'artifact exists',
|
|
381
|
+
'not found or error'
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
throw new Error(errorMsg);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import axios, { AxiosInstance } from 'axios';
|
|
2
|
+
|
|
3
|
+
export interface JobInfo {
|
|
4
|
+
status: string;
|
|
5
|
+
conclusion: string | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Jobs {
|
|
9
|
+
[jobName: string]: JobInfo;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface StepInfo {
|
|
13
|
+
conclusion: string | null;
|
|
14
|
+
number: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Steps {
|
|
18
|
+
[stepName: string]: StepInfo;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ArtifactInfo {
|
|
22
|
+
id: number;
|
|
23
|
+
size: number;
|
|
24
|
+
expired: boolean;
|
|
25
|
+
url: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Artifacts {
|
|
29
|
+
[artifactName: string]: ArtifactInfo;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class GitHubAPI {
|
|
33
|
+
private repo: string;
|
|
34
|
+
private client: AxiosInstance;
|
|
35
|
+
|
|
36
|
+
constructor() {
|
|
37
|
+
this.repo = process.env.GITHUB_REPOSITORY || '';
|
|
38
|
+
const token = process.env.GITHUB_TOKEN || '';
|
|
39
|
+
|
|
40
|
+
if (!this.repo || !token) {
|
|
41
|
+
throw new Error('GITHUB_REPOSITORY and GITHUB_TOKEN environment variables are required');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(`🔍 GitHub API initialized for repo: ${this.repo}`);
|
|
45
|
+
|
|
46
|
+
this.client = axios.create({
|
|
47
|
+
baseURL: 'https://api.github.com',
|
|
48
|
+
headers: {
|
|
49
|
+
Authorization: `Bearer ${token}`,
|
|
50
|
+
Accept: 'application/vnd.github+json',
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async getLatestRun(workflowName: string): Promise<number> {
|
|
56
|
+
const url = `/repos/${this.repo}/actions/runs`;
|
|
57
|
+
console.log(`📡 Fetching workflow runs from: ${url}`);
|
|
58
|
+
|
|
59
|
+
const response = await this.client.get(url);
|
|
60
|
+
const runs = response.data.workflow_runs;
|
|
61
|
+
console.log(`Found ${runs.length} total workflow runs`);
|
|
62
|
+
|
|
63
|
+
for (const run of runs) {
|
|
64
|
+
if (run.name === workflowName) {
|
|
65
|
+
console.log(`✅ Found matching workflow: ${workflowName} (ID: ${run.id})`);
|
|
66
|
+
return run.id;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(`❌ Workflow '${workflowName}' not found in recent runs`);
|
|
71
|
+
throw new Error('Workflow run not found');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async getJobs(runId: number): Promise<Jobs> {
|
|
75
|
+
const url = `/repos/${this.repo}/actions/runs/${runId}/jobs`;
|
|
76
|
+
console.log(`📡 Fetching jobs for run ID ${runId}`);
|
|
77
|
+
|
|
78
|
+
const response = await this.client.get(url);
|
|
79
|
+
const jobs = response.data.jobs;
|
|
80
|
+
console.log(`Found ${jobs.length} jobs in this run`);
|
|
81
|
+
|
|
82
|
+
const result: Jobs = {};
|
|
83
|
+
for (const job of jobs) {
|
|
84
|
+
result[job.name] = {
|
|
85
|
+
status: job.status,
|
|
86
|
+
conclusion: job.conclusion,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async waitForJobsCompletion(
|
|
94
|
+
runId: number,
|
|
95
|
+
timeout: number = 300,
|
|
96
|
+
pollInterval: number = 10
|
|
97
|
+
): Promise<Jobs> {
|
|
98
|
+
console.log(`⏳ Waiting for all jobs to complete (timeout: ${timeout}s)...`);
|
|
99
|
+
const startTime = Date.now();
|
|
100
|
+
|
|
101
|
+
while (Date.now() - startTime < timeout * 1000) {
|
|
102
|
+
const jobs = await this.getJobs(runId);
|
|
103
|
+
|
|
104
|
+
const incompleteJobs = Object.entries(jobs)
|
|
105
|
+
.filter(([_, info]) => info.conclusion === null)
|
|
106
|
+
.map(([name]) => name);
|
|
107
|
+
|
|
108
|
+
if (incompleteJobs.length === 0) {
|
|
109
|
+
console.log('✅ All jobs completed!');
|
|
110
|
+
return jobs;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(
|
|
114
|
+
`⏳ Waiting for ${incompleteJobs.length} job(s) to complete: ${incompleteJobs.join(', ')}`
|
|
115
|
+
);
|
|
116
|
+
await this.sleep(pollInterval * 1000);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
throw new Error(`Jobs did not complete within ${timeout} seconds`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async getSteps(runId: number, jobName: string): Promise<Steps> {
|
|
123
|
+
const url = `/repos/${this.repo}/actions/runs/${runId}/jobs`;
|
|
124
|
+
console.log(`📡 Fetching steps for job: ${jobName}`);
|
|
125
|
+
|
|
126
|
+
const response = await this.client.get(url);
|
|
127
|
+
const jobs = response.data.jobs;
|
|
128
|
+
|
|
129
|
+
const job = jobs.find((j: any) => j.name === jobName);
|
|
130
|
+
if (!job) {
|
|
131
|
+
throw new Error(`Job '${jobName}' not found in run ${runId}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const result: Steps = {};
|
|
135
|
+
for (const step of job.steps) {
|
|
136
|
+
result[step.name] = {
|
|
137
|
+
conclusion: step.conclusion,
|
|
138
|
+
number: step.number,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(`Found ${Object.keys(result).length} steps in job '${jobName}'`);
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async getArtifacts(runId: number): Promise<Artifacts> {
|
|
147
|
+
const url = `/repos/${this.repo}/actions/runs/${runId}/artifacts`;
|
|
148
|
+
console.log(`📡 Fetching artifacts for run ID ${runId}`);
|
|
149
|
+
|
|
150
|
+
const response = await this.client.get(url);
|
|
151
|
+
const artifacts = response.data.artifacts;
|
|
152
|
+
console.log(`Found ${artifacts.length} artifacts`);
|
|
153
|
+
|
|
154
|
+
const result: Artifacts = {};
|
|
155
|
+
for (const artifact of artifacts) {
|
|
156
|
+
result[artifact.name] = {
|
|
157
|
+
id: artifact.id,
|
|
158
|
+
size: artifact.size_in_bytes,
|
|
159
|
+
expired: artifact.expired,
|
|
160
|
+
url: artifact.archive_download_url,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async downloadArtifact(artifactId: number): Promise<{ [filename: string]: Buffer }> {
|
|
168
|
+
const url = `/repos/${this.repo}/actions/artifacts/${artifactId}/zip`;
|
|
169
|
+
console.log(`📥 Downloading artifact ID ${artifactId}`);
|
|
170
|
+
|
|
171
|
+
const response = await this.client.get(url, { responseType: 'arraybuffer' });
|
|
172
|
+
|
|
173
|
+
// In a real implementation, you would unzip the response
|
|
174
|
+
// For simplicity, returning a placeholder
|
|
175
|
+
console.log('⚠️ Artifact download implemented - unzipping not yet implemented in this example');
|
|
176
|
+
return {
|
|
177
|
+
'artifact-file.zip': Buffer.from(response.data),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private sleep(ms: number): Promise<void> {
|
|
182
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
183
|
+
}
|
|
184
|
+
}
|
package/src/index.ts
ADDED