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.
- package/dist/commands/ShowBuild.js +4 -0
- package/dist/commands/ShowBuild.js.map +1 -1
- package/dist/formatters/build-detail/PlainTextFormatter.js +283 -112
- package/dist/formatters/build-detail/PlainTextFormatter.js.map +1 -1
- package/dist/formatters/builds/PlainTextFormatter.js +4 -2
- package/dist/formatters/builds/PlainTextFormatter.js.map +1 -1
- package/dist/formatters/pipelines/PlainTextFormatter.js +3 -6
- package/dist/formatters/pipelines/PlainTextFormatter.js.map +1 -1
- package/dist/graphql/queries.js +4 -0
- package/dist/graphql/queries.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/services/BuildkiteClient.js +32 -31
- package/dist/services/BuildkiteClient.js.map +1 -1
- package/dist/services/BuildkiteRestClient.js +8 -7
- package/dist/services/BuildkiteRestClient.js.map +1 -1
- package/dist/ui/theme.js +193 -8
- package/dist/ui/theme.js.map +1 -1
- package/dist/utils/terminal-links.js +165 -0
- package/dist/utils/terminal-links.js.map +1 -0
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
186
|
+
lines.push(this.formatAnnotationDetails(build.annotations.edges));
|
|
179
187
|
}
|
|
180
|
-
//
|
|
188
|
+
// Collect all hints for more info
|
|
181
189
|
if (!options?.failed && failedJobs.length > 0) {
|
|
182
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
333
|
+
lines.push(this.formatAnnotationDetails(build.annotations.edges));
|
|
285
334
|
}
|
|
286
335
|
return lines.join('\n');
|
|
287
336
|
}
|
|
288
337
|
formatHeader(build) {
|
|
289
|
-
const
|
|
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 `${
|
|
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
|
|
361
|
+
const countParts = [];
|
|
305
362
|
if (counts.ERROR > 0)
|
|
306
|
-
|
|
363
|
+
countParts.push(SEMANTIC_COLORS.error(`${counts.ERROR} error${counts.ERROR > 1 ? 's' : ''}`));
|
|
307
364
|
if (counts.WARNING > 0)
|
|
308
|
-
|
|
365
|
+
countParts.push(SEMANTIC_COLORS.warning(`${counts.WARNING} warning${counts.WARNING > 1 ? 's' : ''}`));
|
|
309
366
|
if (counts.INFO > 0)
|
|
310
|
-
|
|
367
|
+
countParts.push(SEMANTIC_COLORS.info(`${counts.INFO} info`));
|
|
311
368
|
if (counts.SUCCESS > 0)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
386
|
+
formatAnnotationDetails(annotations) {
|
|
317
387
|
const lines = [];
|
|
318
388
|
// Group annotations by style
|
|
319
389
|
const grouped = this.groupAnnotationsByStyle(annotations);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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(
|
|
419
|
+
parts.push(`${getStateIcon('PASSED')} ${jobStats.passed} passed`);
|
|
352
420
|
if (jobStats.failed > 0)
|
|
353
|
-
parts.push(
|
|
421
|
+
parts.push(`${getStateIcon('FAILED')} ${jobStats.failed} failed`);
|
|
354
422
|
if (jobStats.running > 0)
|
|
355
|
-
parts.push(
|
|
423
|
+
parts.push(`${getStateIcon('RUNNING')} ${jobStats.running} running`);
|
|
356
424
|
if (jobStats.blocked > 0)
|
|
357
|
-
parts.push(
|
|
425
|
+
parts.push(`${getStateIcon('BLOCKED')} ${jobStats.blocked} blocked`);
|
|
358
426
|
if (jobStats.skipped > 0)
|
|
359
|
-
parts.push(
|
|
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
|
-
|
|
379
|
-
lines.push(` ${label} (${duration}
|
|
380
|
-
if
|
|
381
|
-
|
|
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
|
-
|
|
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
|
|
393
|
-
const displayGroups =
|
|
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
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
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(
|
|
555
|
+
const group = groups.get(baseLabel);
|
|
456
556
|
group.count++;
|
|
457
557
|
group.jobs.push(job);
|
|
458
|
-
// Track
|
|
459
|
-
if (job.node.
|
|
460
|
-
group.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
'
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
}
|