bktide 1.0.1755559112 → 1.0.1755639164
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/dist/commands/ShowBuild.js +31 -5
- package/dist/commands/ShowBuild.js.map +1 -1
- package/dist/formatters/build-detail/PlainTextFormatter.js +533 -180
- 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/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 -53
- package/dist/graphql/queries.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.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 +109 -32
- 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/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/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 +19 -3
|
@@ -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,28 +127,40 @@ 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 = [];
|
|
137
140
|
// Header line
|
|
138
141
|
lines.push(this.formatHeader(build));
|
|
139
142
|
lines.push(this.formatCommitInfo(build));
|
|
143
|
+
lines.push(''); // Blank line after commit info
|
|
140
144
|
// Show annotations summary if present
|
|
141
145
|
if (build.annotations?.edges?.length > 0) {
|
|
142
|
-
lines.push('');
|
|
143
146
|
lines.push(this.formatAnnotationSummary(build.annotations.edges));
|
|
147
|
+
}
|
|
148
|
+
// Jobs summary
|
|
149
|
+
if (build.jobs?.edges?.length > 0) {
|
|
150
|
+
if (build.annotations?.edges?.length > 0) {
|
|
151
|
+
lines.push(''); // Add space between annotations and steps
|
|
152
|
+
}
|
|
153
|
+
lines.push(this.formatJobSummary(build.jobs, build.state));
|
|
144
154
|
if (!options?.annotations) {
|
|
145
|
-
lines.push(
|
|
155
|
+
lines.push('');
|
|
156
|
+
const tips = formatTips(['Use --annotations to view annotation details'], TipStyle.GROUPED);
|
|
157
|
+
lines.push(tips);
|
|
146
158
|
}
|
|
147
159
|
}
|
|
148
160
|
// Show annotations detail if requested
|
|
149
161
|
if (options?.annotations) {
|
|
150
162
|
lines.push('');
|
|
151
|
-
lines.push(this.formatAnnotationDetails(build.annotations.edges
|
|
163
|
+
lines.push(this.formatAnnotationDetails(build.annotations.edges));
|
|
152
164
|
}
|
|
153
165
|
return lines.join('\n');
|
|
154
166
|
}
|
|
@@ -158,15 +170,18 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
158
170
|
lines.push(this.formatHeader(build));
|
|
159
171
|
lines.push(this.formatCommitInfo(build));
|
|
160
172
|
lines.push('');
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (failedJobs.length > 0) {
|
|
164
|
-
lines.push(this.formatFailedJobsSummary(failedJobs));
|
|
165
|
-
}
|
|
166
|
-
// Annotation summary
|
|
173
|
+
const allHints = [];
|
|
174
|
+
// Annotation summary (first, as it appears first in UI)
|
|
167
175
|
if (build.annotations?.edges?.length > 0) {
|
|
168
176
|
lines.push(this.formatAnnotationSummary(build.annotations.edges));
|
|
169
177
|
}
|
|
178
|
+
// Jobs summary
|
|
179
|
+
if (build.jobs?.edges?.length > 0) {
|
|
180
|
+
if (build.annotations?.edges?.length > 0) {
|
|
181
|
+
lines.push(''); // Add space between annotations and steps
|
|
182
|
+
}
|
|
183
|
+
lines.push(this.formatJobSummary(build.jobs, build.state));
|
|
184
|
+
}
|
|
170
185
|
// Show detailed job info if requested
|
|
171
186
|
if (options?.jobs || options?.failed) {
|
|
172
187
|
lines.push('');
|
|
@@ -175,15 +190,24 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
175
190
|
// Show annotations detail if requested
|
|
176
191
|
if (options?.annotations) {
|
|
177
192
|
lines.push('');
|
|
178
|
-
lines.push(this.formatAnnotationDetails(build.annotations.edges
|
|
193
|
+
lines.push(this.formatAnnotationDetails(build.annotations.edges));
|
|
179
194
|
}
|
|
180
|
-
//
|
|
195
|
+
// Collect all hints for more info
|
|
196
|
+
const failedJobs = this.getFailedJobs(build.jobs?.edges);
|
|
181
197
|
if (!options?.failed && failedJobs.length > 0) {
|
|
182
|
-
|
|
183
|
-
lines.push(SEMANTIC_COLORS.dim(`→ bin/bktide build ${build.number} --failed # show failure details`));
|
|
198
|
+
allHints.push('Use --failed to show failure details');
|
|
184
199
|
}
|
|
185
200
|
if (!options?.annotations && build.annotations?.edges?.length > 0) {
|
|
186
|
-
|
|
201
|
+
allHints.push('Use --annotations to view annotation details');
|
|
202
|
+
}
|
|
203
|
+
// Add hint about incomplete step data if truncated
|
|
204
|
+
if (!options?.jobs && build.jobs?.pageInfo?.hasNextPage) {
|
|
205
|
+
allHints.push('Use --jobs to fetch all step data (currently showing first 100 only)');
|
|
206
|
+
}
|
|
207
|
+
// Display all hints together
|
|
208
|
+
if (allHints.length > 0) {
|
|
209
|
+
lines.push('');
|
|
210
|
+
lines.push(formatTips(allHints, TipStyle.GROUPED));
|
|
187
211
|
}
|
|
188
212
|
return lines.join('\n');
|
|
189
213
|
}
|
|
@@ -193,20 +217,38 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
193
217
|
lines.push(this.formatHeader(build));
|
|
194
218
|
lines.push(this.formatCommitInfo(build));
|
|
195
219
|
lines.push('');
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
220
|
+
// Annotations first (if any)
|
|
221
|
+
if (build.annotations?.edges?.length > 0) {
|
|
222
|
+
lines.push(this.formatAnnotationSummary(build.annotations.edges));
|
|
223
|
+
}
|
|
224
|
+
// Jobs summary with progress
|
|
225
|
+
if (build.jobs?.edges?.length > 0) {
|
|
226
|
+
if (build.annotations?.edges?.length > 0) {
|
|
227
|
+
lines.push(''); // Add space between annotations and steps
|
|
228
|
+
}
|
|
229
|
+
lines.push(this.formatJobSummary(build.jobs, build.state));
|
|
230
|
+
}
|
|
199
231
|
// Show running jobs
|
|
200
232
|
const runningJobs = this.getRunningJobs(build.jobs?.edges);
|
|
201
233
|
if (runningJobs.length > 0) {
|
|
202
234
|
const labels = runningJobs.map(j => this.parseEmoji(j.node.label)).join(', ');
|
|
203
235
|
lines.push(`${SEMANTIC_COLORS.info('Running')}: ${labels}`);
|
|
204
236
|
}
|
|
237
|
+
// Annotation summary
|
|
238
|
+
if (build.annotations?.edges?.length > 0) {
|
|
239
|
+
lines.push('');
|
|
240
|
+
lines.push(this.formatAnnotationSummary(build.annotations.edges));
|
|
241
|
+
}
|
|
205
242
|
// Show job details if requested
|
|
206
243
|
if (options?.jobs) {
|
|
207
244
|
lines.push('');
|
|
208
245
|
lines.push(this.formatJobDetails(build.jobs?.edges, options));
|
|
209
246
|
}
|
|
247
|
+
// Show annotations detail if requested
|
|
248
|
+
if (options?.annotations) {
|
|
249
|
+
lines.push('');
|
|
250
|
+
lines.push(this.formatAnnotationDetails(build.annotations.edges));
|
|
251
|
+
}
|
|
210
252
|
return lines.join('\n');
|
|
211
253
|
}
|
|
212
254
|
formatBlockedBuild(build, options) {
|
|
@@ -215,21 +257,33 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
215
257
|
lines.push(this.formatHeader(build));
|
|
216
258
|
lines.push(this.formatCommitInfo(build));
|
|
217
259
|
lines.push('');
|
|
260
|
+
// Annotations first (if any)
|
|
261
|
+
if (build.annotations?.edges?.length > 0) {
|
|
262
|
+
lines.push(this.formatAnnotationSummary(build.annotations.edges));
|
|
263
|
+
}
|
|
264
|
+
// Jobs summary
|
|
265
|
+
if (build.jobs?.edges?.length > 0) {
|
|
266
|
+
if (build.annotations?.edges?.length > 0) {
|
|
267
|
+
lines.push(''); // Add space between annotations and steps
|
|
268
|
+
}
|
|
269
|
+
lines.push(this.formatJobSummary(build.jobs, build.state));
|
|
270
|
+
}
|
|
218
271
|
// Blocked information
|
|
219
272
|
const blockedJobs = this.getBlockedJobs(build.jobs?.edges);
|
|
220
273
|
if (blockedJobs.length > 0) {
|
|
221
|
-
lines.push(
|
|
222
|
-
|
|
223
|
-
// Show jobs summary
|
|
224
|
-
const jobStats = this.getJobStats(build.jobs?.edges);
|
|
225
|
-
if (jobStats.completed > 0) {
|
|
226
|
-
lines.push(`✅ ${jobStats.completed} jobs passed before block`);
|
|
274
|
+
lines.push('');
|
|
275
|
+
lines.push(`${getProgressIcon('BLOCKED_MESSAGE')} Blocked: "${blockedJobs[0].node.label}" (manual unblock required)`);
|
|
227
276
|
}
|
|
228
277
|
// Show job details if requested
|
|
229
278
|
if (options?.jobs) {
|
|
230
279
|
lines.push('');
|
|
231
280
|
lines.push(this.formatJobDetails(build.jobs?.edges, options));
|
|
232
281
|
}
|
|
282
|
+
// Show annotations detail if requested
|
|
283
|
+
if (options?.annotations) {
|
|
284
|
+
lines.push('');
|
|
285
|
+
lines.push(this.formatAnnotationDetails(build.annotations.edges));
|
|
286
|
+
}
|
|
233
287
|
return lines.join('\n');
|
|
234
288
|
}
|
|
235
289
|
formatCanceledBuild(build, options) {
|
|
@@ -238,14 +292,23 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
238
292
|
lines.push(this.formatHeader(build));
|
|
239
293
|
lines.push(this.formatCommitInfo(build));
|
|
240
294
|
lines.push('');
|
|
295
|
+
// Annotations first (if any)
|
|
296
|
+
if (build.annotations?.edges?.length > 0) {
|
|
297
|
+
lines.push(this.formatAnnotationSummary(build.annotations.edges));
|
|
298
|
+
}
|
|
299
|
+
// Jobs summary
|
|
300
|
+
if (build.jobs?.edges?.length > 0) {
|
|
301
|
+
if (build.annotations?.edges?.length > 0) {
|
|
302
|
+
lines.push(''); // Add space between annotations and steps
|
|
303
|
+
}
|
|
304
|
+
lines.push(this.formatJobSummary(build.jobs, build.state));
|
|
305
|
+
}
|
|
241
306
|
// Canceled information
|
|
242
307
|
if (build.createdBy) {
|
|
308
|
+
lines.push('');
|
|
243
309
|
const creator = build.createdBy.name || build.createdBy.email;
|
|
244
310
|
lines.push(`Canceled by: ${creator}`);
|
|
245
311
|
}
|
|
246
|
-
// Show jobs summary
|
|
247
|
-
const jobStats = this.getJobStats(build.jobs?.edges);
|
|
248
|
-
lines.push(`Completed: ${jobStats.completed}/${jobStats.total} jobs before cancellation`);
|
|
249
312
|
// Show job details if requested
|
|
250
313
|
if (options?.jobs) {
|
|
251
314
|
lines.push('');
|
|
@@ -268,62 +331,185 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
268
331
|
lines.push(` Organization: ${build.organization?.name || 'Unknown'}`);
|
|
269
332
|
lines.push(` Pipeline: ${build.pipeline?.name || 'Unknown'}`);
|
|
270
333
|
if (build.pullRequest) {
|
|
271
|
-
|
|
334
|
+
// Try to construct PR URL from repository URL
|
|
335
|
+
const repoUrl = build.pipeline?.repository?.url;
|
|
336
|
+
if (repoUrl && repoUrl.includes('github.com')) {
|
|
337
|
+
// Extract owner/repo from various GitHub URL formats
|
|
338
|
+
const match = repoUrl.match(/github\.com[/:]([\w-]+)\/([\w-]+)/);
|
|
339
|
+
if (match && build.pullRequest.id) {
|
|
340
|
+
// Extract PR number from GraphQL ID if possible
|
|
341
|
+
// GitHub PR IDs often contain the number
|
|
342
|
+
const prUrl = `https://github.com/${match[1]}/${match[2]}/pull/${build.pullRequest.id}`;
|
|
343
|
+
lines.push(` Pull Request: ${SEMANTIC_COLORS.url(prUrl)}`);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
lines.push(` Pull Request: ${build.pullRequest.id}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
lines.push(` Pull Request: ${build.pullRequest.id}`);
|
|
351
|
+
}
|
|
272
352
|
}
|
|
273
353
|
if (build.triggeredFrom) {
|
|
274
354
|
lines.push(` Triggered from: ${build.triggeredFrom.pipeline?.name} #${build.triggeredFrom.number}`);
|
|
275
355
|
}
|
|
276
356
|
lines.push('');
|
|
277
|
-
//
|
|
278
|
-
lines.push('
|
|
357
|
+
// Steps section
|
|
358
|
+
lines.push('Steps:');
|
|
279
359
|
lines.push(this.formatJobDetails(build.jobs?.edges, { ...options, full: true }));
|
|
280
360
|
// Annotations section
|
|
281
361
|
if (build.annotations?.edges?.length > 0) {
|
|
282
362
|
lines.push('');
|
|
283
363
|
lines.push('Annotations:');
|
|
284
|
-
lines.push(this.formatAnnotationDetails(build.annotations.edges
|
|
364
|
+
lines.push(this.formatAnnotationDetails(build.annotations.edges));
|
|
285
365
|
}
|
|
286
366
|
return lines.join('\n');
|
|
287
367
|
}
|
|
288
368
|
formatHeader(build) {
|
|
289
|
-
const
|
|
369
|
+
const statusIcon = this.getStatusIcon(build.state);
|
|
370
|
+
// Apply appropriate color to the icon based on the state
|
|
371
|
+
const coloredIcon = this.colorizeStatusIcon(statusIcon, build.state);
|
|
290
372
|
const stateFormatted = formatBuildStatus(build.state, { useSymbol: false });
|
|
291
373
|
const duration = this.formatDuration(build);
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
374
|
+
// Get first line of commit message
|
|
375
|
+
const message = build.message || 'No commit message';
|
|
376
|
+
const firstLineMessage = message.split('\n')[0];
|
|
377
|
+
const truncatedMessage = firstLineMessage.length > 80 ? firstLineMessage.substring(0, 77) + '...' : firstLineMessage;
|
|
378
|
+
return `${coloredIcon} ${stateFormatted} ${truncatedMessage} ${SEMANTIC_COLORS.dim(`#${build.number}`)} ${SEMANTIC_COLORS.dim(duration)}`;
|
|
295
379
|
}
|
|
296
380
|
formatCommitInfo(build) {
|
|
297
381
|
const shortSha = build.commit ? build.commit.substring(0, 7) : 'unknown';
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
|
|
382
|
+
const branch = SEMANTIC_COLORS.identifier(build.branch);
|
|
383
|
+
const age = this.formatAge(build.createdAt);
|
|
384
|
+
// Get author information
|
|
385
|
+
const author = build.createdBy?.name || build.createdBy?.email || 'Unknown';
|
|
386
|
+
// Calculate indentation to align with commit message
|
|
387
|
+
// Map each state to its proper indentation (icon + space + state text + space)
|
|
388
|
+
const indentMap = {
|
|
389
|
+
'PASSED': 9, // ✓ PASSED
|
|
390
|
+
'FAILED': 9, // ✗ FAILED
|
|
391
|
+
'RUNNING': 10, // ⟳ RUNNING
|
|
392
|
+
'BLOCKED': 10, // ◼ BLOCKED
|
|
393
|
+
'CANCELED': 11, // ⊘ CANCELED
|
|
394
|
+
'SCHEDULED': 12, // ⏱ SCHEDULED
|
|
395
|
+
'SKIPPED': 10, // ⊙ SKIPPED
|
|
396
|
+
};
|
|
397
|
+
const indent = ' '.repeat(indentMap[build.state] || 9);
|
|
398
|
+
return `${indent}${author} • ${branch} • ${shortSha} • ${SEMANTIC_COLORS.dim(`Created ${age}`)}`;
|
|
301
399
|
}
|
|
302
400
|
formatAnnotationSummary(annotations) {
|
|
401
|
+
if (!annotations || annotations.length === 0) {
|
|
402
|
+
return '';
|
|
403
|
+
}
|
|
404
|
+
const lines = [];
|
|
405
|
+
const total = annotations.length;
|
|
406
|
+
// Header with count
|
|
303
407
|
const counts = this.countAnnotationsByStyle(annotations);
|
|
304
|
-
const
|
|
408
|
+
const countParts = [];
|
|
305
409
|
if (counts.ERROR > 0)
|
|
306
|
-
|
|
410
|
+
countParts.push(SEMANTIC_COLORS.error(`${counts.ERROR} error${counts.ERROR > 1 ? 's' : ''}`));
|
|
307
411
|
if (counts.WARNING > 0)
|
|
308
|
-
|
|
412
|
+
countParts.push(SEMANTIC_COLORS.warning(`${counts.WARNING} warning${counts.WARNING > 1 ? 's' : ''}`));
|
|
309
413
|
if (counts.INFO > 0)
|
|
310
|
-
|
|
414
|
+
countParts.push(SEMANTIC_COLORS.info(`${counts.INFO} info`));
|
|
311
415
|
if (counts.SUCCESS > 0)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
416
|
+
countParts.push(SEMANTIC_COLORS.success(`${counts.SUCCESS} success`));
|
|
417
|
+
lines.push(`${getAnnotationIcon('DEFAULT')} ${SEMANTIC_COLORS.count(String(total))} annotation${total > 1 ? 's' : ''}: ${countParts.join(', ')}`);
|
|
418
|
+
// List each annotation with style and context
|
|
419
|
+
const grouped = this.groupAnnotationsByStyle(annotations);
|
|
420
|
+
const styleOrder = ['ERROR', 'WARNING', 'INFO', 'SUCCESS'];
|
|
421
|
+
for (const style of styleOrder) {
|
|
422
|
+
if (grouped[style]) {
|
|
423
|
+
for (const annotation of grouped[style]) {
|
|
424
|
+
const icon = this.getAnnotationIcon(style);
|
|
425
|
+
const context = annotation.node.context || 'default';
|
|
426
|
+
const styleColored = this.colorizeAnnotationStyle(style);
|
|
427
|
+
lines.push(` ${icon} ${styleColored}: ${context}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return lines.join('\n');
|
|
315
432
|
}
|
|
316
|
-
|
|
433
|
+
formatJobSummary(jobsData, buildState) {
|
|
434
|
+
const jobs = jobsData?.edges;
|
|
435
|
+
if (!jobs || jobs.length === 0) {
|
|
436
|
+
return '';
|
|
437
|
+
}
|
|
438
|
+
const lines = [];
|
|
439
|
+
const jobStats = this.getJobStats(jobs);
|
|
440
|
+
// Build summary parts based on job states
|
|
441
|
+
const countParts = [];
|
|
442
|
+
if (jobStats.failed > 0)
|
|
443
|
+
countParts.push(SEMANTIC_COLORS.error(`${jobStats.failed} failed`));
|
|
444
|
+
if (jobStats.passed > 0)
|
|
445
|
+
countParts.push(SEMANTIC_COLORS.success(`${jobStats.passed} passed`));
|
|
446
|
+
if (jobStats.running > 0)
|
|
447
|
+
countParts.push(SEMANTIC_COLORS.info(`${jobStats.running} running`));
|
|
448
|
+
if (jobStats.blocked > 0)
|
|
449
|
+
countParts.push(SEMANTIC_COLORS.warning(`${jobStats.blocked} blocked`));
|
|
450
|
+
// Don't show skipped jobs
|
|
451
|
+
if (jobStats.canceled > 0)
|
|
452
|
+
countParts.push(SEMANTIC_COLORS.muted(`${jobStats.canceled} canceled`));
|
|
453
|
+
// Use appropriate icon based on build state
|
|
454
|
+
const icon = buildState === 'FAILED' ? getStateIcon('FAILED') :
|
|
455
|
+
buildState === 'RUNNING' ? getStateIcon('RUNNING') :
|
|
456
|
+
buildState === 'PASSED' ? getStateIcon('PASSED') :
|
|
457
|
+
buildState === 'BLOCKED' ? getStateIcon('BLOCKED') : '•';
|
|
458
|
+
// Check if we have partial data
|
|
459
|
+
const hasMorePages = jobsData?.pageInfo?.hasNextPage;
|
|
460
|
+
const totalCount = jobsData?.count;
|
|
461
|
+
if (hasMorePages) {
|
|
462
|
+
const showing = jobs.length;
|
|
463
|
+
const total = totalCount || `${showing}+`;
|
|
464
|
+
lines.push(`${icon} Showing ${SEMANTIC_COLORS.count(String(showing))} of ${SEMANTIC_COLORS.count(String(total))} steps: ${countParts.join(', ')}`);
|
|
465
|
+
lines.push(SEMANTIC_COLORS.warning('⚠️ Showing first 100 steps only (more available)'));
|
|
466
|
+
lines.push(SEMANTIC_COLORS.dim(' → Use --jobs to fetch all step data and see accurate statistics'));
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
lines.push(`${icon} ${SEMANTIC_COLORS.count(String(jobStats.total))} step${jobStats.total > 1 ? 's' : ''}: ${countParts.join(', ')}`);
|
|
470
|
+
}
|
|
471
|
+
// For failed builds, show the specific failed job names
|
|
472
|
+
if (buildState === 'FAILED') {
|
|
473
|
+
const failedJobs = this.getFailedJobs(jobs);
|
|
474
|
+
const jobGroups = this.groupJobsByLabel(failedJobs);
|
|
475
|
+
// Show up to 3 failed job types
|
|
476
|
+
const displayGroups = jobGroups.slice(0, 3);
|
|
477
|
+
for (const group of displayGroups) {
|
|
478
|
+
const label = this.parseEmoji(group.label);
|
|
479
|
+
const icon = getStateIcon('FAILED');
|
|
480
|
+
// Get duration for display
|
|
481
|
+
const duration = group.count === 1 && group.jobs[0]?.node
|
|
482
|
+
? ` ${SEMANTIC_COLORS.dim(`- ran ${this.formatJobDuration(group.jobs[0].node)}`)}`
|
|
483
|
+
: '';
|
|
484
|
+
if (group.parallelTotal > 0) {
|
|
485
|
+
lines.push(` ${icon} ${label} ${SEMANTIC_COLORS.dim(`(${group.stateCounts.failed || 0}/${group.parallelTotal} failed)`)}`);
|
|
486
|
+
}
|
|
487
|
+
else if (group.count > 1) {
|
|
488
|
+
lines.push(` ${icon} ${label} ${SEMANTIC_COLORS.dim(`(${group.stateCounts.failed || 0} failed)`)}`);
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
lines.push(` ${icon} ${label}${duration}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (jobGroups.length > 3) {
|
|
495
|
+
lines.push(` ${SEMANTIC_COLORS.muted(`...and ${jobGroups.length - 3} more`)}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return lines.join('\n');
|
|
499
|
+
}
|
|
500
|
+
formatAnnotationDetails(annotations) {
|
|
317
501
|
const lines = [];
|
|
318
502
|
// Group annotations by style
|
|
319
503
|
const grouped = this.groupAnnotationsByStyle(annotations);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
504
|
+
const styleOrder = ['ERROR', 'WARNING', 'INFO', 'SUCCESS'];
|
|
505
|
+
for (const style of styleOrder) {
|
|
506
|
+
if (grouped[style]) {
|
|
507
|
+
for (const annotation of grouped[style]) {
|
|
508
|
+
const icon = this.getAnnotationIcon(style);
|
|
509
|
+
const context = annotation.node.context || 'default';
|
|
510
|
+
const styleColored = this.colorizeAnnotationStyle(style);
|
|
511
|
+
// When showing annotation details, always show the body text
|
|
512
|
+
lines.push(`${icon} ${styleColored}: ${context}`);
|
|
327
513
|
const body = htmlToText(annotation.node.body?.html || '', {
|
|
328
514
|
wordwrap: 80,
|
|
329
515
|
preserveNewlines: true
|
|
@@ -331,118 +517,123 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
331
517
|
lines.push(body.split('\n').map(l => ` ${l}`).join('\n'));
|
|
332
518
|
lines.push('');
|
|
333
519
|
}
|
|
334
|
-
else {
|
|
335
|
-
// Summary only
|
|
336
|
-
lines.push(`${icon} ${style} [${context}]`);
|
|
337
|
-
}
|
|
338
520
|
}
|
|
339
521
|
}
|
|
340
|
-
return lines.join('\n');
|
|
522
|
+
return lines.join('\n').trim();
|
|
341
523
|
}
|
|
342
524
|
formatJobDetails(jobs, options) {
|
|
343
525
|
if (!jobs || jobs.length === 0) {
|
|
344
|
-
return 'No
|
|
526
|
+
return 'No steps found';
|
|
345
527
|
}
|
|
346
528
|
const lines = [];
|
|
347
529
|
const jobStats = this.getJobStats(jobs);
|
|
348
530
|
// Summary line
|
|
349
531
|
const parts = [];
|
|
350
532
|
if (jobStats.passed > 0)
|
|
351
|
-
parts.push(
|
|
533
|
+
parts.push(`${getStateIcon('PASSED')} ${jobStats.passed} passed`);
|
|
352
534
|
if (jobStats.failed > 0)
|
|
353
|
-
parts.push(
|
|
535
|
+
parts.push(`${getStateIcon('FAILED')} ${jobStats.failed} failed`);
|
|
354
536
|
if (jobStats.running > 0)
|
|
355
|
-
parts.push(
|
|
537
|
+
parts.push(`${getStateIcon('RUNNING')} ${jobStats.running} running`);
|
|
356
538
|
if (jobStats.blocked > 0)
|
|
357
|
-
parts.push(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
lines.push(`Jobs: ${parts.join(' ')}`);
|
|
539
|
+
parts.push(`${getStateIcon('BLOCKED')} ${jobStats.blocked} blocked`);
|
|
540
|
+
// Don't show skipped jobs in summary
|
|
541
|
+
lines.push(`Steps: ${parts.join(' ')}`);
|
|
361
542
|
lines.push('');
|
|
362
543
|
// Filter jobs based on options
|
|
363
544
|
let filteredJobs = jobs;
|
|
364
545
|
if (options?.failed) {
|
|
365
546
|
filteredJobs = this.getFailedJobs(jobs);
|
|
366
547
|
}
|
|
367
|
-
// Group jobs by state
|
|
548
|
+
// Group jobs by state first
|
|
368
549
|
const grouped = this.groupJobsByState(filteredJobs);
|
|
369
550
|
for (const [state, stateJobs] of Object.entries(grouped)) {
|
|
370
551
|
if (stateJobs.length === 0)
|
|
371
552
|
continue;
|
|
372
553
|
const icon = this.getJobStateIcon(state);
|
|
373
554
|
const stateColored = this.colorizeJobState(state);
|
|
555
|
+
// Collapse parallel jobs with same label
|
|
556
|
+
const collapsedGroups = this.collapseParallelJobs(stateJobs);
|
|
374
557
|
lines.push(`${icon} ${stateColored} (${SEMANTIC_COLORS.count(String(stateJobs.length))}):`);
|
|
375
|
-
for (const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
558
|
+
for (const group of collapsedGroups) {
|
|
559
|
+
if (group.isParallelGroup && group.jobs.length > 1) {
|
|
560
|
+
// Collapsed parallel group display
|
|
561
|
+
const label = this.parseEmoji(group.label);
|
|
562
|
+
const total = group.parallelTotal || group.jobs.length;
|
|
563
|
+
const passedCount = group.jobs.filter(j => this.isJobPassed(j.node)).length;
|
|
564
|
+
const failedCount = group.jobs.filter(j => this.isJobFailed(j.node)).length;
|
|
565
|
+
// Show summary line for parallel group
|
|
566
|
+
if (failedCount > 0) {
|
|
567
|
+
// If there are failures, show breakdown
|
|
568
|
+
// Apply state color to label
|
|
569
|
+
const coloredLabel = state === 'Failed' ? SEMANTIC_COLORS.error(label) :
|
|
570
|
+
state === 'Passed' ? SEMANTIC_COLORS.success(label) :
|
|
571
|
+
state === 'Running' ? SEMANTIC_COLORS.info(label) :
|
|
572
|
+
state === 'Blocked' ? SEMANTIC_COLORS.warning(label) : label;
|
|
573
|
+
lines.push(` ${coloredLabel} ${SEMANTIC_COLORS.dim(`(${passedCount}/${total} passed, ${failedCount} failed)`)}`);
|
|
574
|
+
// Show failed steps individually
|
|
575
|
+
const failedJobs = group.jobs.filter(j => this.isJobFailed(j.node));
|
|
576
|
+
for (const job of failedJobs) {
|
|
577
|
+
const duration = this.formatJobDuration(job.node);
|
|
578
|
+
const parallelInfo = job.node.parallelGroupIndex !== undefined
|
|
579
|
+
? ` ${SEMANTIC_COLORS.dim(`[Parallel: ${job.node.parallelGroupIndex + 1}/${job.node.parallelGroupTotal}]`)}`
|
|
580
|
+
: '';
|
|
581
|
+
lines.push(` ${SEMANTIC_COLORS.error('↳ Failed')}: ${SEMANTIC_COLORS.dim(duration)}${parallelInfo}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
// All passed/running/blocked - just show summary
|
|
586
|
+
const avgDuration = this.calculateAverageDuration(group.jobs);
|
|
587
|
+
// Apply state color to label
|
|
588
|
+
const coloredLabel = state === 'Passed' ? SEMANTIC_COLORS.success(label) :
|
|
589
|
+
state === 'Failed' ? SEMANTIC_COLORS.error(label) :
|
|
590
|
+
state === 'Running' ? SEMANTIC_COLORS.info(label) :
|
|
591
|
+
state === 'Blocked' ? SEMANTIC_COLORS.warning(label) : label;
|
|
592
|
+
lines.push(` ${coloredLabel} ${SEMANTIC_COLORS.dim(`(${total} parallel steps, avg: ${avgDuration})`)}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
// Single job or non-parallel group - display as before
|
|
597
|
+
const job = group.jobs[0];
|
|
598
|
+
const label = this.parseEmoji(job.node.label);
|
|
599
|
+
const duration = this.formatJobDuration(job.node);
|
|
600
|
+
// Apply state color to label
|
|
601
|
+
const coloredLabel = state === 'Passed' ? SEMANTIC_COLORS.success(label) :
|
|
602
|
+
state === 'Failed' ? SEMANTIC_COLORS.error(label) :
|
|
603
|
+
state === 'Running' ? SEMANTIC_COLORS.info(label) :
|
|
604
|
+
state === 'Blocked' ? SEMANTIC_COLORS.warning(label) : label;
|
|
605
|
+
// Add parallel info inline if present
|
|
606
|
+
const parallelInfo = (job.node.parallelGroupIndex !== undefined && job.node.parallelGroupTotal)
|
|
607
|
+
? ` ${SEMANTIC_COLORS.dim(`[Parallel: ${job.node.parallelGroupIndex + 1}/${job.node.parallelGroupTotal}]`)}`
|
|
608
|
+
: '';
|
|
609
|
+
// Basic step line with optional parallel info
|
|
610
|
+
lines.push(` ${coloredLabel} ${SEMANTIC_COLORS.dim(`(${duration})`)}${parallelInfo}`);
|
|
611
|
+
// Show additional details if --jobs or --full and single step
|
|
612
|
+
if ((options?.jobs || options?.full) && !group.isParallelGroup) {
|
|
613
|
+
// Retry info
|
|
614
|
+
if (job.node.retried) {
|
|
615
|
+
lines.push(` ${SEMANTIC_COLORS.warning(`${getProgressIcon('RETRY')} Retried`)}`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
382
618
|
}
|
|
383
619
|
}
|
|
384
620
|
lines.push('');
|
|
385
621
|
}
|
|
386
622
|
return lines.join('\n').trim();
|
|
387
623
|
}
|
|
388
|
-
formatFailedJobsSummary(failedJobs) {
|
|
389
|
-
const lines = [];
|
|
390
|
-
// Group identical jobs by label
|
|
391
|
-
const jobGroups = this.groupJobsByLabel(failedJobs);
|
|
392
|
-
// Show first 10 unique job types
|
|
393
|
-
const displayGroups = jobGroups.slice(0, 10);
|
|
394
|
-
for (const group of displayGroups) {
|
|
395
|
-
const label = this.parseEmoji(group.label);
|
|
396
|
-
if (group.count === 1) {
|
|
397
|
-
const duration = this.formatJobDuration(group.jobs[0].node);
|
|
398
|
-
lines.push(` ${SEMANTIC_COLORS.error('Failed')}: ${label} - ran ${duration}`);
|
|
399
|
-
}
|
|
400
|
-
else {
|
|
401
|
-
// Multiple jobs with same label - show detailed breakdown
|
|
402
|
-
const statusParts = [];
|
|
403
|
-
if (group.stateCounts.failed > 0) {
|
|
404
|
-
statusParts.push(`${group.stateCounts.failed} failed`);
|
|
405
|
-
}
|
|
406
|
-
if (group.stateCounts.broken > 0) {
|
|
407
|
-
statusParts.push(`${group.stateCounts.broken} broken`);
|
|
408
|
-
}
|
|
409
|
-
if (group.stateCounts.notStarted > 0) {
|
|
410
|
-
statusParts.push(`${group.stateCounts.notStarted} not started`);
|
|
411
|
-
}
|
|
412
|
-
if (group.stateCounts.passed > 0) {
|
|
413
|
-
statusParts.push(`${group.stateCounts.passed} passed`);
|
|
414
|
-
}
|
|
415
|
-
if (group.stateCounts.other > 0) {
|
|
416
|
-
statusParts.push(`${group.stateCounts.other} other`);
|
|
417
|
-
}
|
|
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
|
-
const statusInfo = statusParts.join(', ') || 'various states';
|
|
426
|
-
lines.push(` ${SEMANTIC_COLORS.error('Failed')}: ${label} (${SEMANTIC_COLORS.count(String(group.count))} jobs: ${statusInfo})`);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
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`)}`);
|
|
433
|
-
}
|
|
434
|
-
return lines.join('\n');
|
|
435
|
-
}
|
|
436
624
|
groupJobsByLabel(jobs) {
|
|
437
625
|
const groups = new Map();
|
|
438
626
|
for (const job of jobs) {
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
627
|
+
const fullLabel = job.node.label || 'Unnamed job';
|
|
628
|
+
// Strip parallel job index from label for grouping
|
|
629
|
+
// e.g., "deposit_and_filing_schedule_calculator rspec (1/22)" -> "deposit_and_filing_schedule_calculator rspec"
|
|
630
|
+
const baseLabel = fullLabel.replace(/\s*\(\d+\/\d+\)\s*$/, '').trim();
|
|
631
|
+
if (!groups.has(baseLabel)) {
|
|
632
|
+
groups.set(baseLabel, {
|
|
633
|
+
label: baseLabel,
|
|
443
634
|
count: 0,
|
|
444
635
|
jobs: [],
|
|
445
|
-
|
|
636
|
+
parallelTotal: 0,
|
|
446
637
|
stateCounts: {
|
|
447
638
|
failed: 0,
|
|
448
639
|
broken: 0,
|
|
@@ -452,18 +643,41 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
452
643
|
}
|
|
453
644
|
});
|
|
454
645
|
}
|
|
455
|
-
const group = groups.get(
|
|
646
|
+
const group = groups.get(baseLabel);
|
|
456
647
|
group.count++;
|
|
457
648
|
group.jobs.push(job);
|
|
458
|
-
// Track
|
|
459
|
-
if (job.node.
|
|
460
|
-
group.
|
|
649
|
+
// Track the maximum parallel total for this job group
|
|
650
|
+
if (job.node.parallelGroupTotal && job.node.parallelGroupTotal > group.parallelTotal) {
|
|
651
|
+
group.parallelTotal = job.node.parallelGroupTotal;
|
|
461
652
|
}
|
|
462
653
|
// Count by state
|
|
463
654
|
const state = job.node.state?.toUpperCase();
|
|
464
|
-
|
|
655
|
+
// Use exit status as source of truth when available
|
|
656
|
+
// Note: exitStatus comes as a string from Buildkite API
|
|
657
|
+
if (job.node.exitStatus !== null && job.node.exitStatus !== undefined) {
|
|
658
|
+
const exitCode = parseInt(job.node.exitStatus, 10);
|
|
659
|
+
if (exitCode === 0) {
|
|
660
|
+
group.stateCounts.passed++;
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
group.stateCounts.failed++;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
else if (!job.node.startedAt) {
|
|
465
667
|
group.stateCounts.notStarted++;
|
|
466
668
|
}
|
|
669
|
+
else if (state === 'FINISHED' || state === 'COMPLETED') {
|
|
670
|
+
// For finished jobs without exit status, check passed field
|
|
671
|
+
if (job.node.passed === true) {
|
|
672
|
+
group.stateCounts.passed++;
|
|
673
|
+
}
|
|
674
|
+
else if (job.node.passed === false) {
|
|
675
|
+
group.stateCounts.failed++;
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
group.stateCounts.other++;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
467
681
|
else if (state === 'FAILED') {
|
|
468
682
|
group.stateCounts.failed++;
|
|
469
683
|
}
|
|
@@ -479,7 +693,6 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
479
693
|
}
|
|
480
694
|
// Convert to array and sort by count (most failures first)
|
|
481
695
|
return Array.from(groups.values())
|
|
482
|
-
.map(g => ({ ...g, exitCodes: Array.from(g.exitCodes) }))
|
|
483
696
|
.sort((a, b) => b.count - a.count);
|
|
484
697
|
}
|
|
485
698
|
formatDuration(build) {
|
|
@@ -528,37 +741,35 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
528
741
|
}
|
|
529
742
|
}
|
|
530
743
|
getStatusIcon(state) {
|
|
531
|
-
|
|
532
|
-
'PASSED': '✅',
|
|
533
|
-
'FAILED': '❌',
|
|
534
|
-
'RUNNING': '🔄',
|
|
535
|
-
'BLOCKED': '⏸️',
|
|
536
|
-
'CANCELED': '🚫',
|
|
537
|
-
'SCHEDULED': '📅',
|
|
538
|
-
'SKIPPED': '⏭️'
|
|
539
|
-
};
|
|
540
|
-
return icons[state] || '❓';
|
|
744
|
+
return getStateIcon(state);
|
|
541
745
|
}
|
|
542
746
|
getJobStateIcon(state) {
|
|
543
|
-
|
|
544
|
-
'passed': '✅',
|
|
545
|
-
'failed': '❌',
|
|
546
|
-
'running': '🔄',
|
|
547
|
-
'blocked': '⏸️',
|
|
548
|
-
'canceled': '🚫',
|
|
549
|
-
'scheduled': '📅',
|
|
550
|
-
'skipped': '⏭️'
|
|
551
|
-
};
|
|
552
|
-
return icons[state.toLowerCase()] || '❓';
|
|
747
|
+
return getStateIcon(state);
|
|
553
748
|
}
|
|
554
749
|
getAnnotationIcon(style) {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
'
|
|
560
|
-
|
|
561
|
-
|
|
750
|
+
return getAnnotationIcon(style);
|
|
751
|
+
}
|
|
752
|
+
colorizeAnnotationStyle(style) {
|
|
753
|
+
switch (style.toUpperCase()) {
|
|
754
|
+
case 'ERROR':
|
|
755
|
+
return SEMANTIC_COLORS.error(style.toLowerCase());
|
|
756
|
+
case 'WARNING':
|
|
757
|
+
return SEMANTIC_COLORS.warning(style.toLowerCase());
|
|
758
|
+
case 'INFO':
|
|
759
|
+
return SEMANTIC_COLORS.info(style.toLowerCase());
|
|
760
|
+
case 'SUCCESS':
|
|
761
|
+
return SEMANTIC_COLORS.success(style.toLowerCase());
|
|
762
|
+
default:
|
|
763
|
+
return style.toLowerCase();
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
colorizeStatusIcon(icon, state) {
|
|
767
|
+
const upperState = state.toUpperCase();
|
|
768
|
+
const theme = BUILD_STATUS_THEME[upperState];
|
|
769
|
+
if (!theme) {
|
|
770
|
+
return SEMANTIC_COLORS.muted(icon);
|
|
771
|
+
}
|
|
772
|
+
return theme.color(icon);
|
|
562
773
|
}
|
|
563
774
|
getJobStats(jobs) {
|
|
564
775
|
const stats = {
|
|
@@ -568,6 +779,7 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
568
779
|
running: 0,
|
|
569
780
|
blocked: 0,
|
|
570
781
|
skipped: 0,
|
|
782
|
+
canceled: 0,
|
|
571
783
|
queued: 0,
|
|
572
784
|
completed: 0
|
|
573
785
|
};
|
|
@@ -575,13 +787,18 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
575
787
|
return stats;
|
|
576
788
|
for (const job of jobs) {
|
|
577
789
|
const state = job.node.state?.toUpperCase() || '';
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
790
|
+
// If we have an exit status, use that as the source of truth
|
|
791
|
+
// Note: exitStatus comes as a string from Buildkite API
|
|
792
|
+
if (job.node.exitStatus !== null && job.node.exitStatus !== undefined) {
|
|
793
|
+
const exitCode = parseInt(job.node.exitStatus, 10);
|
|
794
|
+
if (exitCode === 0) {
|
|
795
|
+
stats.passed++;
|
|
796
|
+
stats.completed++;
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
stats.failed++;
|
|
800
|
+
stats.completed++;
|
|
801
|
+
}
|
|
585
802
|
}
|
|
586
803
|
else if (state === 'RUNNING') {
|
|
587
804
|
stats.running++;
|
|
@@ -589,13 +806,37 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
589
806
|
else if (state === 'BLOCKED') {
|
|
590
807
|
stats.blocked++;
|
|
591
808
|
}
|
|
592
|
-
else if (state === '
|
|
809
|
+
else if (state === 'CANCELED' || state === 'CANCELLED') {
|
|
810
|
+
stats.canceled++;
|
|
811
|
+
stats.completed++;
|
|
812
|
+
}
|
|
813
|
+
else if (state === 'SKIPPED' || state === 'BROKEN') {
|
|
814
|
+
// BROKEN jobs are functionally skipped - they don't run due to conditions not matching
|
|
593
815
|
stats.skipped++;
|
|
594
816
|
stats.completed++;
|
|
595
817
|
}
|
|
596
818
|
else if (state === 'SCHEDULED' || state === 'ASSIGNED') {
|
|
597
819
|
stats.queued++;
|
|
598
820
|
}
|
|
821
|
+
else if (state === 'FINISHED' || state === 'COMPLETED') {
|
|
822
|
+
// For finished jobs without exit status, check passed field
|
|
823
|
+
if (job.node.passed === true) {
|
|
824
|
+
stats.passed++;
|
|
825
|
+
stats.completed++;
|
|
826
|
+
}
|
|
827
|
+
else if (job.node.passed === false) {
|
|
828
|
+
stats.failed++;
|
|
829
|
+
stats.completed++;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
else if (state === 'PASSED' || job.node.passed === true) {
|
|
833
|
+
stats.passed++;
|
|
834
|
+
stats.completed++;
|
|
835
|
+
}
|
|
836
|
+
else if (state === 'FAILED' || job.node.passed === false) {
|
|
837
|
+
stats.failed++;
|
|
838
|
+
stats.completed++;
|
|
839
|
+
}
|
|
599
840
|
}
|
|
600
841
|
return stats;
|
|
601
842
|
}
|
|
@@ -604,7 +845,22 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
604
845
|
return [];
|
|
605
846
|
return jobs.filter(job => {
|
|
606
847
|
const state = job.node.state?.toUpperCase();
|
|
607
|
-
|
|
848
|
+
// BROKEN jobs are skipped/not run, not failed
|
|
849
|
+
if (state === 'BROKEN' || state === 'SKIPPED') {
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
// If we have an exit status, use that as the source of truth
|
|
853
|
+
// Note: exitStatus comes as a string from Buildkite API
|
|
854
|
+
if (job.node.exitStatus !== null && job.node.exitStatus !== undefined) {
|
|
855
|
+
const exitCode = parseInt(job.node.exitStatus, 10);
|
|
856
|
+
return exitCode !== 0;
|
|
857
|
+
}
|
|
858
|
+
// For FINISHED jobs, check the passed field
|
|
859
|
+
if (state === 'FINISHED') {
|
|
860
|
+
return job.node.passed === false;
|
|
861
|
+
}
|
|
862
|
+
// Otherwise check if explicitly failed
|
|
863
|
+
return state === 'FAILED';
|
|
608
864
|
});
|
|
609
865
|
}
|
|
610
866
|
getRunningJobs(jobs) {
|
|
@@ -622,18 +878,23 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
622
878
|
'Failed': [],
|
|
623
879
|
'Passed': [],
|
|
624
880
|
'Running': [],
|
|
625
|
-
'Blocked': []
|
|
626
|
-
'Skipped'
|
|
881
|
+
'Blocked': []
|
|
882
|
+
// Don't include Skipped - we don't display them
|
|
627
883
|
};
|
|
628
884
|
if (!jobs)
|
|
629
885
|
return grouped;
|
|
630
886
|
for (const job of jobs) {
|
|
631
887
|
const state = job.node.state?.toUpperCase() || '';
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
888
|
+
// If we have an exit status, use that as the source of truth
|
|
889
|
+
// Note: exitStatus comes as a string from Buildkite API
|
|
890
|
+
if (job.node.exitStatus !== null && job.node.exitStatus !== undefined) {
|
|
891
|
+
const exitCode = parseInt(job.node.exitStatus, 10);
|
|
892
|
+
if (exitCode === 0) {
|
|
893
|
+
grouped['Passed'].push(job);
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
grouped['Failed'].push(job);
|
|
897
|
+
}
|
|
637
898
|
}
|
|
638
899
|
else if (state === 'RUNNING') {
|
|
639
900
|
grouped['Running'].push(job);
|
|
@@ -641,12 +902,104 @@ export class PlainTextFormatter extends BaseBuildDetailFormatter {
|
|
|
641
902
|
else if (state === 'BLOCKED') {
|
|
642
903
|
grouped['Blocked'].push(job);
|
|
643
904
|
}
|
|
644
|
-
else if (state === 'SKIPPED' || state === 'CANCELED') {
|
|
645
|
-
|
|
905
|
+
else if (state === 'SKIPPED' || state === 'CANCELED' || state === 'BROKEN') {
|
|
906
|
+
// Don't display skipped/broken/canceled jobs - they're not shown in Buildkite UI
|
|
907
|
+
// Skip these entirely
|
|
908
|
+
}
|
|
909
|
+
else if (state === 'FINISHED' || state === 'COMPLETED') {
|
|
910
|
+
// For finished jobs without exit status, check passed field
|
|
911
|
+
if (job.node.passed === true) {
|
|
912
|
+
grouped['Passed'].push(job);
|
|
913
|
+
}
|
|
914
|
+
else if (job.node.passed === false) {
|
|
915
|
+
grouped['Failed'].push(job);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
else if (state === 'PASSED' || job.node.passed === true) {
|
|
919
|
+
grouped['Passed'].push(job);
|
|
920
|
+
}
|
|
921
|
+
else if (state === 'FAILED') {
|
|
922
|
+
grouped['Failed'].push(job);
|
|
646
923
|
}
|
|
647
924
|
}
|
|
648
925
|
return grouped;
|
|
649
926
|
}
|
|
927
|
+
collapseParallelJobs(jobs) {
|
|
928
|
+
const groups = new Map();
|
|
929
|
+
// Group jobs by label
|
|
930
|
+
for (const job of jobs) {
|
|
931
|
+
const label = job.node.label || 'Unnamed';
|
|
932
|
+
if (!groups.has(label)) {
|
|
933
|
+
groups.set(label, []);
|
|
934
|
+
}
|
|
935
|
+
groups.get(label).push(job);
|
|
936
|
+
}
|
|
937
|
+
// Convert to array and determine if each group is a parallel group
|
|
938
|
+
const result = [];
|
|
939
|
+
for (const [label, groupJobs] of groups.entries()) {
|
|
940
|
+
// Check if this is a parallel group (multiple jobs with same label and parallel info)
|
|
941
|
+
const hasParallelInfo = groupJobs.some(j => j.node.parallelGroupIndex !== undefined && j.node.parallelGroupTotal !== undefined);
|
|
942
|
+
const isParallelGroup = hasParallelInfo && groupJobs.length > 1;
|
|
943
|
+
// Get the total from the first job if available
|
|
944
|
+
const parallelTotal = groupJobs[0]?.node?.parallelGroupTotal;
|
|
945
|
+
result.push({
|
|
946
|
+
label,
|
|
947
|
+
jobs: groupJobs,
|
|
948
|
+
isParallelGroup,
|
|
949
|
+
parallelTotal
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
return result;
|
|
953
|
+
}
|
|
954
|
+
isJobPassed(job) {
|
|
955
|
+
const state = job.state?.toUpperCase();
|
|
956
|
+
if (job.exitStatus !== null && job.exitStatus !== undefined) {
|
|
957
|
+
return parseInt(job.exitStatus, 10) === 0;
|
|
958
|
+
}
|
|
959
|
+
if (state === 'PASSED')
|
|
960
|
+
return true;
|
|
961
|
+
if (state === 'FINISHED' || state === 'COMPLETED') {
|
|
962
|
+
return job.passed === true;
|
|
963
|
+
}
|
|
964
|
+
return job.passed === true;
|
|
965
|
+
}
|
|
966
|
+
isJobFailed(job) {
|
|
967
|
+
const state = job.state?.toUpperCase();
|
|
968
|
+
if (job.exitStatus !== null && job.exitStatus !== undefined) {
|
|
969
|
+
return parseInt(job.exitStatus, 10) !== 0;
|
|
970
|
+
}
|
|
971
|
+
if (state === 'FAILED')
|
|
972
|
+
return true;
|
|
973
|
+
if (state === 'FINISHED' || state === 'COMPLETED') {
|
|
974
|
+
return job.passed === false;
|
|
975
|
+
}
|
|
976
|
+
return false;
|
|
977
|
+
}
|
|
978
|
+
calculateAverageDuration(jobs) {
|
|
979
|
+
const durationsMs = jobs
|
|
980
|
+
.filter(j => j.node.startedAt && j.node.finishedAt)
|
|
981
|
+
.map(j => {
|
|
982
|
+
const start = new Date(j.node.startedAt).getTime();
|
|
983
|
+
const end = new Date(j.node.finishedAt).getTime();
|
|
984
|
+
return end - start;
|
|
985
|
+
});
|
|
986
|
+
if (durationsMs.length === 0) {
|
|
987
|
+
return 'unknown';
|
|
988
|
+
}
|
|
989
|
+
const avgMs = durationsMs.reduce((a, b) => a + b, 0) / durationsMs.length;
|
|
990
|
+
const avgSeconds = Math.floor(avgMs / 1000);
|
|
991
|
+
if (avgSeconds < 60) {
|
|
992
|
+
return `${avgSeconds}s`;
|
|
993
|
+
}
|
|
994
|
+
const minutes = Math.floor(avgSeconds / 60);
|
|
995
|
+
const seconds = avgSeconds % 60;
|
|
996
|
+
if (minutes < 60) {
|
|
997
|
+
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
998
|
+
}
|
|
999
|
+
const hours = Math.floor(minutes / 60);
|
|
1000
|
+
const remainingMinutes = minutes % 60;
|
|
1001
|
+
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
|
1002
|
+
}
|
|
650
1003
|
countAnnotationsByStyle(annotations) {
|
|
651
1004
|
const counts = {
|
|
652
1005
|
ERROR: 0,
|