bktide 1.0.1755568192 → 1.0.1755639164
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/README.md +42 -0
- package/dist/commands/ShowBuild.js +27 -5
- package/dist/commands/ShowBuild.js.map +1 -1
- package/dist/formatters/build-detail/PlainTextFormatter.js +310 -128
- package/dist/formatters/build-detail/PlainTextFormatter.js.map +1 -1
- package/dist/graphql/fragments/index.js +3 -0
- package/dist/graphql/fragments/index.js.map +1 -0
- package/dist/graphql/fragments/jobs.js +112 -0
- package/dist/graphql/fragments/jobs.js.map +1 -0
- package/dist/graphql/queries.js +35 -57
- package/dist/graphql/queries.js.map +1 -1
- package/dist/scripts/extract-data-patterns.js +118 -0
- package/dist/scripts/extract-data-patterns.js.map +1 -0
- package/dist/services/BuildkiteClient.js +77 -1
- package/dist/services/BuildkiteClient.js.map +1 -1
- package/dist/test-helpers/DataProfiler.js +307 -0
- package/dist/test-helpers/DataProfiler.js.map +1 -0
- package/dist/test-helpers/PatternMockGenerator.js +590 -0
- package/dist/test-helpers/PatternMockGenerator.js.map +1 -0
- package/package.json +19 -3
|
@@ -140,10 +140,17 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
140
140
|
// Header line
|
|
141
141
|
lines.push(this.formatHeader(build));
|
|
142
142
|
lines.push(this.formatCommitInfo(build));
|
|
143
|
+
lines.push(''); // Blank line after commit info
|
|
143
144
|
// Show annotations summary if present
|
|
144
145
|
if (build.annotations?.edges?.length > 0) {
|
|
145
|
-
lines.push('');
|
|
146
146
|
lines.push(this.formatAnnotationSummary(build.annotations.edges));
|
|
147
|
+
}
|
|
148
|
+
// Jobs summary
|
|
149
|
+
if (build.jobs?.edges?.length > 0) {
|
|
150
|
+
if (build.annotations?.edges?.length > 0) {
|
|
151
|
+
lines.push(''); // Add space between annotations and steps
|
|
152
|
+
}
|
|
153
|
+
lines.push(this.formatJobSummary(build.jobs, build.state));
|
|
147
154
|
if (!options?.annotations) {
|
|
148
155
|
lines.push('');
|
|
149
156
|
const tips = formatTips(['Use --annotations to view annotation details'], TipStyle.GROUPED);
|
|
@@ -163,18 +170,18 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
163
170
|
lines.push(this.formatHeader(build));
|
|
164
171
|
lines.push(this.formatCommitInfo(build));
|
|
165
172
|
lines.push('');
|
|
166
|
-
// Failed jobs summary
|
|
167
|
-
const failedJobs = this.getFailedJobs(build.jobs?.edges);
|
|
168
173
|
const allHints = [];
|
|
169
|
-
|
|
170
|
-
const { summary, hints } = this.formatFailedJobsSummaryWithHints(failedJobs, options);
|
|
171
|
-
lines.push(summary);
|
|
172
|
-
allHints.push(...hints);
|
|
173
|
-
}
|
|
174
|
-
// Annotation summary
|
|
174
|
+
// Annotation summary (first, as it appears first in UI)
|
|
175
175
|
if (build.annotations?.edges?.length > 0) {
|
|
176
176
|
lines.push(this.formatAnnotationSummary(build.annotations.edges));
|
|
177
177
|
}
|
|
178
|
+
// Jobs summary
|
|
179
|
+
if (build.jobs?.edges?.length > 0) {
|
|
180
|
+
if (build.annotations?.edges?.length > 0) {
|
|
181
|
+
lines.push(''); // Add space between annotations and steps
|
|
182
|
+
}
|
|
183
|
+
lines.push(this.formatJobSummary(build.jobs, build.state));
|
|
184
|
+
}
|
|
178
185
|
// Show detailed job info if requested
|
|
179
186
|
if (options?.jobs || options?.failed) {
|
|
180
187
|
lines.push('');
|
|
@@ -186,12 +193,17 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
186
193
|
lines.push(this.formatAnnotationDetails(build.annotations.edges));
|
|
187
194
|
}
|
|
188
195
|
// Collect all hints for more info
|
|
196
|
+
const failedJobs = this.getFailedJobs(build.jobs?.edges);
|
|
189
197
|
if (!options?.failed && failedJobs.length > 0) {
|
|
190
198
|
allHints.push('Use --failed to show failure details');
|
|
191
199
|
}
|
|
192
200
|
if (!options?.annotations && build.annotations?.edges?.length > 0) {
|
|
193
201
|
allHints.push('Use --annotations to view annotation details');
|
|
194
202
|
}
|
|
203
|
+
// Add hint about incomplete step data if truncated
|
|
204
|
+
if (!options?.jobs && build.jobs?.pageInfo?.hasNextPage) {
|
|
205
|
+
allHints.push('Use --jobs to fetch all step data (currently showing first 100 only)');
|
|
206
|
+
}
|
|
195
207
|
// Display all hints together
|
|
196
208
|
if (allHints.length > 0) {
|
|
197
209
|
lines.push('');
|
|
@@ -205,9 +217,17 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
205
217
|
lines.push(this.formatHeader(build));
|
|
206
218
|
lines.push(this.formatCommitInfo(build));
|
|
207
219
|
lines.push('');
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
220
|
+
// Annotations first (if any)
|
|
221
|
+
if (build.annotations?.edges?.length > 0) {
|
|
222
|
+
lines.push(this.formatAnnotationSummary(build.annotations.edges));
|
|
223
|
+
}
|
|
224
|
+
// Jobs summary with progress
|
|
225
|
+
if (build.jobs?.edges?.length > 0) {
|
|
226
|
+
if (build.annotations?.edges?.length > 0) {
|
|
227
|
+
lines.push(''); // Add space between annotations and steps
|
|
228
|
+
}
|
|
229
|
+
lines.push(this.formatJobSummary(build.jobs, build.state));
|
|
230
|
+
}
|
|
211
231
|
// Show running jobs
|
|
212
232
|
const runningJobs = this.getRunningJobs(build.jobs?.edges);
|
|
213
233
|
if (runningJobs.length > 0) {
|
|
@@ -237,20 +257,22 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
237
257
|
lines.push(this.formatHeader(build));
|
|
238
258
|
lines.push(this.formatCommitInfo(build));
|
|
239
259
|
lines.push('');
|
|
260
|
+
// Annotations first (if any)
|
|
261
|
+
if (build.annotations?.edges?.length > 0) {
|
|
262
|
+
lines.push(this.formatAnnotationSummary(build.annotations.edges));
|
|
263
|
+
}
|
|
264
|
+
// Jobs summary
|
|
265
|
+
if (build.jobs?.edges?.length > 0) {
|
|
266
|
+
if (build.annotations?.edges?.length > 0) {
|
|
267
|
+
lines.push(''); // Add space between annotations and steps
|
|
268
|
+
}
|
|
269
|
+
lines.push(this.formatJobSummary(build.jobs, build.state));
|
|
270
|
+
}
|
|
240
271
|
// Blocked information
|
|
241
272
|
const blockedJobs = this.getBlockedJobs(build.jobs?.edges);
|
|
242
273
|
if (blockedJobs.length > 0) {
|
|
243
|
-
lines.push(`${getProgressIcon('BLOCKED_MESSAGE')} Blocked: "${blockedJobs[0].node.label}" (manual unblock required)`);
|
|
244
|
-
}
|
|
245
|
-
// Show jobs summary
|
|
246
|
-
const jobStats = this.getJobStats(build.jobs?.edges);
|
|
247
|
-
if (jobStats.completed > 0) {
|
|
248
|
-
lines.push(`${getStateIcon('PASSED')} ${jobStats.completed} jobs passed before block`);
|
|
249
|
-
}
|
|
250
|
-
// Annotation summary
|
|
251
|
-
if (build.annotations?.edges?.length > 0) {
|
|
252
274
|
lines.push('');
|
|
253
|
-
lines.push(
|
|
275
|
+
lines.push(`${getProgressIcon('BLOCKED_MESSAGE')} Blocked: "${blockedJobs[0].node.label}" (manual unblock required)`);
|
|
254
276
|
}
|
|
255
277
|
// Show job details if requested
|
|
256
278
|
if (options?.jobs) {
|
|
@@ -270,14 +292,23 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
270
292
|
lines.push(this.formatHeader(build));
|
|
271
293
|
lines.push(this.formatCommitInfo(build));
|
|
272
294
|
lines.push('');
|
|
295
|
+
// Annotations first (if any)
|
|
296
|
+
if (build.annotations?.edges?.length > 0) {
|
|
297
|
+
lines.push(this.formatAnnotationSummary(build.annotations.edges));
|
|
298
|
+
}
|
|
299
|
+
// Jobs summary
|
|
300
|
+
if (build.jobs?.edges?.length > 0) {
|
|
301
|
+
if (build.annotations?.edges?.length > 0) {
|
|
302
|
+
lines.push(''); // Add space between annotations and steps
|
|
303
|
+
}
|
|
304
|
+
lines.push(this.formatJobSummary(build.jobs, build.state));
|
|
305
|
+
}
|
|
273
306
|
// Canceled information
|
|
274
307
|
if (build.createdBy) {
|
|
308
|
+
lines.push('');
|
|
275
309
|
const creator = build.createdBy.name || build.createdBy.email;
|
|
276
310
|
lines.push(`Canceled by: ${creator}`);
|
|
277
311
|
}
|
|
278
|
-
// Show jobs summary
|
|
279
|
-
const jobStats = this.getJobStats(build.jobs?.edges);
|
|
280
|
-
lines.push(`Completed: ${jobStats.completed}/${jobStats.total} jobs before cancellation`);
|
|
281
312
|
// Show job details if requested
|
|
282
313
|
if (options?.jobs) {
|
|
283
314
|
lines.push('');
|
|
@@ -323,8 +354,8 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
323
354
|
lines.push(` Triggered from: ${build.triggeredFrom.pipeline?.name} #${build.triggeredFrom.number}`);
|
|
324
355
|
}
|
|
325
356
|
lines.push('');
|
|
326
|
-
//
|
|
327
|
-
lines.push('
|
|
357
|
+
// Steps section
|
|
358
|
+
lines.push('Steps:');
|
|
328
359
|
lines.push(this.formatJobDetails(build.jobs?.edges, { ...options, full: true }));
|
|
329
360
|
// Annotations section
|
|
330
361
|
if (build.annotations?.edges?.length > 0) {
|
|
@@ -340,15 +371,31 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
340
371
|
const coloredIcon = this.colorizeStatusIcon(statusIcon, build.state);
|
|
341
372
|
const stateFormatted = formatBuildStatus(build.state, { useSymbol: false });
|
|
342
373
|
const duration = this.formatDuration(build);
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
374
|
+
// Get first line of commit message
|
|
375
|
+
const message = build.message || 'No commit message';
|
|
376
|
+
const firstLineMessage = message.split('\n')[0];
|
|
377
|
+
const truncatedMessage = firstLineMessage.length > 80 ? firstLineMessage.substring(0, 77) + '...' : firstLineMessage;
|
|
378
|
+
return `${coloredIcon} ${stateFormatted} ${truncatedMessage} ${SEMANTIC_COLORS.dim(`#${build.number}`)} ${SEMANTIC_COLORS.dim(duration)}`;
|
|
346
379
|
}
|
|
347
380
|
formatCommitInfo(build) {
|
|
348
381
|
const shortSha = build.commit ? build.commit.substring(0, 7) : 'unknown';
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
|
|
382
|
+
const branch = SEMANTIC_COLORS.identifier(build.branch);
|
|
383
|
+
const age = this.formatAge(build.createdAt);
|
|
384
|
+
// Get author information
|
|
385
|
+
const author = build.createdBy?.name || build.createdBy?.email || 'Unknown';
|
|
386
|
+
// Calculate indentation to align with commit message
|
|
387
|
+
// Map each state to its proper indentation (icon + space + state text + space)
|
|
388
|
+
const indentMap = {
|
|
389
|
+
'PASSED': 9, // ✓ PASSED
|
|
390
|
+
'FAILED': 9, // ✗ FAILED
|
|
391
|
+
'RUNNING': 10, // ⟳ RUNNING
|
|
392
|
+
'BLOCKED': 10, // ◼ BLOCKED
|
|
393
|
+
'CANCELED': 11, // ⊘ CANCELED
|
|
394
|
+
'SCHEDULED': 12, // ⏱ SCHEDULED
|
|
395
|
+
'SKIPPED': 10, // ⊙ SKIPPED
|
|
396
|
+
};
|
|
397
|
+
const indent = ' '.repeat(indentMap[build.state] || 9);
|
|
398
|
+
return `${indent}${author} • ${branch} • ${shortSha} • ${SEMANTIC_COLORS.dim(`Created ${age}`)}`;
|
|
352
399
|
}
|
|
353
400
|
formatAnnotationSummary(annotations) {
|
|
354
401
|
if (!annotations || annotations.length === 0) {
|
|
@@ -383,6 +430,73 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
383
430
|
}
|
|
384
431
|
return lines.join('\n');
|
|
385
432
|
}
|
|
433
|
+
formatJobSummary(jobsData, buildState) {
|
|
434
|
+
const jobs = jobsData?.edges;
|
|
435
|
+
if (!jobs || jobs.length === 0) {
|
|
436
|
+
return '';
|
|
437
|
+
}
|
|
438
|
+
const lines = [];
|
|
439
|
+
const jobStats = this.getJobStats(jobs);
|
|
440
|
+
// Build summary parts based on job states
|
|
441
|
+
const countParts = [];
|
|
442
|
+
if (jobStats.failed > 0)
|
|
443
|
+
countParts.push(SEMANTIC_COLORS.error(`${jobStats.failed} failed`));
|
|
444
|
+
if (jobStats.passed > 0)
|
|
445
|
+
countParts.push(SEMANTIC_COLORS.success(`${jobStats.passed} passed`));
|
|
446
|
+
if (jobStats.running > 0)
|
|
447
|
+
countParts.push(SEMANTIC_COLORS.info(`${jobStats.running} running`));
|
|
448
|
+
if (jobStats.blocked > 0)
|
|
449
|
+
countParts.push(SEMANTIC_COLORS.warning(`${jobStats.blocked} blocked`));
|
|
450
|
+
// Don't show skipped jobs
|
|
451
|
+
if (jobStats.canceled > 0)
|
|
452
|
+
countParts.push(SEMANTIC_COLORS.muted(`${jobStats.canceled} canceled`));
|
|
453
|
+
// Use appropriate icon based on build state
|
|
454
|
+
const icon = buildState === 'FAILED' ? getStateIcon('FAILED') :
|
|
455
|
+
buildState === 'RUNNING' ? getStateIcon('RUNNING') :
|
|
456
|
+
buildState === 'PASSED' ? getStateIcon('PASSED') :
|
|
457
|
+
buildState === 'BLOCKED' ? getStateIcon('BLOCKED') : '•';
|
|
458
|
+
// Check if we have partial data
|
|
459
|
+
const hasMorePages = jobsData?.pageInfo?.hasNextPage;
|
|
460
|
+
const totalCount = jobsData?.count;
|
|
461
|
+
if (hasMorePages) {
|
|
462
|
+
const showing = jobs.length;
|
|
463
|
+
const total = totalCount || `${showing}+`;
|
|
464
|
+
lines.push(`${icon} Showing ${SEMANTIC_COLORS.count(String(showing))} of ${SEMANTIC_COLORS.count(String(total))} steps: ${countParts.join(', ')}`);
|
|
465
|
+
lines.push(SEMANTIC_COLORS.warning('⚠️ Showing first 100 steps only (more available)'));
|
|
466
|
+
lines.push(SEMANTIC_COLORS.dim(' → Use --jobs to fetch all step data and see accurate statistics'));
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
lines.push(`${icon} ${SEMANTIC_COLORS.count(String(jobStats.total))} step${jobStats.total > 1 ? 's' : ''}: ${countParts.join(', ')}`);
|
|
470
|
+
}
|
|
471
|
+
// For failed builds, show the specific failed job names
|
|
472
|
+
if (buildState === 'FAILED') {
|
|
473
|
+
const failedJobs = this.getFailedJobs(jobs);
|
|
474
|
+
const jobGroups = this.groupJobsByLabel(failedJobs);
|
|
475
|
+
// Show up to 3 failed job types
|
|
476
|
+
const displayGroups = jobGroups.slice(0, 3);
|
|
477
|
+
for (const group of displayGroups) {
|
|
478
|
+
const label = this.parseEmoji(group.label);
|
|
479
|
+
const icon = getStateIcon('FAILED');
|
|
480
|
+
// Get duration for display
|
|
481
|
+
const duration = group.count === 1 && group.jobs[0]?.node
|
|
482
|
+
? ` ${SEMANTIC_COLORS.dim(`- ran ${this.formatJobDuration(group.jobs[0].node)}`)}`
|
|
483
|
+
: '';
|
|
484
|
+
if (group.parallelTotal > 0) {
|
|
485
|
+
lines.push(` ${icon} ${label} ${SEMANTIC_COLORS.dim(`(${group.stateCounts.failed || 0}/${group.parallelTotal} failed)`)}`);
|
|
486
|
+
}
|
|
487
|
+
else if (group.count > 1) {
|
|
488
|
+
lines.push(` ${icon} ${label} ${SEMANTIC_COLORS.dim(`(${group.stateCounts.failed || 0} failed)`)}`);
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
lines.push(` ${icon} ${label}${duration}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (jobGroups.length > 3) {
|
|
495
|
+
lines.push(` ${SEMANTIC_COLORS.muted(`...and ${jobGroups.length - 3} more`)}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return lines.join('\n');
|
|
499
|
+
}
|
|
386
500
|
formatAnnotationDetails(annotations) {
|
|
387
501
|
const lines = [];
|
|
388
502
|
// Group annotations by style
|
|
@@ -409,7 +523,7 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
409
523
|
}
|
|
410
524
|
formatJobDetails(jobs, options) {
|
|
411
525
|
if (!jobs || jobs.length === 0) {
|
|
412
|
-
return 'No
|
|
526
|
+
return 'No steps found';
|
|
413
527
|
}
|
|
414
528
|
const lines = [];
|
|
415
529
|
const jobStats = this.getJobStats(jobs);
|
|
@@ -423,45 +537,83 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
423
537
|
parts.push(`${getStateIcon('RUNNING')} ${jobStats.running} running`);
|
|
424
538
|
if (jobStats.blocked > 0)
|
|
425
539
|
parts.push(`${getStateIcon('BLOCKED')} ${jobStats.blocked} blocked`);
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
lines.push(`Jobs: ${parts.join(' ')}`);
|
|
540
|
+
// Don't show skipped jobs in summary
|
|
541
|
+
lines.push(`Steps: ${parts.join(' ')}`);
|
|
429
542
|
lines.push('');
|
|
430
543
|
// Filter jobs based on options
|
|
431
544
|
let filteredJobs = jobs;
|
|
432
545
|
if (options?.failed) {
|
|
433
546
|
filteredJobs = this.getFailedJobs(jobs);
|
|
434
547
|
}
|
|
435
|
-
// Group jobs by state
|
|
548
|
+
// Group jobs by state first
|
|
436
549
|
const grouped = this.groupJobsByState(filteredJobs);
|
|
437
550
|
for (const [state, stateJobs] of Object.entries(grouped)) {
|
|
438
551
|
if (stateJobs.length === 0)
|
|
439
552
|
continue;
|
|
440
553
|
const icon = this.getJobStateIcon(state);
|
|
441
554
|
const stateColored = this.colorizeJobState(state);
|
|
555
|
+
// Collapse parallel jobs with same label
|
|
556
|
+
const collapsedGroups = this.collapseParallelJobs(stateJobs);
|
|
442
557
|
lines.push(`${icon} ${stateColored} (${SEMANTIC_COLORS.count(String(stateJobs.length))}):`);
|
|
443
|
-
for (const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
//
|
|
451
|
-
if (
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
558
|
+
for (const group of collapsedGroups) {
|
|
559
|
+
if (group.isParallelGroup && group.jobs.length > 1) {
|
|
560
|
+
// Collapsed parallel group display
|
|
561
|
+
const label = this.parseEmoji(group.label);
|
|
562
|
+
const total = group.parallelTotal || group.jobs.length;
|
|
563
|
+
const passedCount = group.jobs.filter(j => this.isJobPassed(j.node)).length;
|
|
564
|
+
const failedCount = group.jobs.filter(j => this.isJobFailed(j.node)).length;
|
|
565
|
+
// Show summary line for parallel group
|
|
566
|
+
if (failedCount > 0) {
|
|
567
|
+
// If there are failures, show breakdown
|
|
568
|
+
// Apply state color to label
|
|
569
|
+
const coloredLabel = state === 'Failed' ? SEMANTIC_COLORS.error(label) :
|
|
570
|
+
state === 'Passed' ? SEMANTIC_COLORS.success(label) :
|
|
571
|
+
state === 'Running' ? SEMANTIC_COLORS.info(label) :
|
|
572
|
+
state === 'Blocked' ? SEMANTIC_COLORS.warning(label) : label;
|
|
573
|
+
lines.push(` ${coloredLabel} ${SEMANTIC_COLORS.dim(`(${passedCount}/${total} passed, ${failedCount} failed)`)}`);
|
|
574
|
+
// Show failed steps individually
|
|
575
|
+
const failedJobs = group.jobs.filter(j => this.isJobFailed(j.node));
|
|
576
|
+
for (const job of failedJobs) {
|
|
577
|
+
const duration = this.formatJobDuration(job.node);
|
|
578
|
+
const parallelInfo = job.node.parallelGroupIndex !== undefined
|
|
579
|
+
? ` ${SEMANTIC_COLORS.dim(`[Parallel: ${job.node.parallelGroupIndex + 1}/${job.node.parallelGroupTotal}]`)}`
|
|
580
|
+
: '';
|
|
581
|
+
lines.push(` ${SEMANTIC_COLORS.error('↳ Failed')}: ${SEMANTIC_COLORS.dim(duration)}${parallelInfo}`);
|
|
582
|
+
}
|
|
457
583
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
584
|
+
else {
|
|
585
|
+
// All passed/running/blocked - just show summary
|
|
586
|
+
const avgDuration = this.calculateAverageDuration(group.jobs);
|
|
587
|
+
// Apply state color to label
|
|
588
|
+
const coloredLabel = state === 'Passed' ? SEMANTIC_COLORS.success(label) :
|
|
589
|
+
state === 'Failed' ? SEMANTIC_COLORS.error(label) :
|
|
590
|
+
state === 'Running' ? SEMANTIC_COLORS.info(label) :
|
|
591
|
+
state === 'Blocked' ? SEMANTIC_COLORS.warning(label) : label;
|
|
592
|
+
lines.push(` ${coloredLabel} ${SEMANTIC_COLORS.dim(`(${total} parallel steps, avg: ${avgDuration})`)}`);
|
|
461
593
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
// Single job or non-parallel group - display as before
|
|
597
|
+
const job = group.jobs[0];
|
|
598
|
+
const label = this.parseEmoji(job.node.label);
|
|
599
|
+
const duration = this.formatJobDuration(job.node);
|
|
600
|
+
// Apply state color to label
|
|
601
|
+
const coloredLabel = state === 'Passed' ? SEMANTIC_COLORS.success(label) :
|
|
602
|
+
state === 'Failed' ? SEMANTIC_COLORS.error(label) :
|
|
603
|
+
state === 'Running' ? SEMANTIC_COLORS.info(label) :
|
|
604
|
+
state === 'Blocked' ? SEMANTIC_COLORS.warning(label) : label;
|
|
605
|
+
// Add parallel info inline if present
|
|
606
|
+
const parallelInfo = (job.node.parallelGroupIndex !== undefined && job.node.parallelGroupTotal)
|
|
607
|
+
? ` ${SEMANTIC_COLORS.dim(`[Parallel: ${job.node.parallelGroupIndex + 1}/${job.node.parallelGroupTotal}]`)}`
|
|
608
|
+
: '';
|
|
609
|
+
// Basic step line with optional parallel info
|
|
610
|
+
lines.push(` ${coloredLabel} ${SEMANTIC_COLORS.dim(`(${duration})`)}${parallelInfo}`);
|
|
611
|
+
// Show additional details if --jobs or --full and single step
|
|
612
|
+
if ((options?.jobs || options?.full) && !group.isParallelGroup) {
|
|
613
|
+
// Retry info
|
|
614
|
+
if (job.node.retried) {
|
|
615
|
+
lines.push(` ${SEMANTIC_COLORS.warning(`${getProgressIcon('RETRY')} Retried`)}`);
|
|
616
|
+
}
|
|
465
617
|
}
|
|
466
618
|
}
|
|
467
619
|
}
|
|
@@ -469,67 +621,6 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
469
621
|
}
|
|
470
622
|
return lines.join('\n').trim();
|
|
471
623
|
}
|
|
472
|
-
formatFailedJobsSummaryWithHints(failedJobs, options) {
|
|
473
|
-
const hints = [];
|
|
474
|
-
const summary = this.formatFailedJobsSummary(failedJobs, options, hints);
|
|
475
|
-
return { summary, hints };
|
|
476
|
-
}
|
|
477
|
-
formatFailedJobsSummary(failedJobs, options, hints) {
|
|
478
|
-
const lines = [];
|
|
479
|
-
// Group identical jobs by label
|
|
480
|
-
const jobGroups = this.groupJobsByLabel(failedJobs);
|
|
481
|
-
// Show all groups if --all-jobs, otherwise limit to 10
|
|
482
|
-
const displayGroups = options?.allJobs
|
|
483
|
-
? jobGroups
|
|
484
|
-
: jobGroups.slice(0, 10);
|
|
485
|
-
for (const group of displayGroups) {
|
|
486
|
-
const label = this.parseEmoji(group.label);
|
|
487
|
-
if (group.count === 1) {
|
|
488
|
-
const duration = this.formatJobDuration(group.jobs[0].node);
|
|
489
|
-
lines.push(` ${SEMANTIC_COLORS.error('Failed')}: ${label} - ran ${duration}`);
|
|
490
|
-
}
|
|
491
|
-
else {
|
|
492
|
-
// Multiple jobs with same label - show detailed breakdown
|
|
493
|
-
const statusParts = [];
|
|
494
|
-
if (group.stateCounts.failed > 0) {
|
|
495
|
-
statusParts.push(`${group.stateCounts.failed} failed`);
|
|
496
|
-
}
|
|
497
|
-
if (group.stateCounts.broken > 0) {
|
|
498
|
-
statusParts.push(`${group.stateCounts.broken} broken`);
|
|
499
|
-
}
|
|
500
|
-
if (group.stateCounts.notStarted > 0) {
|
|
501
|
-
statusParts.push(`${group.stateCounts.notStarted} not started`);
|
|
502
|
-
}
|
|
503
|
-
if (group.stateCounts.passed > 0) {
|
|
504
|
-
statusParts.push(`${group.stateCounts.passed} passed`);
|
|
505
|
-
}
|
|
506
|
-
if (group.stateCounts.other > 0) {
|
|
507
|
-
statusParts.push(`${group.stateCounts.other} other`);
|
|
508
|
-
}
|
|
509
|
-
const statusInfo = statusParts.join(', ') || 'various states';
|
|
510
|
-
// Show parallel info if it's a parallel job group
|
|
511
|
-
const parallelInfo = group.parallelTotal > 0 ? ` (${group.count}/${group.parallelTotal} parallel)` : ` (${SEMANTIC_COLORS.count(String(group.count))} jobs)`;
|
|
512
|
-
lines.push(` ${SEMANTIC_COLORS.error('Failed')}: ${label}${parallelInfo}: ${statusInfo}`);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
// Add summary if there are more job types and not showing all
|
|
516
|
-
if (!options?.allJobs) {
|
|
517
|
-
const remaining = jobGroups.length - displayGroups.length;
|
|
518
|
-
if (remaining > 0) {
|
|
519
|
-
lines.push(` ${SEMANTIC_COLORS.muted(`...and ${remaining} more job types`)}`);
|
|
520
|
-
// If hints array is provided, add hint there; otherwise format inline
|
|
521
|
-
if (hints) {
|
|
522
|
-
hints.push('Use --all-jobs to show all jobs');
|
|
523
|
-
}
|
|
524
|
-
else {
|
|
525
|
-
lines.push('');
|
|
526
|
-
const tips = formatTips(['Use --all-jobs to show all jobs'], TipStyle.GROUPED);
|
|
527
|
-
lines.push(tips);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
return lines.join('\n');
|
|
532
|
-
}
|
|
533
624
|
groupJobsByLabel(jobs) {
|
|
534
625
|
const groups = new Map();
|
|
535
626
|
for (const job of jobs) {
|
|
@@ -688,6 +779,7 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
688
779
|
running: 0,
|
|
689
780
|
blocked: 0,
|
|
690
781
|
skipped: 0,
|
|
782
|
+
canceled: 0,
|
|
691
783
|
queued: 0,
|
|
692
784
|
completed: 0
|
|
693
785
|
};
|
|
@@ -714,7 +806,12 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
714
806
|
else if (state === 'BLOCKED') {
|
|
715
807
|
stats.blocked++;
|
|
716
808
|
}
|
|
717
|
-
else if (state === '
|
|
809
|
+
else if (state === 'CANCELED' || state === 'CANCELLED') {
|
|
810
|
+
stats.canceled++;
|
|
811
|
+
stats.completed++;
|
|
812
|
+
}
|
|
813
|
+
else if (state === 'SKIPPED' || state === 'BROKEN') {
|
|
814
|
+
// BROKEN jobs are functionally skipped - they don't run due to conditions not matching
|
|
718
815
|
stats.skipped++;
|
|
719
816
|
stats.completed++;
|
|
720
817
|
}
|
|
@@ -736,7 +833,7 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
736
833
|
stats.passed++;
|
|
737
834
|
stats.completed++;
|
|
738
835
|
}
|
|
739
|
-
else if (state === 'FAILED' ||
|
|
836
|
+
else if (state === 'FAILED' || job.node.passed === false) {
|
|
740
837
|
stats.failed++;
|
|
741
838
|
stats.completed++;
|
|
742
839
|
}
|
|
@@ -748,14 +845,22 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
748
845
|
return [];
|
|
749
846
|
return jobs.filter(job => {
|
|
750
847
|
const state = job.node.state?.toUpperCase();
|
|
848
|
+
// BROKEN jobs are skipped/not run, not failed
|
|
849
|
+
if (state === 'BROKEN' || state === 'SKIPPED') {
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
751
852
|
// If we have an exit status, use that as the source of truth
|
|
752
853
|
// Note: exitStatus comes as a string from Buildkite API
|
|
753
854
|
if (job.node.exitStatus !== null && job.node.exitStatus !== undefined) {
|
|
754
855
|
const exitCode = parseInt(job.node.exitStatus, 10);
|
|
755
856
|
return exitCode !== 0;
|
|
756
857
|
}
|
|
757
|
-
//
|
|
758
|
-
|
|
858
|
+
// For FINISHED jobs, check the passed field
|
|
859
|
+
if (state === 'FINISHED') {
|
|
860
|
+
return job.node.passed === false;
|
|
861
|
+
}
|
|
862
|
+
// Otherwise check if explicitly failed
|
|
863
|
+
return state === 'FAILED';
|
|
759
864
|
});
|
|
760
865
|
}
|
|
761
866
|
getRunningJobs(jobs) {
|
|
@@ -773,8 +878,8 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
773
878
|
'Failed': [],
|
|
774
879
|
'Passed': [],
|
|
775
880
|
'Running': [],
|
|
776
|
-
'Blocked': []
|
|
777
|
-
'Skipped'
|
|
881
|
+
'Blocked': []
|
|
882
|
+
// Don't include Skipped - we don't display them
|
|
778
883
|
};
|
|
779
884
|
if (!jobs)
|
|
780
885
|
return grouped;
|
|
@@ -797,8 +902,9 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
797
902
|
else if (state === 'BLOCKED') {
|
|
798
903
|
grouped['Blocked'].push(job);
|
|
799
904
|
}
|
|
800
|
-
else if (state === 'SKIPPED' || state === 'CANCELED') {
|
|
801
|
-
|
|
905
|
+
else if (state === 'SKIPPED' || state === 'CANCELED' || state === 'BROKEN') {
|
|
906
|
+
// Don't display skipped/broken/canceled jobs - they're not shown in Buildkite UI
|
|
907
|
+
// Skip these entirely
|
|
802
908
|
}
|
|
803
909
|
else if (state === 'FINISHED' || state === 'COMPLETED') {
|
|
804
910
|
// For finished jobs without exit status, check passed field
|
|
@@ -812,12 +918,88 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
812
918
|
else if (state === 'PASSED' || job.node.passed === true) {
|
|
813
919
|
grouped['Passed'].push(job);
|
|
814
920
|
}
|
|
815
|
-
else if (state === 'FAILED'
|
|
921
|
+
else if (state === 'FAILED') {
|
|
816
922
|
grouped['Failed'].push(job);
|
|
817
923
|
}
|
|
818
924
|
}
|
|
819
925
|
return grouped;
|
|
820
926
|
}
|
|
927
|
+
collapseParallelJobs(jobs) {
|
|
928
|
+
const groups = new Map();
|
|
929
|
+
// Group jobs by label
|
|
930
|
+
for (const job of jobs) {
|
|
931
|
+
const label = job.node.label || 'Unnamed';
|
|
932
|
+
if (!groups.has(label)) {
|
|
933
|
+
groups.set(label, []);
|
|
934
|
+
}
|
|
935
|
+
groups.get(label).push(job);
|
|
936
|
+
}
|
|
937
|
+
// Convert to array and determine if each group is a parallel group
|
|
938
|
+
const result = [];
|
|
939
|
+
for (const [label, groupJobs] of groups.entries()) {
|
|
940
|
+
// Check if this is a parallel group (multiple jobs with same label and parallel info)
|
|
941
|
+
const hasParallelInfo = groupJobs.some(j => j.node.parallelGroupIndex !== undefined && j.node.parallelGroupTotal !== undefined);
|
|
942
|
+
const isParallelGroup = hasParallelInfo && groupJobs.length > 1;
|
|
943
|
+
// Get the total from the first job if available
|
|
944
|
+
const parallelTotal = groupJobs[0]?.node?.parallelGroupTotal;
|
|
945
|
+
result.push({
|
|
946
|
+
label,
|
|
947
|
+
jobs: groupJobs,
|
|
948
|
+
isParallelGroup,
|
|
949
|
+
parallelTotal
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
return result;
|
|
953
|
+
}
|
|
954
|
+
isJobPassed(job) {
|
|
955
|
+
const state = job.state?.toUpperCase();
|
|
956
|
+
if (job.exitStatus !== null && job.exitStatus !== undefined) {
|
|
957
|
+
return parseInt(job.exitStatus, 10) === 0;
|
|
958
|
+
}
|
|
959
|
+
if (state === 'PASSED')
|
|
960
|
+
return true;
|
|
961
|
+
if (state === 'FINISHED' || state === 'COMPLETED') {
|
|
962
|
+
return job.passed === true;
|
|
963
|
+
}
|
|
964
|
+
return job.passed === true;
|
|
965
|
+
}
|
|
966
|
+
isJobFailed(job) {
|
|
967
|
+
const state = job.state?.toUpperCase();
|
|
968
|
+
if (job.exitStatus !== null && job.exitStatus !== undefined) {
|
|
969
|
+
return parseInt(job.exitStatus, 10) !== 0;
|
|
970
|
+
}
|
|
971
|
+
if (state === 'FAILED')
|
|
972
|
+
return true;
|
|
973
|
+
if (state === 'FINISHED' || state === 'COMPLETED') {
|
|
974
|
+
return job.passed === false;
|
|
975
|
+
}
|
|
976
|
+
return false;
|
|
977
|
+
}
|
|
978
|
+
calculateAverageDuration(jobs) {
|
|
979
|
+
const durationsMs = jobs
|
|
980
|
+
.filter(j => j.node.startedAt && j.node.finishedAt)
|
|
981
|
+
.map(j => {
|
|
982
|
+
const start = new Date(j.node.startedAt).getTime();
|
|
983
|
+
const end = new Date(j.node.finishedAt).getTime();
|
|
984
|
+
return end - start;
|
|
985
|
+
});
|
|
986
|
+
if (durationsMs.length === 0) {
|
|
987
|
+
return 'unknown';
|
|
988
|
+
}
|
|
989
|
+
const avgMs = durationsMs.reduce((a, b) => a + b, 0) / durationsMs.length;
|
|
990
|
+
const avgSeconds = Math.floor(avgMs / 1000);
|
|
991
|
+
if (avgSeconds < 60) {
|
|
992
|
+
return `${avgSeconds}s`;
|
|
993
|
+
}
|
|
994
|
+
const minutes = Math.floor(avgSeconds / 60);
|
|
995
|
+
const seconds = avgSeconds % 60;
|
|
996
|
+
if (minutes < 60) {
|
|
997
|
+
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
998
|
+
}
|
|
999
|
+
const hours = Math.floor(minutes / 60);
|
|
1000
|
+
const remainingMinutes = minutes % 60;
|
|
1001
|
+
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
|
1002
|
+
}
|
|
821
1003
|
countAnnotationsByStyle(annotations) {
|
|
822
1004
|
const counts = {
|
|
823
1005
|
ERROR: 0,
|