bktide 1.0.1755559112 → 1.0.1755568192

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.
@@ -1,7 +1,7 @@
1
1
  import { BaseBuildDetailFormatter } from './Formatter.js';
2
2
  import { formatDistanceToNow } from 'date-fns';
3
3
  import { htmlToText } from 'html-to-text';
4
- import { formatEmptyState, formatError, SEMANTIC_COLORS, formatBuildStatus } from '../../ui/theme.js';
4
+ import { formatEmptyState, formatError, SEMANTIC_COLORS, formatBuildStatus, formatTips, TipStyle, getStateIcon, getAnnotationIcon, getProgressIcon, BUILD_STATUS_THEME } from '../../ui/theme.js';
5
5
  // Standard emoji mappings only
6
6
  // Only map universally recognized emoji codes, not Buildkite-specific ones
7
7
  const STANDARD_EMOJI = {
@@ -127,10 +127,13 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
127
127
  return formatError(options?.errorMessage || 'Unknown error');
128
128
  }
129
129
  formatSummaryLine(build) {
130
- const status = this.getStatusIcon(build.state);
130
+ const statusIcon = this.getStatusIcon(build.state);
131
+ const coloredIcon = this.colorizeStatusIcon(statusIcon, build.state);
131
132
  const duration = this.formatDuration(build);
132
133
  const age = this.formatAge(build.createdAt);
133
- return `${status} #${build.number} ${build.state.toLowerCase()} • ${duration} ${build.branch} • ${age}`;
134
+ const stateFormatted = formatBuildStatus(build.state, { useSymbol: false });
135
+ const branch = SEMANTIC_COLORS.identifier(build.branch);
136
+ return `${coloredIcon} ${SEMANTIC_COLORS.label(`#${build.number}`)} ${stateFormatted} • ${duration} • ${branch} • ${age}`;
134
137
  }
135
138
  formatPassedBuild(build, options) {
136
139
  const lines = [];
@@ -142,13 +145,15 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
142
145
  lines.push('');
143
146
  lines.push(this.formatAnnotationSummary(build.annotations.edges));
144
147
  if (!options?.annotations) {
145
- lines.push(SEMANTIC_COLORS.dim(`→ bin/bktide build ${build.number} --annotations # view annotations`));
148
+ lines.push('');
149
+ const tips = formatTips(['Use --annotations to view annotation details'], TipStyle.GROUPED);
150
+ lines.push(tips);
146
151
  }
147
152
  }
148
153
  // Show annotations detail if requested
149
154
  if (options?.annotations) {
150
155
  lines.push('');
151
- lines.push(this.formatAnnotationDetails(build.annotations.edges, options));
156
+ lines.push(this.formatAnnotationDetails(build.annotations.edges));
152
157
  }
153
158
  return lines.join('\n');
154
159
  }
@@ -160,8 +165,11 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
160
165
  lines.push('');
161
166
  // Failed jobs summary
162
167
  const failedJobs = this.getFailedJobs(build.jobs?.edges);
168
+ const allHints = [];
163
169
  if (failedJobs.length > 0) {
164
- lines.push(this.formatFailedJobsSummary(failedJobs));
170
+ const { summary, hints } = this.formatFailedJobsSummaryWithHints(failedJobs, options);
171
+ lines.push(summary);
172
+ allHints.push(...hints);
165
173
  }
166
174
  // Annotation summary
167
175
  if (build.annotations?.edges?.length > 0) {
@@ -175,15 +183,19 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
175
183
  // Show annotations detail if requested
176
184
  if (options?.annotations) {
177
185
  lines.push('');
178
- lines.push(this.formatAnnotationDetails(build.annotations.edges, options));
186
+ lines.push(this.formatAnnotationDetails(build.annotations.edges));
179
187
  }
180
- // Hints for more info (no Tips label)
188
+ // Collect all hints for more info
181
189
  if (!options?.failed && failedJobs.length > 0) {
182
- lines.push('');
183
- lines.push(SEMANTIC_COLORS.dim(`→ bin/bktide build ${build.number} --failed # show failure details`));
190
+ allHints.push('Use --failed to show failure details');
184
191
  }
185
192
  if (!options?.annotations && build.annotations?.edges?.length > 0) {
186
- lines.push(SEMANTIC_COLORS.dim(`→ bin/bktide build ${build.number} --annotations # view annotations`));
193
+ allHints.push('Use --annotations to view annotation details');
194
+ }
195
+ // Display all hints together
196
+ if (allHints.length > 0) {
197
+ lines.push('');
198
+ lines.push(formatTips(allHints, TipStyle.GROUPED));
187
199
  }
188
200
  return lines.join('\n');
189
201
  }
