@shipfox/api-workflows-dto 13.1.0 → 14.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/api-workflows-dto",
3
3
  "license": "MIT",
4
- "version": "13.1.0",
4
+ "version": "14.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "zod": "^4.4.3",
26
- "@shipfox/api-agent-dto": "13.1.0",
26
+ "@shipfox/api-agent-dto": "14.0.0",
27
27
  "@shipfox/inter-module": "0.2.3"
28
28
  },
29
29
  "imports": {
package/src/index.ts CHANGED
@@ -78,11 +78,13 @@ export {
78
78
  type WorkflowRunAttemptDto,
79
79
  type WorkflowRunAttemptsResponseDto,
80
80
  type WorkflowRunDetailResponseDto,
81
+ type WorkflowRunDevSourceDto,
81
82
  type WorkflowRunDto,
82
83
  type WorkflowRunJobDetailDto,
83
84
  type WorkflowRunJobExecutionDetailDto,
84
85
  type WorkflowRunListQueryDto,
85
86
  type WorkflowRunListResponseDto,
87
+ type WorkflowRunOriginDto,
86
88
  type WorkflowRunRerunModeDto,
87
89
  type WorkflowRunResponseDto,
88
90
  type WorkflowRunStatusDto,
@@ -94,6 +96,7 @@ export {
94
96
  workflowRunAttemptDtoSchema,
95
97
  workflowRunAttemptsResponseSchema,
96
98
  workflowRunDetailResponseSchema,
99
+ workflowRunDevSourceSchema,
97
100
  workflowRunDtoSchema,
98
101
  workflowRunJobDetailDtoSchema,
99
102
  workflowRunJobDisplayStatusCountDtoSchema,
@@ -103,6 +106,7 @@ export {
103
106
  workflowRunListItemSchema,
104
107
  workflowRunListQuerySchema,
105
108
  workflowRunListResponseSchema,
109
+ workflowRunOriginSchema,
106
110
  workflowRunRerunModeSchema,
107
111
  workflowRunResponseSchema,
108
112
  workflowRunStatusSchema,
@@ -89,6 +89,7 @@ export {
89
89
  type WorkflowRunAggregatesResponseDto,
90
90
  type WorkflowRunAttemptDto,
91
91
  type WorkflowRunAttemptsResponseDto,
92
+ type WorkflowRunDevSourceDto,
92
93
  type WorkflowRunDto,
93
94
  type WorkflowRunJobDisplayStatusCountDto,
94
95
  type WorkflowRunJobStatusCountDto,
@@ -96,6 +97,7 @@ export {
96
97
  type WorkflowRunListItemDto,
97
98
  type WorkflowRunListQueryDto,
98
99
  type WorkflowRunListResponseDto,
100
+ type WorkflowRunOriginDto,
99
101
  type WorkflowRunRerunModeDto,
100
102
  type WorkflowRunResponseDto,
101
103
  type WorkflowRunStatusDto,
@@ -105,6 +107,7 @@ export {
105
107
  workflowRunAggregatesResponseSchema,
106
108
  workflowRunAttemptDtoSchema,
107
109
  workflowRunAttemptsResponseSchema,
110
+ workflowRunDevSourceSchema,
108
111
  workflowRunDtoSchema,
109
112
  workflowRunJobDisplayStatusCountDtoSchema,
110
113
  workflowRunJobStatusCountDtoSchema,
@@ -112,6 +115,7 @@ export {
112
115
  workflowRunListItemSchema,
113
116
  workflowRunListQuerySchema,
114
117
  workflowRunListResponseSchema,
118
+ workflowRunOriginSchema,
115
119
  workflowRunRerunModeSchema,
116
120
  workflowRunResponseSchema,
117
121
  workflowRunStatusSchema,
@@ -22,6 +22,27 @@ describe('jobListeningSchema', () => {
22
22
 
23
23
  expect(result).toEqual(input);
24
24
  });
25
+
26
+ it('parses listener matchers with the event omitted', () => {
27
+ const input = {
28
+ on: [{source: 'github_acme'}],
29
+ until: [{source: 'github_acme', filter: 'event.action == "closed"'}],
30
+ timeout_ms: 1000,
31
+ max_executions: 3,
32
+ batch: null,
33
+ on_resolve: 'finish',
34
+ execution_timeout_ms: null,
35
+ name: null,
36
+ };
37
+
38
+ const result = jobListeningSchema.parse(input);
39
+
40
+ expect(result.on).toEqual([{source: 'github_acme'}]);
41
+ expect(result.until?.[0]).toEqual({
42
+ source: 'github_acme',
43
+ filter: 'event.action == "closed"',
44
+ });
45
+ });
25
46
  });
26
47
 
27
48
  describe('execution context schemas', () => {
@@ -6,7 +6,7 @@ export const resolutionReasonSchema = z.enum(['until', 'timeout', 'max_execution
6
6
 
7
7
  export const listeningTriggerSchema = z.object({
8
8
  source: z.string(),
9
- event: z.string(),
9
+ event: z.string().optional(),
10
10
  inputs: z.record(z.string(), z.unknown()).optional(),
11
11
  filter: z.string().optional(),
12
12
  });
@@ -5,8 +5,9 @@ import {workflowExecutionEventSchema} from './job-listening.js';
5
5
  import {stepAttemptDetailDtoSchema, stepAttemptDtoSchema, stepDtoSchema} from './step.js';
6
6
  import {
7
7
  jobExecutionStatusSchema,
8
+ validateWorkflowRunOrigin,
8
9
  workflowRunAttemptDtoSchema,
9
- workflowRunResponseSchema,
10
+ workflowRunDtoFields,
10
11
  } from './workflow-run.js';
11
12
 
12
13
  export const jobExecutionDtoSchema = z.object({
@@ -69,17 +70,20 @@ export type WorkflowRunJobDetailDto = z.infer<typeof workflowRunJobDetailDtoSche
69
70
 
70
71
  // The run detail read model returned by `GET /workflows/runs/:id`: a run plus its
71
72
  // jobs, each job's steps, and each step's attempt history.
72
- export const workflowRunDetailResponseSchema = workflowRunResponseSchema.extend({
73
- run_attempt: workflowRunAttemptDtoSchema,
74
- jobs: z.array(workflowRunJobDetailDtoSchema),
75
- /**
76
- * Whether any job execution of this attempt reached its runner. Redundant with the executions
77
- * below, and deliberately so: the server decides it once for both this response and the run
78
- * list, which is what keeps the two surfaces from reaching different answers.
79
- *
80
- * Defaults to started for the same rollout reason as the list item's copy.
81
- */
82
- has_started_job_execution: z.boolean().optional().default(true),
83
- });
73
+ export const workflowRunDetailResponseSchema = z
74
+ .object({
75
+ ...workflowRunDtoFields,
76
+ run_attempt: workflowRunAttemptDtoSchema,
77
+ jobs: z.array(workflowRunJobDetailDtoSchema),
78
+ /**
79
+ * Whether any job execution of this attempt reached its runner. Redundant with the executions
80
+ * below, and deliberately so: the server decides it once for both this response and the run
81
+ * list, which is what keeps the two surfaces from reaching different answers.
82
+ *
83
+ * Defaults to started for the same rollout reason as the list item's copy.
84
+ */
85
+ has_started_job_execution: z.boolean().optional().default(true),
86
+ })
87
+ .superRefine(validateWorkflowRunOrigin);
84
88
 
85
89
  export type WorkflowRunDetailResponseDto = z.infer<typeof workflowRunDetailResponseSchema>;
@@ -2,6 +2,7 @@ import {
2
2
  WORKFLOW_RUN_JOB_PREVIEW_LIMIT,
3
3
  workflowRunDtoSchema,
4
4
  workflowRunListItemSchema,
5
+ workflowRunListQuerySchema,
5
6
  workflowSourceSnapshotSchema,
6
7
  } from './workflow-run.js';
7
8
 
@@ -13,6 +14,8 @@ const baseRun = {
13
14
  name: 'Build',
14
15
  workflow_name: 'Build',
15
16
  status: 'pending',
17
+ origin: 'synced',
18
+ dev_source: null,
16
19
  source_run_id: null,
17
20
  root_run_id: null,
18
21
  attempt: 1,
@@ -31,6 +34,8 @@ const baseRun = {
31
34
  finished_at: null,
32
35
  };
33
36
 
37
+ const {origin: _origin, dev_source: _devSource, ...legacyRun} = baseRun;
38
+
34
39
  describe('workflow source snapshot schemas', () => {
35
40
  test('accepts YAML source snapshots', () => {
36
41
  const result = workflowSourceSnapshotSchema.parse({
@@ -100,6 +105,152 @@ describe('workflow run trigger reference schema', () => {
100
105
  });
101
106
  });
102
107
 
108
+ describe('workflow run origin schemas', () => {
109
+ test('defaults missing origin fields for a legacy response', () => {
110
+ const result = workflowRunDtoSchema.parse({...legacyRun, source_snapshot: null});
111
+
112
+ expect(result.origin).toBe('synced');
113
+ expect(result.dev_source).toBeNull();
114
+ });
115
+
116
+ test('accepts a dev run with its provenance', () => {
117
+ const result = workflowRunDtoSchema.parse({
118
+ ...baseRun,
119
+ source_snapshot: null,
120
+ origin: 'dev',
121
+ dev_source: {
122
+ ref: 'fix-triage-prompt',
123
+ commit: 'abc123',
124
+ config_path: '.shipfox/workflows/triage-sentry.yml',
125
+ initiated_by_user_id: '55555555-5555-4555-8555-555555555555',
126
+ replay_of_event_id: '66666666-6666-4666-8666-666666666666',
127
+ },
128
+ });
129
+
130
+ expect(result.origin).toBe('dev');
131
+ expect(result.dev_source).toMatchObject({
132
+ ref: 'fix-triage-prompt',
133
+ replay_of_event_id: '66666666-6666-4666-8666-666666666666',
134
+ });
135
+ });
136
+
137
+ test('accepts a dev run without a replayed event', () => {
138
+ const result = workflowRunDtoSchema.parse({
139
+ ...baseRun,
140
+ source_snapshot: null,
141
+ origin: 'dev',
142
+ dev_source: {
143
+ ref: 'fix-triage-prompt',
144
+ commit: 'abc123',
145
+ config_path: '.shipfox/workflows/triage-sentry.yml',
146
+ initiated_by_user_id: '55555555-5555-4555-8555-555555555555',
147
+ replay_of_event_id: null,
148
+ },
149
+ });
150
+
151
+ expect(result.dev_source?.replay_of_event_id).toBeNull();
152
+ });
153
+
154
+ test('rejects synced runs with dev provenance', () => {
155
+ const result = workflowRunDtoSchema.safeParse({
156
+ ...baseRun,
157
+ source_snapshot: null,
158
+ dev_source: {
159
+ ref: 'fix-triage-prompt',
160
+ commit: 'abc123',
161
+ config_path: '.shipfox/workflows/triage-sentry.yml',
162
+ initiated_by_user_id: '55555555-5555-4555-8555-555555555555',
163
+ replay_of_event_id: null,
164
+ },
165
+ });
166
+
167
+ expect(result.success).toBe(false);
168
+ });
169
+
170
+ test('rejects dev runs without provenance', () => {
171
+ const result = workflowRunDtoSchema.safeParse({
172
+ ...baseRun,
173
+ source_snapshot: null,
174
+ origin: 'dev',
175
+ dev_source: null,
176
+ });
177
+
178
+ expect(result.success).toBe(false);
179
+ });
180
+
181
+ test('rejects an unknown origin value', () => {
182
+ const result = workflowRunDtoSchema.safeParse({...baseRun, origin: 'staging'});
183
+
184
+ expect(result.success).toBe(false);
185
+ });
186
+
187
+ test('rejects a dev source missing a field rather than defaulting it', () => {
188
+ const result = workflowRunDtoSchema.safeParse({
189
+ ...baseRun,
190
+ source_snapshot: null,
191
+ origin: 'dev',
192
+ dev_source: {
193
+ ref: 'fix-triage-prompt',
194
+ commit: 'abc123',
195
+ config_path: '.shipfox/workflows/triage-sentry.yml',
196
+ initiated_by_user_id: '55555555-5555-4555-8555-555555555555',
197
+ },
198
+ });
199
+
200
+ expect(result.success).toBe(false);
201
+ });
202
+
203
+ test('rejects a non-uuid initiated user id', () => {
204
+ const result = workflowRunDtoSchema.safeParse({
205
+ ...baseRun,
206
+ source_snapshot: null,
207
+ origin: 'dev',
208
+ dev_source: {
209
+ ref: 'fix-triage-prompt',
210
+ commit: 'abc123',
211
+ config_path: '.shipfox/workflows/triage-sentry.yml',
212
+ initiated_by_user_id: 'not-a-uuid',
213
+ replay_of_event_id: null,
214
+ },
215
+ });
216
+
217
+ expect(result.success).toBe(false);
218
+ });
219
+
220
+ test('rejects a non-uuid replayed event id', () => {
221
+ const result = workflowRunDtoSchema.safeParse({
222
+ ...baseRun,
223
+ source_snapshot: null,
224
+ origin: 'dev',
225
+ dev_source: {
226
+ ref: 'fix-triage-prompt',
227
+ commit: 'abc123',
228
+ config_path: '.shipfox/workflows/triage-sentry.yml',
229
+ initiated_by_user_id: '55555555-5555-4555-8555-555555555555',
230
+ replay_of_event_id: 'not-a-uuid',
231
+ },
232
+ });
233
+
234
+ expect(result.success).toBe(false);
235
+ });
236
+ });
237
+
238
+ describe('workflow run list query schema', () => {
239
+ const baseQuery = {project_id: '22222222-2222-4222-8222-222222222222'};
240
+
241
+ test('accepts an origin facet', () => {
242
+ const result = workflowRunListQuerySchema.parse({...baseQuery, origin: 'dev'});
243
+
244
+ expect(result.origin).toBe('dev');
245
+ });
246
+
247
+ test('rejects an unknown origin facet', () => {
248
+ const result = workflowRunListQuerySchema.safeParse({...baseQuery, origin: 'staging'});
249
+
250
+ expect(result.success).toBe(false);
251
+ });
252
+ });
253
+
103
254
  describe('workflow run list item schema', () => {
104
255
  function jobDto(position: number) {
105
256
  return {
@@ -24,6 +24,22 @@ export const workflowRunRerunModeSchema = z.enum(['all', 'failed']);
24
24
 
25
25
  export type WorkflowRunRerunModeDto = z.infer<typeof workflowRunRerunModeSchema>;
26
26
 
27
+ export const workflowRunOriginSchema = z.enum(['synced', 'dev']);
28
+
29
+ export type WorkflowRunOriginDto = z.infer<typeof workflowRunOriginSchema>;
30
+
31
+ // Dev-run provenance: the ref and pinned commit the definition came from, the file that
32
+ // ran, the user who started the run, and the journaled event it replays when any.
33
+ export const workflowRunDevSourceSchema = z.object({
34
+ ref: z.string(),
35
+ commit: z.string(),
36
+ config_path: z.string(),
37
+ initiated_by_user_id: z.string().uuid(),
38
+ replay_of_event_id: z.string().uuid().nullable(),
39
+ });
40
+
41
+ export type WorkflowRunDevSourceDto = z.infer<typeof workflowRunDevSourceSchema>;
42
+
27
43
  export const rerunWorkflowRunBodySchema = z.object({
28
44
  mode: workflowRunRerunModeSchema,
29
45
  });
@@ -38,6 +54,7 @@ const runListQueryBaseSchema = z.object({
38
54
  status: workflowRunStatusSchema.optional(),
39
55
  definition_id: z.string().uuid().optional(),
40
56
  trigger_source: z.string().optional(),
57
+ origin: workflowRunOriginSchema.optional(),
41
58
  created_from: isoDateTimeSchema.optional(),
42
59
  created_to: isoDateTimeSchema.optional(),
43
60
  });
@@ -97,7 +114,7 @@ export const workflowRunTriggerReferenceSchema = z.object({
97
114
 
98
115
  export type WorkflowRunTriggerReferenceDto = z.infer<typeof workflowRunTriggerReferenceSchema>;
99
116
 
100
- export const workflowRunDtoSchema = z.object({
117
+ export const workflowRunDtoFields = {
101
118
  id: z.string().uuid(),
102
119
  project_id: z.string().uuid(),
103
120
  definition_id: z.string().uuid(),
@@ -105,6 +122,9 @@ export const workflowRunDtoSchema = z.object({
105
122
  name: z.string(),
106
123
  workflow_name: z.string(),
107
124
  status: workflowRunStatusSchema,
125
+ // These defaults keep the response contract compatible while API and web deploys overlap.
126
+ origin: workflowRunOriginSchema.optional().default('synced'),
127
+ dev_source: workflowRunDevSourceSchema.nullable().optional().default(null),
108
128
  current_attempt: z.number().int().positive(),
109
129
  latest_attempt: z.number().int().positive(),
110
130
  trigger_provider: z.string().nullable(),
@@ -118,7 +138,31 @@ export const workflowRunDtoSchema = z.object({
118
138
  updated_at: z.string(),
119
139
  started_at: z.string().nullable(),
120
140
  finished_at: z.string().nullable(),
121
- });
141
+ };
142
+
143
+ export function validateWorkflowRunOrigin(
144
+ value: {origin: WorkflowRunOriginDto; dev_source: WorkflowRunDevSourceDto | null},
145
+ ctx: z.RefinementCtx,
146
+ ) {
147
+ if (value.origin === 'synced' && value.dev_source !== null) {
148
+ ctx.addIssue({
149
+ code: z.ZodIssueCode.custom,
150
+ message: 'Synced runs cannot include dev_source',
151
+ path: ['dev_source'],
152
+ });
153
+ }
154
+ if (value.origin === 'dev' && value.dev_source === null) {
155
+ ctx.addIssue({
156
+ code: z.ZodIssueCode.custom,
157
+ message: 'Dev runs require dev_source',
158
+ path: ['dev_source'],
159
+ });
160
+ }
161
+ }
162
+
163
+ export const workflowRunDtoSchema = z
164
+ .object(workflowRunDtoFields)
165
+ .superRefine(validateWorkflowRunOrigin);
122
166
 
123
167
  export type WorkflowRunDto = z.infer<typeof workflowRunDtoSchema>;
124
168
 
@@ -191,27 +235,35 @@ export type WorkflowRunJobDisplayStatusCountDto = z.infer<
191
235
  typeof workflowRunJobDisplayStatusCountDtoSchema
192
236
  >;
193
237
 
194
- export const workflowRunListItemSchema = workflowRunResponseSchema.extend({
195
- /** Up to `WORKFLOW_RUN_JOB_PREVIEW_LIMIT` jobs in graph order, not the whole set. */
196
- jobs: z.array(workflowRunJobSummaryDtoSchema).max(WORKFLOW_RUN_JOB_PREVIEW_LIMIT),
197
- /** Persisted verdict counts, kept stable so older web clients can consume new responses. */
198
- job_status_counts: z.array(workflowRunJobStatusCountDtoSchema),
199
- /**
200
- * Display-state counts over every job of the attempt, including those past the preview.
201
- * Optional so a new web client can consume an older API response during rollout.
202
- */
203
- job_display_status_counts: z.array(workflowRunJobDisplayStatusCountDtoSchema).optional(),
204
- /**
205
- * Whether any job execution in the attempt reached its runner. A `cancelled` job does not say
206
- * this on its own, so the counts above cannot answer it.
207
- *
208
- * Defaults to started, so an older API response during a rollout keeps the reading a run had
209
- * before this field existed rather than claiming work that ran never began.
210
- */
211
- has_started_job_execution: z.boolean().optional().default(true),
212
- });
213
-
214
- export type WorkflowRunListItemDto = z.input<typeof workflowRunListItemSchema>;
238
+ export const workflowRunListItemSchema = z
239
+ .object({
240
+ ...workflowRunDtoFields,
241
+ /** Up to `WORKFLOW_RUN_JOB_PREVIEW_LIMIT` jobs in graph order, not the whole set. */
242
+ jobs: z.array(workflowRunJobSummaryDtoSchema).max(WORKFLOW_RUN_JOB_PREVIEW_LIMIT),
243
+ /** Persisted verdict counts, kept stable so older web clients can consume new responses. */
244
+ job_status_counts: z.array(workflowRunJobStatusCountDtoSchema),
245
+ /**
246
+ * Display-state counts over every job of the attempt, including those past the preview.
247
+ * Optional so a new web client can consume an older API response during rollout.
248
+ */
249
+ job_display_status_counts: z.array(workflowRunJobDisplayStatusCountDtoSchema).optional(),
250
+ /**
251
+ * Whether any job execution in the attempt reached its runner. A `cancelled` job does not say
252
+ * this on its own, so the counts above cannot answer it.
253
+ *
254
+ * Defaults to started, so an older API response during a rollout keeps the reading a run had
255
+ * before this field existed rather than claiming work that ran never began.
256
+ */
257
+ has_started_job_execution: z.boolean().optional().default(true),
258
+ })
259
+ .superRefine(validateWorkflowRunOrigin);
260
+
261
+ type WorkflowRunListItemInput = z.input<typeof workflowRunListItemSchema>;
262
+
263
+ export type WorkflowRunListItemDto = Omit<WorkflowRunListItemInput, 'origin' | 'dev_source'> & {
264
+ origin: WorkflowRunOriginDto;
265
+ dev_source: WorkflowRunDevSourceDto | null;
266
+ };
215
267
 
216
268
  export const workflowRunListResponseSchema = z.object({
217
269
  runs: z.array(workflowRunListItemSchema),
@@ -219,7 +271,11 @@ export const workflowRunListResponseSchema = z.object({
219
271
  filtered_total_count: z.number().int().nonnegative().nullable(),
220
272
  });
221
273
 
222
- export type WorkflowRunListResponseDto = z.input<typeof workflowRunListResponseSchema>;
274
+ type WorkflowRunListResponseInput = z.input<typeof workflowRunListResponseSchema>;
275
+
276
+ export type WorkflowRunListResponseDto = Omit<WorkflowRunListResponseInput, 'runs'> & {
277
+ runs: WorkflowRunListItemDto[];
278
+ };
223
279
 
224
280
  const aggregateBucketSchema = z.object({
225
281
  value: z.string(),