bktide 1.0.1755568192 → 1.0.1755655078
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/annotations/PlainTextFormatter.js +79 -20
- package/dist/formatters/annotations/PlainTextFormatter.js.map +1 -1
- package/dist/formatters/build-detail/PlainTextFormatter.js +373 -133
- 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
|
@@ -2,6 +2,8 @@ import { BaseBuildDetailFormatter } from './Formatter.js';
|
|
|
2
2
|
import { formatDistanceToNow } from 'date-fns';
|
|
3
3
|
import { htmlToText } from 'html-to-text';
|
|
4
4
|
import { formatEmptyState, formatError, SEMANTIC_COLORS, formatBuildStatus, formatTips, TipStyle, getStateIcon, getAnnotationIcon, getProgressIcon, BUILD_STATUS_THEME } from '../../ui/theme.js';
|
|
5
|
+
import { useAscii } from '../../ui/symbols.js';
|
|
6
|
+
import { termWidth } from '../../ui/width.js';
|
|
5
7
|
// Standard emoji mappings only
|
|
6
8
|
// Only map universally recognized emoji codes, not Buildkite-specific ones
|
|
7
9
|
const STANDARD_EMOJI = {
|
|
@@ -140,10 +142,17 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
140
142
|
// Header line
|
|
141
143
|
lines.push(this.formatHeader(build));
|
|
142
144
|
lines.push(this.formatCommitInfo(build));
|
|
145
|
+
lines.push(''); // Blank line after commit info
|
|
143
146
|
// Show annotations summary if present
|
|
144
147
|
if (build.annotations?.edges?.length > 0) {
|
|
145
|
-
lines.push('');
|
|
146
148
|
lines.push(this.formatAnnotationSummary(build.annotations.edges));
|
|
149
|
+
}
|
|
150
|
+
// Jobs summary
|
|
151
|
+
if (build.jobs?.edges?.length > 0) {
|
|
152
|
+
if (build.annotations?.edges?.length > 0) {
|
|
153
|
+
lines.push(''); // Add space between annotations and steps
|
|
154
|
+
}
|
|
155
|
+
lines.push(this.formatJobSummary(build.jobs, build.state));
|
|
147
156
|
if (!options?.annotations) {
|
|
148
157
|
lines.push('');
|
|
149
158
|
const tips = formatTips(['Use --annotations to view annotation details'], TipStyle.GROUPED);
|
|
@@ -163,18 +172,18 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
163
172
|
lines.push(this.formatHeader(build));
|
|
164
173
|
lines.push(this.formatCommitInfo(build));
|
|
165
174
|
lines.push('');
|
|
166
|
-
// Failed jobs summary
|
|
167
|
-
const failedJobs = this.getFailedJobs(build.jobs?.edges);
|
|
168
175
|
const allHints = [];
|
|
169
|
-
|
|
170
|
-
const { summary, hints } = this.formatFailedJobsSummaryWithHints(failedJobs, options);
|
|
171
|
-
lines.push(summary);
|
|
172
|
-
allHints.push(...hints);
|
|
173
|
-
}
|
|
174
|
-
// Annotation summary
|
|
176
|
+
// Annotation summary (first, as it appears first in UI)
|
|
175
177
|
if (build.annotations?.edges?.length > 0) {
|
|
176
178
|
lines.push(this.formatAnnotationSummary(build.annotations.edges));
|
|
177
179
|
}
|
|
180
|
+
// Jobs summary
|
|
181
|
+
if (build.jobs?.edges?.length > 0) {
|
|
182
|
+
if (build.annotations?.edges?.length > 0) {
|
|
183
|
+
lines.push(''); // Add space between annotations and steps
|
|
184
|
+
}
|
|
185
|
+
lines.push(this.formatJobSummary(build.jobs, build.state));
|
|
186
|
+
}
|
|
178
187
|
// Show detailed job info if requested
|
|
179
188
|
if (options?.jobs || options?.failed) {
|
|
180
189
|
lines.push('');
|
|
@@ -186,12 +195,17 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
186
195
|
lines.push(this.formatAnnotationDetails(build.annotations.edges));
|
|
187
196
|
}
|
|
188
197
|
// Collect all hints for more info
|
|
198
|
+
const failedJobs = this.getFailedJobs(build.jobs?.edges);
|
|
189
199
|
if (!options?.failed && failedJobs.length > 0) {
|
|
190
200
|
allHints.push('Use --failed to show failure details');
|
|
191
201
|
}
|
|
192
202
|
if (!options?.annotations && build.annotations?.edges?.length > 0) {
|
|
193
203
|
allHints.push('Use --annotations to view annotation details');
|
|
194
204
|
}
|
|
205
|
+
// Add hint about incomplete step data if truncated
|
|
206
|
+
if (!options?.jobs && build.jobs?.pageInfo?.hasNextPage) {
|
|
207
|
+
allHints.push('Use --jobs to fetch all step data (currently showing first 100 only)');
|
|
208
|
+
}
|
|
195
209
|
// Display all hints together
|
|
196
210
|
if (allHints.length > 0) {
|
|
197
211
|
lines.push('');
|
|
@@ -205,9 +219,17 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
205
219
|
lines.push(this.formatHeader(build));
|
|
206
220
|
lines.push(this.formatCommitInfo(build));
|
|
207
221
|
lines.push('');
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
222
|
+
// Annotations first (if any)
|
|
223
|
+
if (build.annotations?.edges?.length > 0) {
|
|
224
|
+
lines.push(this.formatAnnotationSummary(build.annotations.edges));
|
|
225
|
+
}
|
|
226
|
+
// Jobs summary with progress
|
|
227
|
+
if (build.jobs?.edges?.length > 0) {
|
|
228
|
+
if (build.annotations?.edges?.length > 0) {
|
|
229
|
+
lines.push(''); // Add space between annotations and steps
|
|
230
|
+
}
|
|
231
|
+
lines.push(this.formatJobSummary(build.jobs, build.state));
|
|
232
|
+
}
|
|
211
233
|
// Show running jobs
|
|
212
234
|
const runningJobs = this.getRunningJobs(build.jobs?.edges);
|
|
213
235
|
if (runningJobs.length > 0) {
|
|
@@ -237,20 +259,22 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
237
259
|
lines.push(this.formatHeader(build));
|
|
238
260
|
lines.push(this.formatCommitInfo(build));
|
|
239
261
|
lines.push('');
|
|
262
|
+
// Annotations first (if any)
|
|
263
|
+
if (build.annotations?.edges?.length > 0) {
|
|
264
|
+
lines.push(this.formatAnnotationSummary(build.annotations.edges));
|
|
265
|
+
}
|
|
266
|
+
// Jobs summary
|
|
267
|
+
if (build.jobs?.edges?.length > 0) {
|
|
268
|
+
if (build.annotations?.edges?.length > 0) {
|
|
269
|
+
lines.push(''); // Add space between annotations and steps
|
|
270
|
+
}
|
|
271
|
+
lines.push(this.formatJobSummary(build.jobs, build.state));
|
|
272
|
+
}
|
|
240
273
|
// Blocked information
|
|
241
274
|
const blockedJobs = this.getBlockedJobs(build.jobs?.edges);
|
|
242
275
|
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
276
|
lines.push('');
|
|
253
|
-
lines.push(
|
|
277
|
+
lines.push(`${getProgressIcon('BLOCKED_MESSAGE')} Blocked: "${blockedJobs[0].node.label}" (manual unblock required)`);
|
|
254
278
|
}
|
|
255
279
|
// Show job details if requested
|
|
256
280
|
if (options?.jobs) {
|
|
@@ -270,14 +294,23 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
270
294
|
lines.push(this.formatHeader(build));
|
|
271
295
|
lines.push(this.formatCommitInfo(build));
|
|
272
296
|
lines.push('');
|
|
297
|
+
// Annotations first (if any)
|
|
298
|
+
if (build.annotations?.edges?.length > 0) {
|
|
299
|
+
lines.push(this.formatAnnotationSummary(build.annotations.edges));
|
|
300
|
+
}
|
|
301
|
+
// Jobs summary
|
|
302
|
+
if (build.jobs?.edges?.length > 0) {
|
|
303
|
+
if (build.annotations?.edges?.length > 0) {
|
|
304
|
+
lines.push(''); // Add space between annotations and steps
|
|
305
|
+
}
|
|
306
|
+
lines.push(this.formatJobSummary(build.jobs, build.state));
|
|
307
|
+
}
|
|
273
308
|
// Canceled information
|
|
274
309
|
if (build.createdBy) {
|
|
310
|
+
lines.push('');
|
|
275
311
|
const creator = build.createdBy.name || build.createdBy.email;
|
|
276
312
|
lines.push(`Canceled by: ${creator}`);
|
|
277
313
|
}
|
|
278
|
-
// Show jobs summary
|
|
279
|
-
const jobStats = this.getJobStats(build.jobs?.edges);
|
|
280
|
-
lines.push(`Completed: ${jobStats.completed}/${jobStats.total} jobs before cancellation`);
|
|
281
314
|
// Show job details if requested
|
|
282
315
|
if (options?.jobs) {
|
|
283
316
|
lines.push('');
|
|
@@ -323,8 +356,8 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
323
356
|
lines.push(` Triggered from: ${build.triggeredFrom.pipeline?.name} #${build.triggeredFrom.number}`);
|
|
324
357
|
}
|
|
325
358
|
lines.push('');
|
|
326
|
-
//
|
|
327
|
-
lines.push('
|
|
359
|
+
// Steps section
|
|
360
|
+
lines.push('Steps:');
|
|
328
361
|
lines.push(this.formatJobDetails(build.jobs?.edges, { ...options, full: true }));
|
|
329
362
|
// Annotations section
|
|
330
363
|
if (build.annotations?.edges?.length > 0) {
|
|
@@ -340,15 +373,31 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
340
373
|
const coloredIcon = this.colorizeStatusIcon(statusIcon, build.state);
|
|
341
374
|
const stateFormatted = formatBuildStatus(build.state, { useSymbol: false });
|
|
342
375
|
const duration = this.formatDuration(build);
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
376
|
+
// Get first line of commit message
|
|
377
|
+
const message = build.message || 'No commit message';
|
|
378
|
+
const firstLineMessage = message.split('\n')[0];
|
|
379
|
+
const truncatedMessage = firstLineMessage.length > 80 ? firstLineMessage.substring(0, 77) + '...' : firstLineMessage;
|
|
380
|
+
return `${coloredIcon} ${stateFormatted} ${truncatedMessage} ${SEMANTIC_COLORS.dim(`#${build.number}`)} ${SEMANTIC_COLORS.dim(duration)}`;
|
|
346
381
|
}
|
|
347
382
|
formatCommitInfo(build) {
|
|
348
383
|
const shortSha = build.commit ? build.commit.substring(0, 7) : 'unknown';
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
|
|
384
|
+
const branch = SEMANTIC_COLORS.identifier(build.branch);
|
|
385
|
+
const age = this.formatAge(build.createdAt);
|
|
386
|
+
// Get author information
|
|
387
|
+
const author = build.createdBy?.name || build.createdBy?.email || 'Unknown';
|
|
388
|
+
// Calculate indentation to align with commit message
|
|
389
|
+
// Map each state to its proper indentation (icon + space + state text + space)
|
|
390
|
+
const indentMap = {
|
|
391
|
+
'PASSED': 9, // ✓ PASSED
|
|
392
|
+
'FAILED': 9, // ✗ FAILED
|
|
393
|
+
'RUNNING': 10, // ⟳ RUNNING
|
|
394
|
+
'BLOCKED': 10, // ◼ BLOCKED
|
|
395
|
+
'CANCELED': 11, // ⊘ CANCELED
|
|
396
|
+
'SCHEDULED': 12, // ⏱ SCHEDULED
|
|
397
|
+
'SKIPPED': 10, // ⊙ SKIPPED
|
|
398
|
+
};
|
|
399
|
+
const indent = ' '.repeat(indentMap[build.state] || 9);
|
|
400
|
+
return `${indent}${author} • ${branch} • ${shortSha} • ${SEMANTIC_COLORS.dim(`Created ${age}`)}`;
|
|
352
401
|
}
|
|
353
402
|
formatAnnotationSummary(annotations) {
|
|
354
403
|
if (!annotations || annotations.length === 0) {
|
|
@@ -383,33 +432,156 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
383
432
|
}
|
|
384
433
|
return lines.join('\n');
|
|
385
434
|
}
|
|
435
|
+
formatJobSummary(jobsData, buildState) {
|
|
436
|
+
const jobs = jobsData?.edges;
|
|
437
|
+
if (!jobs || jobs.length === 0) {
|
|
438
|
+
return '';
|
|
439
|
+
}
|
|
440
|
+
const lines = [];
|
|
441
|
+
const jobStats = this.getJobStats(jobs);
|
|
442
|
+
// Build summary parts based on job states
|
|
443
|
+
const countParts = [];
|
|
444
|
+
if (jobStats.failed > 0)
|
|
445
|
+
countParts.push(SEMANTIC_COLORS.error(`${jobStats.failed} failed`));
|
|
446
|
+
if (jobStats.passed > 0)
|
|
447
|
+
countParts.push(SEMANTIC_COLORS.success(`${jobStats.passed} passed`));
|
|
448
|
+
if (jobStats.running > 0)
|
|
449
|
+
countParts.push(SEMANTIC_COLORS.info(`${jobStats.running} running`));
|
|
450
|
+
if (jobStats.blocked > 0)
|
|
451
|
+
countParts.push(SEMANTIC_COLORS.warning(`${jobStats.blocked} blocked`));
|
|
452
|
+
// Don't show skipped jobs
|
|
453
|
+
if (jobStats.canceled > 0)
|
|
454
|
+
countParts.push(SEMANTIC_COLORS.muted(`${jobStats.canceled} canceled`));
|
|
455
|
+
// Use appropriate icon based on build state
|
|
456
|
+
const icon = buildState === 'FAILED' ? getStateIcon('FAILED') :
|
|
457
|
+
buildState === 'RUNNING' ? getStateIcon('RUNNING') :
|
|
458
|
+
buildState === 'PASSED' ? getStateIcon('PASSED') :
|
|
459
|
+
buildState === 'BLOCKED' ? getStateIcon('BLOCKED') : '•';
|
|
460
|
+
// Check if we have partial data
|
|
461
|
+
const hasMorePages = jobsData?.pageInfo?.hasNextPage;
|
|
462
|
+
const totalCount = jobsData?.count;
|
|
463
|
+
if (hasMorePages) {
|
|
464
|
+
const showing = jobs.length;
|
|
465
|
+
const total = totalCount || `${showing}+`;
|
|
466
|
+
lines.push(`${icon} Showing ${SEMANTIC_COLORS.count(String(showing))} of ${SEMANTIC_COLORS.count(String(total))} steps: ${countParts.join(', ')}`);
|
|
467
|
+
lines.push(SEMANTIC_COLORS.warning('⚠️ Showing first 100 steps only (more available)'));
|
|
468
|
+
lines.push(SEMANTIC_COLORS.dim(' → Use --jobs to fetch all step data and see accurate statistics'));
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
lines.push(`${icon} ${SEMANTIC_COLORS.count(String(jobStats.total))} step${jobStats.total > 1 ? 's' : ''}: ${countParts.join(', ')}`);
|
|
472
|
+
}
|
|
473
|
+
// For failed builds, show the specific failed job names
|
|
474
|
+
if (buildState === 'FAILED') {
|
|
475
|
+
const failedJobs = this.getFailedJobs(jobs);
|
|
476
|
+
const jobGroups = this.groupJobsByLabel(failedJobs);
|
|
477
|
+
// Show up to 3 failed job types
|
|
478
|
+
const displayGroups = jobGroups.slice(0, 3);
|
|
479
|
+
for (const group of displayGroups) {
|
|
480
|
+
const label = this.parseEmoji(group.label);
|
|
481
|
+
const icon = getStateIcon('FAILED');
|
|
482
|
+
// Get duration for display
|
|
483
|
+
const duration = group.count === 1 && group.jobs[0]?.node
|
|
484
|
+
? ` ${SEMANTIC_COLORS.dim(`- ran ${this.formatJobDuration(group.jobs[0].node)}`)}`
|
|
485
|
+
: '';
|
|
486
|
+
if (group.parallelTotal > 0) {
|
|
487
|
+
lines.push(` ${icon} ${label} ${SEMANTIC_COLORS.dim(`(${group.stateCounts.failed || 0}/${group.parallelTotal} failed)`)}`);
|
|
488
|
+
}
|
|
489
|
+
else if (group.count > 1) {
|
|
490
|
+
lines.push(` ${icon} ${label} ${SEMANTIC_COLORS.dim(`(${group.stateCounts.failed || 0} failed)`)}`);
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
lines.push(` ${icon} ${label}${duration}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (jobGroups.length > 3) {
|
|
497
|
+
lines.push(` ${SEMANTIC_COLORS.muted(`...and ${jobGroups.length - 3} more`)}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return lines.join('\n');
|
|
501
|
+
}
|
|
386
502
|
formatAnnotationDetails(annotations) {
|
|
387
503
|
const lines = [];
|
|
504
|
+
const isAscii = useAscii();
|
|
505
|
+
const terminalWidth = termWidth();
|
|
506
|
+
// Box drawing characters
|
|
507
|
+
const boxChars = isAscii ? {
|
|
508
|
+
horizontal: '-',
|
|
509
|
+
vertical: '|'
|
|
510
|
+
} : {
|
|
511
|
+
horizontal: '─',
|
|
512
|
+
vertical: '│'
|
|
513
|
+
};
|
|
514
|
+
// Create a horizontal divider with padding and centering
|
|
515
|
+
const createDivider = (width = 80) => {
|
|
516
|
+
const padding = 2; // 1 space on each side
|
|
517
|
+
const maxWidth = Math.min(width, terminalWidth - padding);
|
|
518
|
+
const dividerLength = Math.max(20, maxWidth - padding); // Minimum 20 chars
|
|
519
|
+
const divider = boxChars.horizontal.repeat(dividerLength);
|
|
520
|
+
// Center the divider within the terminal width
|
|
521
|
+
const totalPadding = terminalWidth - dividerLength;
|
|
522
|
+
const leftPadding = Math.floor(totalPadding / 2);
|
|
523
|
+
const spaces = ' '.repeat(Math.max(0, leftPadding));
|
|
524
|
+
return SEMANTIC_COLORS.dim(spaces + divider);
|
|
525
|
+
};
|
|
388
526
|
// Group annotations by style
|
|
389
527
|
const grouped = this.groupAnnotationsByStyle(annotations);
|
|
390
528
|
const styleOrder = ['ERROR', 'WARNING', 'INFO', 'SUCCESS'];
|
|
529
|
+
let annotationIndex = 0;
|
|
391
530
|
for (const style of styleOrder) {
|
|
392
531
|
if (grouped[style]) {
|
|
393
532
|
for (const annotation of grouped[style]) {
|
|
533
|
+
// Add divider between annotations (but not before the first one)
|
|
534
|
+
if (annotationIndex > 0) {
|
|
535
|
+
lines.push('');
|
|
536
|
+
lines.push(createDivider());
|
|
537
|
+
lines.push('');
|
|
538
|
+
}
|
|
394
539
|
const icon = this.getAnnotationIcon(style);
|
|
395
540
|
const context = annotation.node.context || 'default';
|
|
396
|
-
const
|
|
397
|
-
//
|
|
398
|
-
|
|
541
|
+
const colorFn = this.getStyleColorFunction(style);
|
|
542
|
+
// Single line header with pipe: "│ ℹ info: test-mapping-build"
|
|
543
|
+
const pipe = colorFn(boxChars.vertical);
|
|
544
|
+
const header = `${pipe} ${icon} ${style.toLowerCase()}: ${context}`;
|
|
545
|
+
lines.push(header);
|
|
546
|
+
// Add blank line with pipe for visual continuity
|
|
547
|
+
lines.push(pipe);
|
|
548
|
+
// Format the body HTML with proper HTML/markdown handling
|
|
399
549
|
const body = htmlToText(annotation.node.body?.html || '', {
|
|
400
550
|
wordwrap: 80,
|
|
401
551
|
preserveNewlines: true
|
|
402
552
|
});
|
|
403
|
-
|
|
404
|
-
|
|
553
|
+
// Add vertical pipes to the left of the body content for visual continuity
|
|
554
|
+
// Use the same color as the header for the pipes
|
|
555
|
+
const bodyLines = body.split('\n');
|
|
556
|
+
bodyLines.forEach((line) => {
|
|
557
|
+
const paddedLine = line ? ` ${line}` : '';
|
|
558
|
+
lines.push(`${pipe}${paddedLine}`);
|
|
559
|
+
});
|
|
560
|
+
annotationIndex++;
|
|
405
561
|
}
|
|
406
562
|
}
|
|
407
563
|
}
|
|
564
|
+
// Add summary footer for multiple annotations
|
|
565
|
+
if (annotations.length > 1) {
|
|
566
|
+
lines.push('');
|
|
567
|
+
lines.push(createDivider());
|
|
568
|
+
lines.push('');
|
|
569
|
+
lines.push(SEMANTIC_COLORS.dim(`${SEMANTIC_COLORS.count(annotations.length.toString())} annotations found`));
|
|
570
|
+
}
|
|
408
571
|
return lines.join('\n').trim();
|
|
409
572
|
}
|
|
573
|
+
getStyleColorFunction(style) {
|
|
574
|
+
const styleColorMap = {
|
|
575
|
+
'ERROR': SEMANTIC_COLORS.error,
|
|
576
|
+
'WARNING': SEMANTIC_COLORS.warning,
|
|
577
|
+
'INFO': SEMANTIC_COLORS.info,
|
|
578
|
+
'SUCCESS': SEMANTIC_COLORS.success
|
|
579
|
+
};
|
|
580
|
+
return styleColorMap[style] || ((s) => s);
|
|
581
|
+
}
|
|
410
582
|
formatJobDetails(jobs, options) {
|
|
411
583
|
if (!jobs || jobs.length === 0) {
|
|
412
|
-
return 'No
|
|
584
|
+
return 'No steps found';
|
|
413
585
|
}
|
|
414
586
|
const lines = [];
|
|
415
587
|
const jobStats = this.getJobStats(jobs);
|
|
@@ -423,45 +595,83 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
423
595
|
parts.push(`${getStateIcon('RUNNING')} ${jobStats.running} running`);
|
|
424
596
|
if (jobStats.blocked > 0)
|
|
425
597
|
parts.push(`${getStateIcon('BLOCKED')} ${jobStats.blocked} blocked`);
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
lines.push(`Jobs: ${parts.join(' ')}`);
|
|
598
|
+
// Don't show skipped jobs in summary
|
|
599
|
+
lines.push(`Steps: ${parts.join(' ')}`);
|
|
429
600
|
lines.push('');
|
|
430
601
|
// Filter jobs based on options
|
|
431
602
|
let filteredJobs = jobs;
|
|
432
603
|
if (options?.failed) {
|
|
433
604
|
filteredJobs = this.getFailedJobs(jobs);
|
|
434
605
|
}
|
|
435
|
-
// Group jobs by state
|
|
606
|
+
// Group jobs by state first
|
|
436
607
|
const grouped = this.groupJobsByState(filteredJobs);
|
|
437
608
|
for (const [state, stateJobs] of Object.entries(grouped)) {
|
|
438
609
|
if (stateJobs.length === 0)
|
|
439
610
|
continue;
|
|
440
611
|
const icon = this.getJobStateIcon(state);
|
|
441
612
|
const stateColored = this.colorizeJobState(state);
|
|
613
|
+
// Collapse parallel jobs with same label
|
|
614
|
+
const collapsedGroups = this.collapseParallelJobs(stateJobs);
|
|
442
615
|
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
|
-
|
|
616
|
+
for (const group of collapsedGroups) {
|
|
617
|
+
if (group.isParallelGroup && group.jobs.length > 1) {
|
|
618
|
+
// Collapsed parallel group display
|
|
619
|
+
const label = this.parseEmoji(group.label);
|
|
620
|
+
const total = group.parallelTotal || group.jobs.length;
|
|
621
|
+
const passedCount = group.jobs.filter(j => this.isJobPassed(j.node)).length;
|
|
622
|
+
const failedCount = group.jobs.filter(j => this.isJobFailed(j.node)).length;
|
|
623
|
+
// Show summary line for parallel group
|
|
624
|
+
if (failedCount > 0) {
|
|
625
|
+
// If there are failures, show breakdown
|
|
626
|
+
// Apply state color to label
|
|
627
|
+
const coloredLabel = state === 'Failed' ? SEMANTIC_COLORS.error(label) :
|
|
628
|
+
state === 'Passed' ? SEMANTIC_COLORS.success(label) :
|
|
629
|
+
state === 'Running' ? SEMANTIC_COLORS.info(label) :
|
|
630
|
+
state === 'Blocked' ? SEMANTIC_COLORS.warning(label) : label;
|
|
631
|
+
lines.push(` ${coloredLabel} ${SEMANTIC_COLORS.dim(`(${passedCount}/${total} passed, ${failedCount} failed)`)}`);
|
|
632
|
+
// Show failed steps individually
|
|
633
|
+
const failedJobs = group.jobs.filter(j => this.isJobFailed(j.node));
|
|
634
|
+
for (const job of failedJobs) {
|
|
635
|
+
const duration = this.formatJobDuration(job.node);
|
|
636
|
+
const parallelInfo = job.node.parallelGroupIndex !== undefined
|
|
637
|
+
? ` ${SEMANTIC_COLORS.dim(`[Parallel: ${job.node.parallelGroupIndex + 1}/${job.node.parallelGroupTotal}]`)}`
|
|
638
|
+
: '';
|
|
639
|
+
lines.push(` ${SEMANTIC_COLORS.error('↳ Failed')}: ${SEMANTIC_COLORS.dim(duration)}${parallelInfo}`);
|
|
640
|
+
}
|
|
457
641
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
642
|
+
else {
|
|
643
|
+
// All passed/running/blocked - just show summary
|
|
644
|
+
const avgDuration = this.calculateAverageDuration(group.jobs);
|
|
645
|
+
// Apply state color to label
|
|
646
|
+
const coloredLabel = state === 'Passed' ? SEMANTIC_COLORS.success(label) :
|
|
647
|
+
state === 'Failed' ? SEMANTIC_COLORS.error(label) :
|
|
648
|
+
state === 'Running' ? SEMANTIC_COLORS.info(label) :
|
|
649
|
+
state === 'Blocked' ? SEMANTIC_COLORS.warning(label) : label;
|
|
650
|
+
lines.push(` ${coloredLabel} ${SEMANTIC_COLORS.dim(`(${total} parallel steps, avg: ${avgDuration})`)}`);
|
|
461
651
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
// Single job or non-parallel group - display as before
|
|
655
|
+
const job = group.jobs[0];
|
|
656
|
+
const label = this.parseEmoji(job.node.label);
|
|
657
|
+
const duration = this.formatJobDuration(job.node);
|
|
658
|
+
// Apply state color to label
|
|
659
|
+
const coloredLabel = state === 'Passed' ? SEMANTIC_COLORS.success(label) :
|
|
660
|
+
state === 'Failed' ? SEMANTIC_COLORS.error(label) :
|
|
661
|
+
state === 'Running' ? SEMANTIC_COLORS.info(label) :
|
|
662
|
+
state === 'Blocked' ? SEMANTIC_COLORS.warning(label) : label;
|
|
663
|
+
// Add parallel info inline if present
|
|
664
|
+
const parallelInfo = (job.node.parallelGroupIndex !== undefined && job.node.parallelGroupTotal)
|
|
665
|
+
? ` ${SEMANTIC_COLORS.dim(`[Parallel: ${job.node.parallelGroupIndex + 1}/${job.node.parallelGroupTotal}]`)}`
|
|
666
|
+
: '';
|
|
667
|
+
// Basic step line with optional parallel info
|
|
668
|
+
lines.push(` ${coloredLabel} ${SEMANTIC_COLORS.dim(`(${duration})`)}${parallelInfo}`);
|
|
669
|
+
// Show additional details if --jobs or --full and single step
|
|
670
|
+
if ((options?.jobs || options?.full) && !group.isParallelGroup) {
|
|
671
|
+
// Retry info
|
|
672
|
+
if (job.node.retried) {
|
|
673
|
+
lines.push(` ${SEMANTIC_COLORS.warning(`${getProgressIcon('RETRY')} Retried`)}`);
|
|
674
|
+
}
|
|
465
675
|
}
|
|
466
676
|
}
|
|
467
677
|
}
|
|
@@ -469,67 +679,6 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
469
679
|
}
|
|
470
680
|
return lines.join('\n').trim();
|
|
471
681
|
}
|
|
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
682
|
groupJobsByLabel(jobs) {
|
|
534
683
|
const groups = new Map();
|
|
535
684
|
for (const job of jobs) {
|
|
@@ -688,6 +837,7 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
688
837
|
running: 0,
|
|
689
838
|
blocked: 0,
|
|
690
839
|
skipped: 0,
|
|
840
|
+
canceled: 0,
|
|
691
841
|
queued: 0,
|
|
692
842
|
completed: 0
|
|
693
843
|
};
|
|
@@ -714,7 +864,12 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
714
864
|
else if (state === 'BLOCKED') {
|
|
715
865
|
stats.blocked++;
|
|
716
866
|
}
|
|
717
|
-
else if (state === '
|
|
867
|
+
else if (state === 'CANCELED' || state === 'CANCELLED') {
|
|
868
|
+
stats.canceled++;
|
|
869
|
+
stats.completed++;
|
|
870
|
+
}
|
|
871
|
+
else if (state === 'SKIPPED' || state === 'BROKEN') {
|
|
872
|
+
// BROKEN jobs are functionally skipped - they don't run due to conditions not matching
|
|
718
873
|
stats.skipped++;
|
|
719
874
|
stats.completed++;
|
|
720
875
|
}
|
|
@@ -736,7 +891,7 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
736
891
|
stats.passed++;
|
|
737
892
|
stats.completed++;
|
|
738
893
|
}
|
|
739
|
-
else if (state === 'FAILED' ||
|
|
894
|
+
else if (state === 'FAILED' || job.node.passed === false) {
|
|
740
895
|
stats.failed++;
|
|
741
896
|
stats.completed++;
|
|
742
897
|
}
|
|
@@ -748,14 +903,22 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
748
903
|
return [];
|
|
749
904
|
return jobs.filter(job => {
|
|
750
905
|
const state = job.node.state?.toUpperCase();
|
|
906
|
+
// BROKEN jobs are skipped/not run, not failed
|
|
907
|
+
if (state === 'BROKEN' || state === 'SKIPPED') {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
751
910
|
// If we have an exit status, use that as the source of truth
|
|
752
911
|
// Note: exitStatus comes as a string from Buildkite API
|
|
753
912
|
if (job.node.exitStatus !== null && job.node.exitStatus !== undefined) {
|
|
754
913
|
const exitCode = parseInt(job.node.exitStatus, 10);
|
|
755
914
|
return exitCode !== 0;
|
|
756
915
|
}
|
|
757
|
-
//
|
|
758
|
-
|
|
916
|
+
// For FINISHED jobs, check the passed field
|
|
917
|
+
if (state === 'FINISHED') {
|
|
918
|
+
return job.node.passed === false;
|
|
919
|
+
}
|
|
920
|
+
// Otherwise check if explicitly failed
|
|
921
|
+
return state === 'FAILED';
|
|
759
922
|
});
|
|
760
923
|
}
|
|
761
924
|
getRunningJobs(jobs) {
|
|
@@ -773,8 +936,8 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
773
936
|
'Failed': [],
|
|
774
937
|
'Passed': [],
|
|
775
938
|
'Running': [],
|
|
776
|
-
'Blocked': []
|
|
777
|
-
'Skipped'
|
|
939
|
+
'Blocked': []
|
|
940
|
+
// Don't include Skipped - we don't display them
|
|
778
941
|
};
|
|
779
942
|
if (!jobs)
|
|
780
943
|
return grouped;
|
|
@@ -797,8 +960,9 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
797
960
|
else if (state === 'BLOCKED') {
|
|
798
961
|
grouped['Blocked'].push(job);
|
|
799
962
|
}
|
|
800
|
-
else if (state === 'SKIPPED' || state === 'CANCELED') {
|
|
801
|
-
|
|
963
|
+
else if (state === 'SKIPPED' || state === 'CANCELED' || state === 'BROKEN') {
|
|
964
|
+
// Don't display skipped/broken/canceled jobs - they're not shown in Buildkite UI
|
|
965
|
+
// Skip these entirely
|
|
802
966
|
}
|
|
803
967
|
else if (state === 'FINISHED' || state === 'COMPLETED') {
|
|
804
968
|
// For finished jobs without exit status, check passed field
|
|
@@ -812,12 +976,88 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
812
976
|
else if (state === 'PASSED' || job.node.passed === true) {
|
|
813
977
|
grouped['Passed'].push(job);
|
|
814
978
|
}
|
|
815
|
-
else if (state === 'FAILED'
|
|
979
|
+
else if (state === 'FAILED') {
|
|
816
980
|
grouped['Failed'].push(job);
|
|
817
981
|
}
|
|
818
982
|
}
|
|
819
983
|
return grouped;
|
|
820
984
|
}
|
|
985
|
+
collapseParallelJobs(jobs) {
|
|
986
|
+
const groups = new Map();
|
|
987
|
+
// Group jobs by label
|
|
988
|
+
for (const job of jobs) {
|
|
989
|
+
const label = job.node.label || 'Unnamed';
|
|
990
|
+
if (!groups.has(label)) {
|
|
991
|
+
groups.set(label, []);
|
|
992
|
+
}
|
|
993
|
+
groups.get(label).push(job);
|
|
994
|
+
}
|
|
995
|
+
// Convert to array and determine if each group is a parallel group
|
|
996
|
+
const result = [];
|
|
997
|
+
for (const [label, groupJobs] of groups.entries()) {
|
|
998
|
+
// Check if this is a parallel group (multiple jobs with same label and parallel info)
|
|
999
|
+
const hasParallelInfo = groupJobs.some(j => j.node.parallelGroupIndex !== undefined && j.node.parallelGroupTotal !== undefined);
|
|
1000
|
+
const isParallelGroup = hasParallelInfo && groupJobs.length > 1;
|
|
1001
|
+
// Get the total from the first job if available
|
|
1002
|
+
const parallelTotal = groupJobs[0]?.node?.parallelGroupTotal;
|
|
1003
|
+
result.push({
|
|
1004
|
+
label,
|
|
1005
|
+
jobs: groupJobs,
|
|
1006
|
+
isParallelGroup,
|
|
1007
|
+
parallelTotal
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
return result;
|
|
1011
|
+
}
|
|
1012
|
+
isJobPassed(job) {
|
|
1013
|
+
const state = job.state?.toUpperCase();
|
|
1014
|
+
if (job.exitStatus !== null && job.exitStatus !== undefined) {
|
|
1015
|
+
return parseInt(job.exitStatus, 10) === 0;
|
|
1016
|
+
}
|
|
1017
|
+
if (state === 'PASSED')
|
|
1018
|
+
return true;
|
|
1019
|
+
if (state === 'FINISHED' || state === 'COMPLETED') {
|
|
1020
|
+
return job.passed === true;
|
|
1021
|
+
}
|
|
1022
|
+
return job.passed === true;
|
|
1023
|
+
}
|
|
1024
|
+
isJobFailed(job) {
|
|
1025
|
+
const state = job.state?.toUpperCase();
|
|
1026
|
+
if (job.exitStatus !== null && job.exitStatus !== undefined) {
|
|
1027
|
+
return parseInt(job.exitStatus, 10) !== 0;
|
|
1028
|
+
}
|
|
1029
|
+
if (state === 'FAILED')
|
|
1030
|
+
return true;
|
|
1031
|
+
if (state === 'FINISHED' || state === 'COMPLETED') {
|
|
1032
|
+
return job.passed === false;
|
|
1033
|
+
}
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
calculateAverageDuration(jobs) {
|
|
1037
|
+
const durationsMs = jobs
|
|
1038
|
+
.filter(j => j.node.startedAt && j.node.finishedAt)
|
|
1039
|
+
.map(j => {
|
|
1040
|
+
const start = new Date(j.node.startedAt).getTime();
|
|
1041
|
+
const end = new Date(j.node.finishedAt).getTime();
|
|
1042
|
+
return end - start;
|
|
1043
|
+
});
|
|
1044
|
+
if (durationsMs.length === 0) {
|
|
1045
|
+
return 'unknown';
|
|
1046
|
+
}
|
|
1047
|
+
const avgMs = durationsMs.reduce((a, b) => a + b, 0) / durationsMs.length;
|
|
1048
|
+
const avgSeconds = Math.floor(avgMs / 1000);
|
|
1049
|
+
if (avgSeconds < 60) {
|
|
1050
|
+
return `${avgSeconds}s`;
|
|
1051
|
+
}
|
|
1052
|
+
const minutes = Math.floor(avgSeconds / 60);
|
|
1053
|
+
const seconds = avgSeconds % 60;
|
|
1054
|
+
if (minutes < 60) {
|
|
1055
|
+
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
1056
|
+
}
|
|
1057
|
+
const hours = Math.floor(minutes / 60);
|
|
1058
|
+
const remainingMinutes = minutes % 60;
|
|
1059
|
+
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
|
1060
|
+
}
|
|
821
1061
|
countAnnotationsByStyle(annotations) {
|
|
822
1062
|
const counts = {
|
|
823
1063
|
ERROR: 0,
|