@@ -202,11 +214,21 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
202
214
  const labels = runningJobs.map(j => this.parseEmoji(j.node.label)).join(', ');
203
215
  lines.push(`${SEMANTIC_COLORS.info('Running')}: ${labels}`);
204
216
  }
217
+ // Annotation summary
218
+ if (build.annotations?.edges?.length > 0) {
219
+ lines.push('');
220
+ lines.push(this.formatAnnotationSummary(build.annotations.edges));
221
+ }
205
222
  // Show job details if requested
206
223
  if (options?.jobs) {
207
224
  lines.push('');
208
225
  lines.push(this.formatJobDetails(build.jobs?.edges, options));
209
226
  }
227
+ // Show annotations detail if requested
228
+ if (options?.annotations) {
229
+ lines.push('');
230
+ lines.push(this.formatAnnotationDetails(build.annotations.edges));
231
+ }
210
232
  return lines.join('\n');
211
233
  }
212
234
  formatBlockedBuild(build, options) {
@@ -218,18 +240,28 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
218
240
  // Blocked information
219
241
  const blockedJobs = this.getBlockedJobs(build.jobs?.edges);
220
242
  if (blockedJobs.length > 0) {
221
- lines.push(`🚫 Blocked: "${blockedJobs[0].node.label}" (manual unblock required)`);
243
+ lines.push(`${getProgressIcon('BLOCKED_MESSAGE')} Blocked: "${blockedJobs[0].node.label}" (manual unblock required)`);
222
244
  }
223
245
  // Show jobs summary
224
246
  const jobStats = this.getJobStats(build.jobs?.edges);
225
247
  if (jobStats.completed > 0) {
226
- lines.push(`✅ ${jobStats.completed} jobs passed before block`);
248
+ lines.push(`${getStateIcon('PASSED')} ${jobStats.completed} jobs passed before block`);
249
+ }
250
+ // Annotation summary
251
+ if (build.annotations?.edges?.length > 0) {
252
+ lines.push('');
253
+ lines.push(this.formatAnnotationSummary(build.annotations.edges));
227
254
  }
228
255
  // Show job details if requested
229
256
  if (options?.jobs) {
230
257
  lines.push('');
231
258
  lines.push(this.formatJobDetails(build.jobs?.edges, options));
232
259
  }
260
+ // Show annotations detail if requested
261
+ if (options?.annotations) {
262
+ lines.push('');
263
+ lines.push(this.formatAnnotationDetails(build.annotations.edges));
264
+ }
233
265
  return lines.join('\n');
234
266
  }
