@shipfox/client-workflows 17.0.0 → 19.0.0

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.
Files changed (59) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-type.log +0 -1
  3. package/CHANGELOG.md +23 -0
  4. package/dist/components/job-detail/job-context-panel.d.ts.map +1 -1
  5. package/dist/components/job-detail/job-context-panel.js +5 -1
  6. package/dist/components/job-detail/job-context-panel.js.map +1 -1
  7. package/dist/components/job-detail/job-detail-view.d.ts.map +1 -1
  8. package/dist/components/job-detail/job-detail-view.js +34 -27
  9. package/dist/components/job-detail/job-detail-view.js.map +1 -1
  10. package/dist/components/job-detail/job-empty-states.d.ts +4 -0
  11. package/dist/components/job-detail/job-empty-states.d.ts.map +1 -1
  12. package/dist/components/job-detail/job-empty-states.js +43 -3
  13. package/dist/components/job-detail/job-empty-states.js.map +1 -1
  14. package/dist/components/job-detail/step-troubleshooting.js +4 -0
  15. package/dist/components/job-detail/step-troubleshooting.js.map +1 -1
  16. package/dist/components/step-list/step-list-model.d.ts.map +1 -1
  17. package/dist/components/step-list/step-list-model.js +1 -0
  18. package/dist/components/step-list/step-list-model.js.map +1 -1
  19. package/dist/components/workflow-run-list/job-status-strip.d.ts.map +1 -1
  20. package/dist/components/workflow-run-list/job-status-strip.js +20 -7
  21. package/dist/components/workflow-run-list/job-status-strip.js.map +1 -1
  22. package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -1
  23. package/dist/components/workflow-run-view/run-workspace-nav.js +6 -5
  24. package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
  25. package/dist/core/entities/job-execution.d.ts +2 -0
  26. package/dist/core/entities/job-execution.d.ts.map +1 -1
  27. package/dist/core/entities/job-execution.js.map +1 -1
  28. package/dist/core/entities/job.d.ts +6 -3
  29. package/dist/core/entities/job.d.ts.map +1 -1
  30. package/dist/core/entities/job.js +8 -1
  31. package/dist/core/entities/job.js.map +1 -1
  32. package/dist/core/entities/workflow-run.d.ts +7 -4
  33. package/dist/core/entities/workflow-run.d.ts.map +1 -1
  34. package/dist/core/entities/workflow-run.js.map +1 -1
  35. package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
  36. package/dist/hooks/api/workflow-run-mapper.js +12 -1
  37. package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
  38. package/dist/tsconfig.test.tsbuildinfo +1 -1
  39. package/package.json +2 -2
  40. package/src/components/job-detail/job-context-panel.tsx +4 -0
  41. package/src/components/job-detail/job-detail-view.tsx +37 -33
  42. package/src/components/job-detail/job-empty-states.test.ts +105 -1
  43. package/src/components/job-detail/job-empty-states.tsx +56 -2
  44. package/src/components/job-detail/step-troubleshooting.tsx +4 -0
  45. package/src/components/job-graph/job-node.test.tsx +2 -2
  46. package/src/components/step-list/step-list-model.ts +1 -0
  47. package/src/components/workflow-run-list/job-status-strip.tsx +25 -12
  48. package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +73 -0
  49. package/src/components/workflow-run-list/workflow-run-list.stories.tsx +31 -0
  50. package/src/components/workflow-run-view/run-workspace-nav.tsx +13 -6
  51. package/src/core/entities/job-execution.ts +2 -0
  52. package/src/core/entities/job-status.test.ts +47 -1
  53. package/src/core/entities/job.ts +18 -4
  54. package/src/core/entities/workflow-run-display.test.ts +28 -0
  55. package/src/core/entities/workflow-run.ts +18 -4
  56. package/src/core/workflow-run.test.ts +13 -0
  57. package/src/hooks/api/workflow-run-mapper.ts +19 -1
  58. package/test/fixtures/workflow-run.ts +32 -12
  59. package/tsconfig.build.tsbuildinfo +1 -1