235
267
  formatCanceledBuild(build, options) {
@@ -268,7 +300,24 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
268
300
  lines.push(` Organization: ${build.organization?.name || 'Unknown'}`);
269
301
  lines.push(` Pipeline: ${build.pipeline?.name || 'Unknown'}`);
270
302
  if (build.pullRequest) {
271
- lines.push(` Pull Request: #${build.pullRequest.number}`);
303
+ // Try to construct PR URL from repository URL
304
+ const repoUrl = build.pipeline?.repository?.url;
305
+ if (repoUrl && repoUrl.includes('github.com')) {
306
+ // Extract owner/repo from various GitHub URL formats
307
+ const match = repoUrl.match(/github\.com[/:]([\w-]+)\/([\w-]+)/);
308
+ if (match && build.pullRequest.id) {
309
+ // Extract PR number from GraphQL ID if possible
310
+ // GitHub PR IDs often contain the number
311
+ const prUrl = `https://github.com/${match[1]}/${match[2]}/pull/${build.pullRequest.id}`;
312
+ lines.push(` Pull Request: ${SEMANTIC_COLORS.url(prUrl)}`);
313
+ }
314
+ else {
315
+ lines.push(` Pull Request: ${build.pullRequest.id}`);
316
+ }
317
+ }
318
+ else {
319
+ lines.push(` Pull Request: ${build.pullRequest.id}`);
320
+ }
272
321
  }
273
322
  if (build.triggeredFrom) {
274
323
  lines.push(` Triggered from: ${build.triggeredFrom.pipeline?.name} #${build.triggeredFrom.number}`);
@@ -281,17 +330,19 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
281
330
  if (build.annotations?.edges?.length > 0) {
282
331
  lines.push('');
283
332
  lines.push('Annotations:');
284
- lines.push(this.formatAnnotationDetails(build.annotations.edges, { ...options, annotationsFull: true }));
333
+ lines.push(this.formatAnnotationDetails(build.annotations.edges));
285
334
  }
286
335
  return lines.join('\n');
287
336
  }
288
337
  formatHeader(build) {
289
- const status = this.getStatusIcon(build.state);
338
+ const statusIcon = this.getStatusIcon(build.state);
339
+ // Apply appropriate color to the icon based on the state
340
+ const coloredIcon = this.colorizeStatusIcon(statusIcon, build.state);
290
341
  const stateFormatted = formatBuildStatus(build.state, { useSymbol: false });
291
342
  const duration = this.formatDuration(build);
292
343
  const age = this.formatAge(build.createdAt);
293
344
  const branch = SEMANTIC_COLORS.identifier(build.branch);
294
- return `${status} ${SEMANTIC_COLORS.label(`#${build.number}`)} ${stateFormatted} • ${duration} • ${branch} • ${age}`;
345
+ return `${coloredIcon} ${SEMANTIC_COLORS.label(`#${build.number}`)} ${stateFormatted} • ${duration} • ${branch} • ${age}`;
295
346
  }
296
347
  formatCommitInfo(build) {
297
348
  const shortSha = build.commit ? build.commit.substring(0, 7) : 'unknown';
@@ -300,30 +351,51 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
300
351
  return ` "${truncatedMessage}" (${shortSha})`;
301
352
  }
302
353
  formatAnnotationSummary(annotations) {
354
+ if (!annotations || annotations.length === 0) {
355
+ return '';
356
+ }
357
+ const lines = [];
358
+ const total = annotations.length;
359
+ // Header with count
303
360
  const counts = this.countAnnotationsByStyle(annotations);
304
- const parts = [];
361
+ const countParts = [];
305
362
  if (counts.ERROR > 0)
306
- parts.push(SEMANTIC_COLORS.error(`${counts.ERROR} error${counts.ERROR > 1 ? 's' : ''}`));
363
+ countParts.push(SEMANTIC_COLORS.error(`${counts.ERROR} error${counts.ERROR > 1 ? 's' : ''}`));
307
364
  if (counts.WARNING > 0)
308
- parts.push(SEMANTIC_COLORS.warning(`${counts.WARNING} warning${counts.WARNING > 1 ? 's' : ''}`));
365
+ countParts.push(SEMANTIC_COLORS.warning(`${counts.WARNING} warning${counts.WARNING > 1 ? 's' : ''}`));
309
366
  if (counts.INFO > 0)
310
- parts.push(SEMANTIC_COLORS.info(`${counts.INFO} info`));
367
+ countParts.push(SEMANTIC_COLORS.info(`${counts.INFO} info`));
311
368
  if (counts.SUCCESS > 0)
312
- parts.push(SEMANTIC_COLORS.success(`${counts.SUCCESS} success`));
313
- const total = annotations.length;
314
- return `📝 ${SEMANTIC_COLORS.count(String(total))} annotation${total > 1 ? 's' : ''}: ${parts.join(', ')}`;
369
+ countParts.push(SEMANTIC_COLORS.success(`${counts.SUCCESS} success`));
370
+ lines.push(`${getAnnotationIcon('DEFAULT')} ${SEMANTIC_COLORS.count(String(total))} annotation${total > 1 ? 's' : ''}: ${countParts.join(', ')}`);
371
+ // List each annotation with style and context
372
+ const grouped = this.groupAnnotationsByStyle(annotations);
373
+ const styleOrder = ['ERROR', 'WARNING', 'INFO', 'SUCCESS'];
374
+ for (const style of styleOrder) {
375
+ if (grouped[style]) {
376
+ for (const annotation of grouped[style]) {
377
+ const icon = this.getAnnotationIcon(style);
378
+ const context = annotation.node.context || 'default';
379
+ const styleColored = this.colorizeAnnotationStyle(style);
380
+ lines.push(` ${icon} ${styleColored}: ${context}`);
381
+ }
382
+ }
383
+ }
384
+ return lines.join('\n');
315
385
  }
316
- formatAnnotationDetails(annotations, options) {
386
+ formatAnnotationDetails(annotations) {
317
387
  const lines = [];
318
388
  // Group annotations by style
319
389
  const grouped = this.groupAnnotationsByStyle(annotations);
320
- for (const [style, items] of Object.entries(grouped)) {
321
- for (const annotation of items) {
322
- const icon = this.getAnnotationIcon(style);
323
- const context = annotation.node.context || 'default';
324
- if (options?.annotationsFull) {
325
- // Full content
326
- lines.push(`${icon} ${style} [${context}]:`);
390
+ const styleOrder = ['ERROR', 'WARNING', 'INFO', 'SUCCESS'];
391
+ for (const style of styleOrder) {
392
+ if (grouped[style]) {
393
+ for (const annotation of grouped[style]) {
394
+ const icon = this.getAnnotationIcon(style);
395
+ const context = annotation.node.context || 'default';
396
+ const styleColored = this.colorizeAnnotationStyle(style);
397
+ // When showing annotation details, always show the body text
398
+ lines.push(`${icon} ${styleColored}: ${context}`);
327
399
  const body = htmlToText(annotation.node.body?.html || '', {
328
400
  wordwrap: 80,
329
401
  preserveNewlines: true
@@ -331,13 +403,9 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
331
403
  lines.push(body.split('\n').map(l => ` ${l}`).join('\n'));
332
404
  lines.push('');
333
405
  }
334
- else {
335
- // Summary only
336
- lines.push(`${icon} ${style} [${context}]`);
337
- }
338
406
  }
339
407
  }
340
- return lines.join('\n');
408
+ return lines.join('\n').trim();
341
409
  }
342
410
  formatJobDetails(jobs, options) {
343
411
  if (!jobs || jobs.length === 0) {
@@ -348,15 +416,15 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
348
416
  // Summary line
349
417
  const parts = [];
350
418
  if (jobStats.passed > 0)
351
- parts.push(`✅ ${jobStats.passed} passed`);
419
+ parts.push(`${getStateIcon('PASSED')} ${jobStats.passed} passed`);
352
420
  if (jobStats.failed > 0)
353
- parts.push(`❌ ${jobStats.failed} failed`);
421
+ parts.push(`${getStateIcon('FAILED')} ${jobStats.failed} failed`);
354
422
  if (jobStats.running > 0)
355
- parts.push(`🔄 ${jobStats.running} running`);
423
+ parts.push(`${getStateIcon('RUNNING')} ${jobStats.running} running`);
356
424
  if (jobStats.blocked > 0)
357
- parts.push(`⏸️ ${jobStats.blocked} blocked`);
425
+ parts.push(`${getStateIcon('BLOCKED')} ${jobStats.blocked} blocked`);
358
426
  if (jobStats.skipped > 0)
359
- parts.push(`⏭️ ${jobStats.skipped} skipped`);
427
+ parts.push(`${getStateIcon('SKIPPED')} ${jobStats.skipped} skipped`);
360
428
  lines.push(`Jobs: ${parts.join(' ')}`);
361
429
  lines.push('');
362
430
  // Filter jobs based on options
@@ -375,22 +443,45 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
375
443
  for (const job of stateJobs) {
376
444
  const label = this.parseEmoji(job.node.label);
377
445
  const duration = this.formatJobDuration(job.node);
378
- const exitCode = job.node.exitStatus ? `, exit ${job.node.exitStatus}` : '';
379
- lines.push(` ${label} (${duration}${exitCode})`);
380
- if (options?.full && job.node.agent) {
381
- lines.push(` ${SEMANTIC_COLORS.dim(`Agent: ${job.node.agent.name || job.node.agent.hostname}`)}`);
446
+ // Basic job line
447
+ lines.push(` ${label} (${duration})`);
448
+ // Show additional details if --jobs or --full
449
+ if (options?.jobs || options?.full) {
450
+ // Timing details
451
+ if (job.node.startedAt) {
452
+ const startTime = new Date(job.node.startedAt).toLocaleTimeString();
453
+ const endTime = job.node.finishedAt
454
+ ? new Date(job.node.finishedAt).toLocaleTimeString()
455
+ : 'still running';
456
+ lines.push(` ${SEMANTIC_COLORS.dim(`${getProgressIcon('TIMING')} ${startTime} → ${endTime}`)}`);
457
+ }
458
+ // Parallel group info
459
+ if (job.node.parallelGroupIndex !== undefined && job.node.parallelGroupTotal) {
460
+ lines.push(` ${SEMANTIC_COLORS.dim(`${getProgressIcon('PARALLEL')} Parallel: ${job.node.parallelGroupIndex + 1}/${job.node.parallelGroupTotal}`)}`);
461
+ }
462
+ // Retry info
463
+ if (job.node.retried) {
464
+ lines.push(` ${SEMANTIC_COLORS.warning(`${getProgressIcon('RETRY')} Retried`)}`);
465
+ }
382
466
  }
383
467
  }
384
468
  lines.push('');
385
469
  }
386
470
  return lines.join('\n').trim();
387
471
  }
388
- formatFailedJobsSummary(failedJobs) {
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) {
389
478
  const lines = [];
390
479
  // Group identical jobs by label
391
480
  const jobGroups = this.groupJobsByLabel(failedJobs);
392
- // Show first 10 unique job types
393
- const displayGroups = jobGroups.slice(0, 10);
481
+ // Show all groups if --all-jobs, otherwise limit to 10
482
+ const displayGroups = options?.allJobs
483
+ ? jobGroups
484
+ : jobGroups.slice(0, 10);
394
485
  for (const group of displayGroups) {
395
486
  const label = this.parseEmoji(group.label);
396
487
  if (group.count === 1) {
@@ -415,34 +506,43 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
415
506
  if (group.stateCounts.other > 0) {
416
507
  statusParts.push(`${group.stateCounts.other} other`);
417
508
  }
418
- // Add exit codes if available
419
- if (group.exitCodes.length > 0) {
420
- const exitCodeStr = group.exitCodes.length === 1
421
- ? `exit ${group.exitCodes[0]}`
422
- : `exits: ${group.exitCodes.join(', ')}`;
423
- statusParts.push(exitCodeStr);
424
- }
425
509
  const statusInfo = statusParts.join(', ') || 'various states';
426
- lines.push(` ${SEMANTIC_COLORS.error('Failed')}: ${label} (${SEMANTIC_COLORS.count(String(group.count))} jobs: ${statusInfo})`);
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}`);
427
513
  }
428
514
  }
429
- // Add summary if there are more job types
430
- const remaining = jobGroups.length - displayGroups.length;
431
- if (remaining > 0) {
432
- lines.push(` ${SEMANTIC_COLORS.muted(`...and ${remaining} more job types`)}`);
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
+ }
433
530
  }
434
531
  return lines.join('\n');
435
532
  }
436
533
  groupJobsByLabel(jobs) {
437
534
  const groups = new Map();
438
535
  for (const job of jobs) {
439
- const label = job.node.label || 'Unnamed job';
440
- if (!groups.has(label)) {
441
- groups.set(label, {
442
- label,
536
+ const fullLabel = job.node.label || 'Unnamed job';
537
+ // Strip parallel job index from label for grouping
538
+ // e.g., "deposit_and_filing_schedule_calculator rspec (1/22)" -> "deposit_and_filing_schedule_calculator rspec"
539
+ const baseLabel = fullLabel.replace(/\s*\(\d+\/\d+\)\s*$/, '').trim();
540
+ if (!groups.has(baseLabel)) {
541
+ groups.set(baseLabel, {
542
+ label: baseLabel,
443
543
  count: 0,
444
544
  jobs: [],
445
- exitCodes: new Set(),
545
+ parallelTotal: 0,
446
546
  stateCounts: {
447
547
  failed: 0,
448
548
  broken: 0,
@@ -452,18 +552,41 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
452
552
  }
453
553
  });
454
554
  }
455
- const group = groups.get(label);
555
+ const group = groups.get(baseLabel);
456
556
  group.count++;
457
557
  group.jobs.push(job);
458
- // Track exit codes
459
- if (job.node.exitStatus !== null && job.node.exitStatus !== undefined) {
460
- group.exitCodes.add(job.node.exitStatus);
558
+ // Track the maximum parallel total for this job group
559
+ if (job.node.parallelGroupTotal && job.node.parallelGroupTotal > group.parallelTotal) {
560
+ group.parallelTotal = job.node.parallelGroupTotal;
461
561
  }
462
562
  // Count by state
463
563
  const state = job.node.state?.toUpperCase();
464
- if (!job.node.startedAt) {
564
+ // Use exit status as source of truth when available
565
+ // Note: exitStatus comes as a string from Buildkite API
566
+ if (job.node.exitStatus !== null && job.node.exitStatus !== undefined) {
567
+ const exitCode = parseInt(job.node.exitStatus, 10);
568
+ if (exitCode === 0) {
569
+ group.stateCounts.passed++;
570
+ }
571
+ else {
572
+ group.stateCounts.failed++;
573
+ }
574
+ }
575
+ else if (!job.node.startedAt) {
465
576
  group.stateCounts.notStarted++;
466
577
  }
578
+ else if (state === 'FINISHED' || state === 'COMPLETED') {
579
+ // For finished jobs without exit status, check passed field
580
+ if (job.node.passed === true) {
581
+ group.stateCounts.passed++;
582
+ }
583
+ else if (job.node.passed === false) {
584
+ group.stateCounts.failed++;
585
+ }
586
+ else {
587
+ group.stateCounts.other++;
588
+ }
589
+ }
467
590
  else if (state === 'FAILED') {
468
591
  group.stateCounts.failed++;
469
592
  }
@@ -479,7 +602,6 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
479
602
  }
480
603
  // Convert to array and sort by count (most failures first)
481
604
  return Array.from(groups.values())
482
- .map(g => ({ ...g, exitCodes: Array.from(g.exitCodes) }))
483
605
  .sort((a, b) => b.count - a.count);
484
606
  }
485
607
  formatDuration(build) {
@@ -528,37 +650,35 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
528
650
  }
529
651
  }
530
652
  getStatusIcon(state) {
531
- const icons = {
532
- 'PASSED': '✅',
533
- 'FAILED': '❌',
534
- 'RUNNING': '🔄',
535
- 'BLOCKED': '⏸️',
536
- 'CANCELED': '🚫',
537
- 'SCHEDULED': '📅',
538
- 'SKIPPED': '⏭️'
539
- };
540
- return icons[state] || '❓';
653
+ return getStateIcon(state);
541
654
  }
542
655
  getJobStateIcon(state) {
543
- const icons = {
544
- 'passed': '✅',
545
- 'failed': '❌',
546
- 'running': '🔄',
547
- 'blocked': '⏸️',
548
- 'canceled': '🚫',
549
- 'scheduled': '📅',
550
- 'skipped': '⏭️'
551
- };
552
- return icons[state.toLowerCase()] || '❓';
656
+ return getStateIcon(state);
553
657
  }
554
658
  getAnnotationIcon(style) {
555
- const icons = {
556
- 'ERROR': '❌',
557
- 'WARNING': '⚠️',
558
- 'INFO': 'ℹ️',
559
- 'SUCCESS': '✅'
560
- };
561
- return icons[style.toUpperCase()] || '📝';
659
+ return getAnnotationIcon(style);
660
+ }
661
+ colorizeAnnotationStyle(style) {
662
+ switch (style.toUpperCase()) {
663
+ case 'ERROR':
664
+ return SEMANTIC_COLORS.error(style.toLowerCase());
665
+ case 'WARNING':
666
+ return SEMANTIC_COLORS.warning(style.toLowerCase());
667
+ case 'INFO':
668
+ return SEMANTIC_COLORS.info(style.toLowerCase());
669
+ case 'SUCCESS':
670
+ return SEMANTIC_COLORS.success(style.toLowerCase());
671
+ default:
672
+ return style.toLowerCase();
673
+ }
674
+ }
675
+ colorizeStatusIcon(icon, state) {
676
+ const upperState = state.toUpperCase();
677
+ const theme = BUILD_STATUS_THEME[upperState];
678
+ if (!theme) {
679
+ return SEMANTIC_COLORS.muted(icon);
680
+ }
681
+ return theme.color(icon);
562
682
  }
563
683
  getJobStats(jobs) {
564
684
  const stats = {
@@ -575,13 +695,18 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
575
695
  return stats;
576
696
  for (const job of jobs) {
577
697
  const state = job.node.state?.toUpperCase() || '';
578
- if (state === 'PASSED' || (job.node.passed === true && state !== 'BROKEN')) {
579
- stats.passed++;
580
- stats.completed++;
581
- }
582
- else if (state === 'FAILED' || state === 'BROKEN' || (job.node.exitStatus && job.node.exitStatus !== 0) || job.node.passed === false) {
583
- stats.failed++;
584
- stats.completed++;
698
+ // If we have an exit status, use that as the source of truth
699
+ // Note: exitStatus comes as a string from Buildkite API
700
+ if (job.node.exitStatus !== null && job.node.exitStatus !== undefined) {
701
+ const exitCode = parseInt(job.node.exitStatus, 10);
702
+ if (exitCode === 0) {
703
+ stats.passed++;
704
+ stats.completed++;
705
+ }
706
+ else {
707
+ stats.failed++;
708
+ stats.completed++;
709
+ }
585
710
  }
586
711
  else if (state === 'RUNNING') {
587
712
  stats.running++;
@@ -596,6 +721,25 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
596
721
  else if (state === 'SCHEDULED' || state === 'ASSIGNED') {
597
722
  stats.queued++;
598
723
  }
724
+ else if (state === 'FINISHED' || state === 'COMPLETED') {
725
+ // For finished jobs without exit status, check passed field
726
+ if (job.node.passed === true) {
727
+ stats.passed++;
728
+ stats.completed++;
729
+ }
730
+ else if (job.node.passed === false) {
731
+ stats.failed++;
732
+ stats.completed++;
733
+ }
734
+ }
735
+ else if (state === 'PASSED' || job.node.passed === true) {
736
+ stats.passed++;
737
+ stats.completed++;
738
+ }
739
+ else if (state === 'FAILED' || state === 'BROKEN' || job.node.passed === false) {
740
+ stats.failed++;
741
+ stats.completed++;
742
+ }
599
743
  }
600
744
  return stats;
601
745
  }
@@ -604,7 +748,14 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
604
748
  return [];
605
749
  return jobs.filter(job => {
606
750
  const state = job.node.state?.toUpperCase();
607
- return state === 'FAILED' || state === 'BROKEN' || (job.node.exitStatus && job.node.exitStatus !== 0) || job.node.passed === false;
751
+ // If we have an exit status, use that as the source of truth
752
+ // Note: exitStatus comes as a string from Buildkite API
753
+ if (job.node.exitStatus !== null && job.node.exitStatus !== undefined) {
754
+ const exitCode = parseInt(job.node.exitStatus, 10);
755
+ return exitCode !== 0;
756
+ }
757
+ // Otherwise fall back to state
758
+ return state === 'FAILED' || state === 'BROKEN' || job.node.passed === false;
608
759
  });
609
760
  }
610
761
  getRunningJobs(jobs) {
@@ -629,11 +780,16 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
629
780
  return grouped;
630
781
  for (const job of jobs) {
631
782
  const state = job.node.state?.toUpperCase() || '';
632
- if (state === 'FAILED' || state === 'BROKEN' || (job.node.exitStatus && job.node.exitStatus !== 0) || job.node.passed === false) {
633
- grouped['Failed'].push(job);
634
- }
635
- else if (state === 'PASSED' || (job.node.passed === true && state !== 'BROKEN')) {
636
- grouped['Passed'].push(job);
783
+ // If we have an exit status, use that as the source of truth
784
+ // Note: exitStatus comes as a string from Buildkite API
785
+ if (job.node.exitStatus !== null && job.node.exitStatus !== undefined) {
786
+ const exitCode = parseInt(job.node.exitStatus, 10);
787
+ if (exitCode === 0) {
788
+ grouped['Passed'].push(job);
789
+ }
790
+ else {
791
+ grouped['Failed'].push(job);
792
+ }
637
793
  }
638
794
  else if (state === 'RUNNING') {
639
795
  grouped['Running'].push(job);
@@ -644,6 +800,21 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
644
800
  else if (state === 'SKIPPED' || state === 'CANCELED') {
645
801
  grouped['Skipped'].push(job);
646
802
  }
803
+ else if (state === 'FINISHED' || state === 'COMPLETED') {
804
+ // For finished jobs without exit status, check passed field
805
+ if (job.node.passed === true) {
806
+ grouped['Passed'].push(job);
807
+ }
808
+ else if (job.node.passed === false) {
809
+ grouped['Failed'].push(job);
810
+ }
811
+ }
812
+ else if (state === 'PASSED' || job.node.passed === true) {
813
+ grouped['Passed'].push(job);
814
+ }
815
+ else if (state === 'FAILED' || state === 'BROKEN' || job.node.passed === false) {
816
+ grouped['Failed'].push(job);
817
+ }
647
818
  }
648
819
  return grouped;
649
820
  }