@@ -29,6 +29,7 @@ interface JobExecutionFields {
29
29
  name: string;
30
30
  status: JobExecutionStatus;
31
31
  statusReason: string | null;
32
+ statusReasonMessage: string | null;
32
33
  runner: string[] | null;
33
34
  outputs: Record<string, unknown> | null;
34
35
  triggerEvents: WorkflowExecutionEvent[];
@@ -49,6 +50,7 @@ export class JobExecution {
49
50
  name!: string;
50
51
  status!: JobExecutionStatus;
51
52
  statusReason!: string | null;
53
+ statusReasonMessage!: string | null;
52
54
  runner!: string[] | null;
53
55
  outputs!: Record<string, unknown> | null;
54
56
  triggerEvents!: WorkflowExecutionEvent[];
@@ -42,7 +42,7 @@ describe('deriveJobDisplayStatus', () => {
42
42
  const statuses: Array<[JobExecutionStatus, string[], JobDisplayStatus]> = [
43
43
  ['pending', [], 'pending'],
44
44
  ['pending', ['running'], 'running'],
45
- ['running', [], 'pending'],
45
+ ['running', [], 'running'],
46
46
  ['running', ['running'], 'running'],
47
47
  ];
48
48
 
@@ -91,6 +91,52 @@ describe('deriveJobDisplayStatus', () => {
91
91
  ).toBe('listening');
92
92
  });
93
93
 
94
+ test.each([
95
+ ['pending', 'pending'],
96
+ ['failed', 'failed'],
97
+ [null, 'pending'],
98
+ ] as const)('uses list execution evidence for %s', (executionStatus, expected) => {
99
+ expect(
100
+ deriveJobDisplayStatus({
101
+ mode: 'one_shot',
102
+ status: 'pending',
103
+ listenerStatus: 'inactive',
104
+ executionStatus,
105
+ jobExecutions: [],
106
+ }),
107
+ ).toBe(expected);
108
+ });
109
+
110
+ test('does not infer execution from a running verdict without execution evidence', () => {
111
+ expect(
112
+ deriveJobDisplayStatus({
113
+ mode: 'one_shot',
114
+ status: 'running',
115
+ listenerStatus: 'inactive',
116
+ executionStatus: null,
117
+ jobExecutions: [],
118
+ }),
119
+ ).toBe('pending');
120
+ });
121
+
122
+ test('uses the same running execution evidence with and without a step tree', () => {
123
+ const detailStatus = deriveJobDisplayStatus({
124
+ mode: 'one_shot',
125
+ status: 'pending',
126
+ listenerStatus: 'inactive',
127
+ jobExecutions: [{status: 'running', steps: [], sequence: 1}] as never,
128
+ });
129
+ const listStatus = deriveJobDisplayStatus({
130
+ mode: 'one_shot',
131
+ status: 'pending',
132
+ listenerStatus: 'inactive',
133
+ executionStatus: 'running',
134
+ jobExecutions: [],
135
+ });
136
+
137
+ expect(listStatus).toBe(detailStatus);
138
+ });
139
+
94
140
  test('uses the terminal job status when an active listener has resolved', () => {
95
141
  expect(
96
142
  deriveJobDisplayStatus({
@@ -2,6 +2,7 @@ import {
2
2
  deriveJobExecutionDisplayStatus,
3
3
  type JobExecution,
4
4
  type JobExecutionDisplayDuration,
5
+ type JobExecutionStatus,
5
6
  } from './job-execution.js';
6
7
  import type {EvaluationTraceEntry} from './step-attempt.js';
7
8
 
@@ -20,8 +21,10 @@ export type JobStatusReason =
20
21
  | 'run_cancelled'
21
22
  | 'timed_out'
22
23
  | 'runner_lost'
24
+ | 'output_too_large'
23
25
  | 'step_failed'
24
- | 'unknown';
26
+ | 'unknown'
27
+ | 'output_invalid';
25
28
  export interface JobListening {
26
29
  on: Array<{
27
30
  source: string;
@@ -138,12 +141,23 @@ export function isTerminalJobStatus(status: JobStatus): boolean {
138
141
  }
139
142
 
140
143
  export function deriveJobDisplayStatus(
141
- job: Pick<Job, 'mode' | 'status' | 'listenerStatus' | 'jobExecutions'>,
144
+ job: Pick<Job, 'mode' | 'status' | 'listenerStatus' | 'jobExecutions'> & {
145
+ /** List previews carry the selected execution state without its step tree. */
146
+ executionStatus?: JobExecutionStatus | null | undefined;
147
+ },
142
148
  ): JobDisplayStatus {
143
149
  if (isTerminalJobStatus(job.status)) return job.status;
144
150
  if (job.mode === 'listening' && job.listenerStatus === 'listening') return 'listening';
145
-
146
- const execution = defaultJobExecution(job);
151
+ const execution =
152
+ job.executionStatus === undefined
153
+ ? defaultJobExecution(job)
154
+ : job.executionStatus === null
155
+ ? undefined
156
+ : {status: job.executionStatus, steps: []};
157
+ // A running execution is the shared display rule for both list previews and detail jobs.
158
+ // The list has no step tree, while the detail path can still use the execution's steps for
159
+ // other non-running states.
160
+ if (execution?.status === 'running') return 'running';
147
161
  return execution ? deriveJobExecutionDisplayStatus(execution) : 'pending';
148
162
  }
149
163
 
@@ -100,6 +100,34 @@ describe('workflowRunListItemDisplay', () => {
100
100
  expect(display.status).toBe('running');
101
101
  expect(display.duration).toMatchObject({kind: 'run'});
102
102
  });
103
+
104
+ test('uses execution-derived running counts for the row headline', () => {
105
+ const run = workflowRunListItem({
106
+ status: 'running',
107
+ ...workflowRunJobsFixture(['pending']),
108
+ job_display_status_counts: [{status: 'running', count: 1}],
109
+ started_at: ATTEMPT_STARTED_AT,
110
+ });
111
+
112
+ const display = workflowRunListItemDisplay(run);
113
+
114
+ expect(display.status).toBe('running');
115
+ expect(display.duration).toMatchObject({kind: 'run'});
116
+ });
117
+
118
+ test('uses execution-derived listening counts as started work', () => {
119
+ const run = workflowRunListItem({
120
+ status: 'running',
121
+ ...workflowRunJobsFixture(['pending']),
122
+ job_display_status_counts: [{status: 'listening', count: 1}],
123
+ started_at: ATTEMPT_STARTED_AT,
124
+ });
125
+
126
+ const display = workflowRunListItemDisplay(run);
127
+
128
+ expect(display.status).toBe('running');
129
+ expect(display.duration).toMatchObject({kind: 'run'});
130
+ });
103
131
  });
104
132
 
105
133
  describe('workflowRunBlockingJob', () => {
@@ -1,5 +1,16 @@
1
- import {type Job, type JobStatus, WORKFLOW_JOB_STATUSES} from './job.js';
2
- import {elapsedTimeFromTimestamps, type JobExecutionDisplayDuration} from './job-execution.js';
1
+ import {
2
+ type Job,
3
+ type JobDisplayStatus,
4
+ type JobMode,
5
+ type JobStatus,
6
+ type ListenerStatus,
7
+ WORKFLOW_JOB_STATUSES,
8
+ } from './job.js';
9
+ import {
10
+ elapsedTimeFromTimestamps,
11
+ type JobExecutionDisplayDuration,
12
+ type JobExecutionStatus,
13
+ } from './job-execution.js';
3
14
  import type {WorkflowRunAttempt, WorkflowRunAttemptSummary} from './workflow-run-attempt.js';
4
15
 
5
16
  export type WorkflowRunStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'cancelled';
@@ -60,11 +71,14 @@ export interface WorkflowRunJobSummary {
60
71
  key: string;
61
72
  name: string | null;
62
73
  status: JobStatus;
74
+ mode: JobMode;
75
+ listenerStatus: ListenerStatus;
76
+ executionStatus: JobExecutionStatus | null;
63
77
  position: number;
64
78
  }
65
79
 
66
80
  export interface WorkflowRunJobStatusCount {
67
- status: JobStatus;
81
+ status: JobDisplayStatus;
68
82
  count: number;
69
83
  }
70
84
 
@@ -209,7 +223,7 @@ export interface WorkflowRunProgress {
209
223
  runStatus: WorkflowRunStatus;
210
224
  startedAt: string | null;
211
225
  finishedAt: string | null;
212
- jobStatuses: JobStatus[];
226
+ jobStatuses: JobDisplayStatus[];
213
227
  firstStartedAt?: string | null | undefined;
214
228
  }
215
229
 
@@ -12,6 +12,7 @@ import {
12
12
  workflowRunAttemptDto,
13
13
  workflowRunDetailDto,
14
14
  workflowRunDto,
15
+ workflowRunJobSummaryDto,
15
16
  workflowRunListResponseDto,
16
17
  workflowStepAttemptDto,
17
18
  workflowStepDto,
@@ -127,6 +128,18 @@ describe('workflow run model mapping', () => {
127
128
  });
128
129
  });
129
130
 
131
+ test('keeps legacy raw-status counts aligned with preview glyphs', () => {
132
+ const {job_display_status_counts: _displayCounts, ...legacyDto} = workflowRunDto({
133
+ jobs: [workflowRunJobSummaryDto({status: 'running', execution_status: null})],
134
+ job_status_counts: [{status: 'running', count: 1}],
135
+ });
136
+
137
+ const run = toWorkflowRunListItem(legacyDto);
138
+
139
+ expect(run.jobs.preview[0]?.executionStatus).toBe('running');
140
+ expect(run.jobs.statusCounts).toEqual([{status: 'running', count: 1}]);
141
+ });
142
+
130
143
  test('maps run list pagination fields', () => {
131
144
  const dto = workflowRunListResponseDto({
132
145
  runs: [
@@ -96,7 +96,10 @@ export function toWorkflowRunRecord(dto: WorkflowRunResponseDto): WorkflowRunRec
96
96
  }
97
97
 
98
98
  export function toWorkflowRunListItem(dto: WorkflowRunListItemDto): WorkflowRunListItem {
99
- const statusCounts = dto.job_status_counts.map(({status, count}) => ({status, count}));
99
+ const hasDisplayStatusCounts = dto.job_display_status_counts !== undefined;
100
+ const statusCounts = (dto.job_display_status_counts ?? dto.job_status_counts).map(
101
+ ({status, count}) => ({status, count}),
102
+ );
100
103
  return {
101
104
  ...toWorkflowRunRecord(dto),
102
105
  jobs: {
@@ -105,6 +108,14 @@ export function toWorkflowRunListItem(dto: WorkflowRunListItemDto): WorkflowRunL
105
108
  key: job.key,
106
109
  name: job.name,
107
110
  status: job.status,
111
+ mode: job.mode ?? 'one_shot',
112
+ listenerStatus: job.listener_status ?? 'inactive',
113
+ // The optional display-count field is the rollout capability signal. Pre-display API
114
+ // responses only carry raw verdict counts, so mirror their non-terminal verdict into
115
+ // execution evidence to keep each legacy glyph aligned with those fallback counts.
116
+ executionStatus: hasDisplayStatusCounts
117
+ ? (job.execution_status ?? null)
118
+ : legacyExecutionStatus(job.status),
108
119
  position: job.position,
109
120
  })),
110
121
  statusCounts,
@@ -115,6 +126,12 @@ export function toWorkflowRunListItem(dto: WorkflowRunListItemDto): WorkflowRunL
115
126
  };
116
127
  }
117
128
 
129
+ function legacyExecutionStatus(
130
+ status: WorkflowRunListItemDto['jobs'][number]['status'],
131
+ ): 'pending' | 'running' | null {
132
+ return status === 'pending' || status === 'running' ? status : null;
133
+ }
134
+
118
135
  export function toWorkflowRunListPage(dto: WorkflowRunListResponseDto): WorkflowRunListPage {
119
136
  return {
120
137
  runs: dto.runs.map(toWorkflowRunListItem),
@@ -165,6 +182,7 @@ export function toJobExecution(dto: WorkflowRunJobExecutionDetailDto): JobExecut
165
182
  name: dto.name,
166
183
  status: dto.status,
167
184
  statusReason: dto.status_reason,
185
+ statusReasonMessage: dto.status_reason_message ?? null,
168
186
  runner: dto.runner ?? null,
169
187
  outputs: dto.outputs ?? null,
170
188
  triggerEvents: (dto.trigger_events ?? []).map(toWorkflowExecutionEvent),
@@ -81,6 +81,7 @@ export function workflowRunDto(
81
81
  finished_at: null,
82
82
  jobs: [],
83
83
  job_status_counts: [],
84
+ job_display_status_counts: [],
84
85
  ...overrides,
85
86
  };
86
87
  }
@@ -89,11 +90,15 @@ export function workflowRunJobSummaryDto(
89
90
  overrides: Partial<WorkflowRunJobSummaryDto> = {},
90
91
  ): WorkflowRunJobSummaryDto {
91
92
  jobSummarySequence += 1;
93
+ const status = overrides.status ?? 'succeeded';
92
94
  return {
93
95
  id: `${JOB_SUMMARY_ID_PREFIX}${String(jobSummarySequence).padStart(11, '0')}`,
94
96
  key: `job-${jobSummarySequence}`,
95
97
  name: null,
96
- status: 'succeeded',
98
+ status,
99
+ mode: 'one_shot',
100
+ listener_status: 'inactive',
101
+ execution_status: executionStatusForFixtureStatus(status),
97
102
  position: jobSummarySequence - 1,
98
103
  ...overrides,
99
104
  };
@@ -104,13 +109,25 @@ export function workflowRunJobSummaryDtos(
104
109
  count: number,
105
110
  statuses: readonly JobStatusDto[] = [],
106
111
  ): WorkflowRunJobSummaryDto[] {
107
- return Array.from({length: count}, (_, index) =>
108
- workflowRunJobSummaryDto({
112
+ return Array.from({length: count}, (_, index) => {
113
+ const status = statuses[index];
114
+ return workflowRunJobSummaryDto({
109
115
  key: `job-${index + 1}`,
110
116
  position: index,
111
- ...(statuses[index] ? {status: statuses[index]} : {}),
112
- }),
113
- );
117
+ ...(status
118
+ ? {
119
+ status,
120
+ execution_status: executionStatusForFixtureStatus(status),
121
+ }
122
+ : {}),
123
+ });
124
+ });
125
+ }
126
+
127
+ function executionStatusForFixtureStatus(
128
+ status: JobStatusDto,
129
+ ): WorkflowRunJobSummaryDto['execution_status'] {
130
+ return status === 'pending' || status === 'running' ? status : null;
114
131
  }
115
132
 
116
133
  /**
@@ -122,16 +139,19 @@ export function workflowRunJobSummaryDtos(
122
139
  */
123
140
  export function workflowRunJobsFixture(
124
141
  statuses: readonly JobStatusDto[],
125
- ): Pick<WorkflowRunListItemDto, 'jobs' | 'job_status_counts'> {
142
+ ): Pick<WorkflowRunListItemDto, 'jobs' | 'job_status_counts' | 'job_display_status_counts'> {
126
143
  const counts = new Map<JobStatusDto, number>();
127
144
  for (const status of statuses) counts.set(status, (counts.get(status) ?? 0) + 1);
128
145
 
146
+ const preview = workflowRunJobSummaryDtos(
147
+ Math.min(statuses.length, WORKFLOW_RUN_JOB_PREVIEW_LIMIT),
148
+ statuses,
149
+ );
150
+
129
151
  return {
130
- jobs: workflowRunJobSummaryDtos(
131
- Math.min(statuses.length, WORKFLOW_RUN_JOB_PREVIEW_LIMIT),
132
- statuses,
133
- ),
152
+ jobs: preview,
134
153
  job_status_counts: [...counts.entries()].map(([status, count]) => ({status, count})),
154
+ job_display_status_counts: [...counts.entries()].map(([status, count]) => ({status, count})),
135
155
  };
136
156
  }
137
157
 
@@ -139,7 +159,7 @@ export function workflowRunJobsFixture(
139
159
  export function workflowRunJobsOfStatus(
140
160
  count: number,
141
161
  status: JobStatusDto = 'succeeded',
142
- ): Pick<WorkflowRunListItemDto, 'jobs' | 'job_status_counts'> {
162
+ ): Pick<WorkflowRunListItemDto, 'jobs' | 'job_status_counts' | 'job_display_status_counts'> {
143
163
  return workflowRunJobsFixture(Array.from({length: count}, () => status));
144
164
  }
145
165