@shipfox/api-workflows 13.0.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.
Files changed (101) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +46 -0
  3. package/README.md +12 -5
  4. package/dist/core/checkout.d.ts +4 -2
  5. package/dist/core/checkout.d.ts.map +1 -1
  6. package/dist/core/checkout.js +7 -2
  7. package/dist/core/checkout.js.map +1 -1
  8. package/dist/core/entities/job.d.ts +1 -1
  9. package/dist/core/entities/job.d.ts.map +1 -1
  10. package/dist/core/entities/job.js.map +1 -1
  11. package/dist/core/entities/workflow-run.d.ts +23 -4
  12. package/dist/core/entities/workflow-run.d.ts.map +1 -1
  13. package/dist/core/entities/workflow-run.js.map +1 -1
  14. package/dist/core/errors.d.ts +9 -1
  15. package/dist/core/errors.d.ts.map +1 -1
  16. package/dist/core/errors.js +5 -1
  17. package/dist/core/errors.js.map +1 -1
  18. package/dist/core/job-execution.d.ts.map +1 -1
  19. package/dist/core/job-execution.js +12 -2
  20. package/dist/core/job-execution.js.map +1 -1
  21. package/dist/core/run-workflow.js +1 -1
  22. package/dist/core/run-workflow.js.map +1 -1
  23. package/dist/core/step-config/agent.d.ts.map +1 -1
  24. package/dist/core/step-config/agent.js +9 -1
  25. package/dist/core/step-config/agent.js.map +1 -1
  26. package/dist/db/db.d.ts +72 -0
  27. package/dist/db/db.d.ts.map +1 -1
  28. package/dist/db/schema/workflow-runs.d.ts +45 -1
  29. package/dist/db/schema/workflow-runs.d.ts.map +1 -1
  30. package/dist/db/schema/workflow-runs.js +42 -1
  31. package/dist/db/schema/workflow-runs.js.map +1 -1
  32. package/dist/db/workflow-runs/jobs.d.ts +3 -1
  33. package/dist/db/workflow-runs/jobs.d.ts.map +1 -1
  34. package/dist/db/workflow-runs/jobs.js +12 -3
  35. package/dist/db/workflow-runs/jobs.js.map +1 -1
  36. package/dist/db/workflow-runs/queries.d.ts +3 -2
  37. package/dist/db/workflow-runs/queries.d.ts.map +1 -1
  38. package/dist/db/workflow-runs/queries.js +3 -0
  39. package/dist/db/workflow-runs/queries.js.map +1 -1
  40. package/dist/presentation/dto/step.d.ts.map +1 -1
  41. package/dist/presentation/dto/step.js +14 -0
  42. package/dist/presentation/dto/step.js.map +1 -1
  43. package/dist/presentation/dto/workflow-run.d.ts.map +1 -1
  44. package/dist/presentation/dto/workflow-run.js +14 -0
  45. package/dist/presentation/dto/workflow-run.js.map +1 -1
  46. package/dist/presentation/routes/agent-runtime-config.d.ts.map +1 -1
  47. package/dist/presentation/routes/agent-runtime-config.js +22 -0
  48. package/dist/presentation/routes/agent-runtime-config.js.map +1 -1
  49. package/dist/presentation/routes/checkout-token.d.ts.map +1 -1
  50. package/dist/presentation/routes/checkout-token.js +2 -1
  51. package/dist/presentation/routes/checkout-token.js.map +1 -1
  52. package/dist/presentation/routes/get-run-aggregates.d.ts.map +1 -1
  53. package/dist/presentation/routes/get-run-aggregates.js +2 -1
  54. package/dist/presentation/routes/get-run-aggregates.js.map +1 -1
  55. package/dist/presentation/routes/leased-step.d.ts +3 -1
  56. package/dist/presentation/routes/leased-step.d.ts.map +1 -1
  57. package/dist/presentation/routes/leased-step.js +2 -1
  58. package/dist/presentation/routes/leased-step.js.map +1 -1
  59. package/dist/presentation/routes/list-runs.d.ts.map +1 -1
  60. package/dist/presentation/routes/list-runs.js +2 -1
  61. package/dist/presentation/routes/list-runs.js.map +1 -1
  62. package/dist/presentation/routes/project-access.d.ts +1 -0
  63. package/dist/presentation/routes/project-access.d.ts.map +1 -1
  64. package/dist/tsconfig.test.tsbuildinfo +1 -1
  65. package/drizzle/0004_dev_run_origin.sql +12 -0
  66. package/drizzle/meta/0004_snapshot.json +1773 -0
  67. package/drizzle/meta/_journal.json +7 -0
  68. package/package.json +10 -10
  69. package/src/core/checkout.test.ts +198 -1
  70. package/src/core/checkout.ts +18 -5
  71. package/src/core/entities/job.ts +1 -1
  72. package/src/core/entities/workflow-run.ts +22 -4
  73. package/src/core/errors.ts +17 -2
  74. package/src/core/job-execution.test.ts +54 -0
  75. package/src/core/job-execution.ts +6 -1
  76. package/src/core/job-transition/decide-job-activation.test.ts +2 -0
  77. package/src/core/run-workflow.test.ts +50 -1
  78. package/src/core/run-workflow.ts +1 -1
  79. package/src/core/step-config/agent.ts +8 -1
  80. package/src/core/step-config/complete-step-dispatch-config.test.ts +37 -0
  81. package/src/core/workflow-run-creation.test.ts +2 -0
  82. package/src/db/job-listeners.test.ts +17 -0
  83. package/src/db/schema/workflow-runs.ts +58 -0
  84. package/src/db/workflow-runs/jobs.ts +17 -3
  85. package/src/db/workflow-runs/queries.ts +6 -1
  86. package/src/db/workflow-runs/run-create.test.ts +39 -0
  87. package/src/presentation/dto/step.test.ts +8 -0
  88. package/src/presentation/dto/step.ts +9 -0
  89. package/src/presentation/dto/workflow-run.ts +23 -1
  90. package/src/presentation/routes/agent-runtime-config.test.ts +84 -2
  91. package/src/presentation/routes/agent-runtime-config.ts +24 -0
  92. package/src/presentation/routes/checkout-token.test.ts +45 -0
  93. package/src/presentation/routes/checkout-token.ts +2 -1
  94. package/src/presentation/routes/get-run-aggregates.test.ts +56 -0
  95. package/src/presentation/routes/get-run-aggregates.ts +2 -0
  96. package/src/presentation/routes/get-step-secrets.test.ts +1 -0
  97. package/src/presentation/routes/leased-step.ts +7 -1
  98. package/src/presentation/routes/list-runs.test.ts +152 -0
  99. package/src/presentation/routes/list-runs.ts +2 -0
  100. package/test/factories/workflow-run.ts +2 -0
  101. package/tsconfig.build.tsbuildinfo +1 -1
@@ -29,6 +29,13 @@
29
29
  "when": 1786266649202,
30
30
  "tag": "0003_output_invalid_reason",
31
31
  "breakpoints": true
32
+ },
33
+ {
34
+ "idx": 4,
35
+ "version": "7",
36
+ "when": 1787477580166,
37
+ "tag": "0004_dev_run_origin",
38
+ "breakpoints": true
32
39
  }
33
40
  ]
34
41
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/api-workflows",
3
3
  "license": "MIT",
4
- "version": "13.0.0",
4
+ "version": "14.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -25,28 +25,28 @@
25
25
  "zod": "^4.4.3",
26
26
  "@shipfox/api-auth-context": "12.2.0",
27
27
  "@shipfox/api-auth-dto": "12.0.0",
28
- "@shipfox/api-agent-dto": "12.2.0",
29
28
  "@shipfox/annotations-dto": "12.3.0",
30
- "@shipfox/api-definitions-dto": "12.3.0",
31
- "@shipfox/api-projects-dto": "12.2.0",
32
- "@shipfox/api-integration-core-dto": "12.2.0",
33
- "@shipfox/api-runners-dto": "13.0.0",
29
+ "@shipfox/api-agent-dto": "14.0.0",
30
+ "@shipfox/api-definitions-dto": "14.0.0",
31
+ "@shipfox/api-integration-core-dto": "14.0.0",
32
+ "@shipfox/api-runners-dto": "14.0.0",
33
+ "@shipfox/api-projects-dto": "14.0.0",
34
+ "@shipfox/api-workflows-dto": "14.0.0",
34
35
  "@shipfox/api-secrets-dto": "12.0.0",
35
- "@shipfox/api-workflows-dto": "13.0.0",
36
36
  "@shipfox/api-workspaces-dto": "12.0.0",
37
- "@shipfox/config": "1.2.4",
38
37
  "@shipfox/inter-module": "0.2.3",
39
- "@shipfox/expression": "2.2.0",
38
+ "@shipfox/expression": "2.2.1",
40
39
  "@shipfox/node-drizzle": "0.3.5",
41
40
  "@shipfox/node-error-monitoring": "0.3.0",
42
41
  "@shipfox/node-fastify": "0.4.2",
43
42
  "@shipfox/node-module": "1.0.6",
44
43
  "@shipfox/node-opentelemetry": "0.6.4",
45
44
  "@shipfox/node-outbox": "0.2.6",
45
+ "@shipfox/config": "1.2.4",
46
46
  "@shipfox/node-postgres": "0.5.0",
47
47
  "@shipfox/node-temporal": "0.4.5",
48
48
  "@shipfox/runner-labels": "0.2.0",
49
- "@shipfox/workflow-document": "3.0.1"
49
+ "@shipfox/workflow-document": "3.1.0"
50
50
  },
51
51
  "imports": {
52
52
  "#*": "./dist/*"
@@ -3,9 +3,24 @@ import type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module'
3
3
  import {projectFactory} from '#test/factories/project.js';
4
4
  import {createStepCheckoutSpec} from './checkout.js';
5
5
  import type {Step} from './entities/step.js';
6
- import type {WorkflowRunTriggerReference} from './entities/workflow-run.js';
6
+ import type {WorkflowRunDevSource, WorkflowRunTriggerReference} from './entities/workflow-run.js';
7
7
  import {CheckoutConfigInvalidError, CheckoutIntentUnresolvedError} from './errors.js';
8
8
 
9
+ const syncedRun = {origin: 'synced', devSource: null} as const;
10
+
11
+ function devRun(commit: string): {origin: 'dev'; devSource: WorkflowRunDevSource} {
12
+ return {
13
+ origin: 'dev',
14
+ devSource: {
15
+ ref: 'fix-triage-prompt',
16
+ commit,
17
+ configPath: '.shipfox/workflows/triage-sentry.yml',
18
+ initiatedByUserId: crypto.randomUUID(),
19
+ replayOfEventId: null,
20
+ },
21
+ };
22
+ }
23
+
9
24
  const getProjectById = vi.fn();
10
25
  const resolveCheckoutTarget = vi.fn();
11
26
  const projects = {
@@ -46,6 +61,7 @@ describe('createStepCheckoutSpec', () => {
46
61
  });
47
62
 
48
63
  const result = await createStepCheckoutSpec({
64
+ run: syncedRun,
49
65
  step,
50
66
  workspaceId: project.workspaceId,
51
67
  projectId: project.id,
@@ -96,6 +112,7 @@ describe('createStepCheckoutSpec', () => {
96
112
  });
97
113
 
98
114
  const result = await createStepCheckoutSpec({
115
+ run: syncedRun,
99
116
  step,
100
117
  workspaceId: project.workspaceId,
101
118
  projectId: project.id,
@@ -134,6 +151,7 @@ describe('createStepCheckoutSpec', () => {
134
151
  });
135
152
 
136
153
  await createStepCheckoutSpec({
154
+ run: syncedRun,
137
155
  step,
138
156
  workspaceId: project.workspaceId,
139
157
  projectId: project.id,
@@ -167,6 +185,7 @@ describe('createStepCheckoutSpec', () => {
167
185
  });
168
186
 
169
187
  await createStepCheckoutSpec({
188
+ run: syncedRun,
170
189
  step,
171
190
  workspaceId: project.workspaceId,
172
191
  projectId: project.id,
@@ -200,6 +219,7 @@ describe('createStepCheckoutSpec', () => {
200
219
  });
201
220
 
202
221
  await createStepCheckoutSpec({
222
+ run: syncedRun,
203
223
  step,
204
224
  workspaceId: project.workspaceId,
205
225
  projectId: project.id,
@@ -216,6 +236,41 @@ describe('createStepCheckoutSpec', () => {
216
236
  });
217
237
  });
218
238
 
239
+ it('falls through to the dev commit when a same-project trigger has no commit', async () => {
240
+ const project = projectFactory.build();
241
+ const devCommit = 'b'.repeat(40);
242
+ const triggerReference = {...triggerReferenceFor(project.id), commit: null};
243
+ const step = checkoutStep({permissions: {contents: 'read'}});
244
+ getProjectById.mockResolvedValue({project});
245
+ resolveCheckoutTarget.mockResolvedValue({
246
+ projectId: project.id,
247
+ connectionId: project.sourceConnectionId,
248
+ externalRepositoryId: project.sourceExternalRepositoryId,
249
+ });
250
+ createCheckoutSpec.mockResolvedValue({
251
+ repositoryUrl: 'https://github.com/acme/repo.git',
252
+ ref: devCommit,
253
+ });
254
+
255
+ await createStepCheckoutSpec({
256
+ run: devRun(devCommit),
257
+ step,
258
+ workspaceId: project.workspaceId,
259
+ projectId: project.id,
260
+ triggerReference,
261
+ integrations: integrations as IntegrationsModuleClient,
262
+ projects: projects as ProjectsModuleClient,
263
+ });
264
+
265
+ expect(createCheckoutSpec).toHaveBeenCalledWith({
266
+ workspaceId: project.workspaceId,
267
+ connectionId: project.sourceConnectionId,
268
+ externalRepositoryId: project.sourceExternalRepositoryId,
269
+ ref: devCommit,
270
+ permissions: {contents: 'read'},
271
+ });
272
+ });
273
+
219
274
  it('keeps the provider default ref for a cross-repository target', async () => {
220
275
  const project = projectFactory.build();
221
276
  const targetProjectId = crypto.randomUUID();
@@ -232,6 +287,142 @@ describe('createStepCheckoutSpec', () => {
232
287
  });
233
288
 
234
289
  await createStepCheckoutSpec({
290
+ run: syncedRun,
291
+ step,
292
+ workspaceId: project.workspaceId,
293
+ projectId: project.id,
294
+ triggerReference: triggerReferenceFor(project.id),
295
+ integrations: integrations as IntegrationsModuleClient,
296
+ projects: projects as ProjectsModuleClient,
297
+ });
298
+
299
+ expect(createCheckoutSpec).toHaveBeenCalledWith({
300
+ workspaceId: project.workspaceId,
301
+ connectionId: expect.any(String),
302
+ externalRepositoryId: 'github:412',
303
+ permissions: {contents: 'read'},
304
+ });
305
+ });
306
+
307
+ it('checks out the dev commit for a dev run without a trigger reference', async () => {
308
+ const project = projectFactory.build();
309
+ const devCommit = 'b'.repeat(40);
310
+ const step = checkoutStep({permissions: {contents: 'read'}});
311
+ getProjectById.mockResolvedValue({project});
312
+ resolveCheckoutTarget.mockResolvedValue({
313
+ projectId: project.id,
314
+ connectionId: project.sourceConnectionId,
315
+ externalRepositoryId: project.sourceExternalRepositoryId,
316
+ });
317
+ createCheckoutSpec.mockResolvedValue({
318
+ repositoryUrl: 'https://github.com/acme/repo.git',
319
+ ref: devCommit,
320
+ });
321
+
322
+ await createStepCheckoutSpec({
323
+ run: devRun(devCommit),
324
+ step,
325
+ workspaceId: project.workspaceId,
326
+ projectId: project.id,
327
+ integrations: integrations as IntegrationsModuleClient,
328
+ projects: projects as ProjectsModuleClient,
329
+ });
330
+
331
+ expect(createCheckoutSpec).toHaveBeenCalledWith({
332
+ workspaceId: project.workspaceId,
333
+ connectionId: project.sourceConnectionId,
334
+ externalRepositoryId: project.sourceExternalRepositoryId,
335
+ ref: devCommit,
336
+ permissions: {contents: 'read'},
337
+ });
338
+ });
339
+
340
+ it('prefers the event commit over the dev commit on a dev replay of a same-project push', async () => {
341
+ const project = projectFactory.build();
342
+ const eventCommit = 'c'.repeat(40);
343
+ const triggerReference = {...triggerReferenceFor(project.id), commit: eventCommit};
344
+ const step = checkoutStep({permissions: {contents: 'read'}});
345
+ getProjectById.mockResolvedValue({project});
346
+ resolveCheckoutTarget.mockResolvedValue({
347
+ projectId: project.id,
348
+ connectionId: project.sourceConnectionId,
349
+ externalRepositoryId: project.sourceExternalRepositoryId,
350
+ });
351
+ createCheckoutSpec.mockResolvedValue({
352
+ repositoryUrl: 'https://github.com/acme/repo.git',
353
+ ref: eventCommit,
354
+ });
355
+
356
+ await createStepCheckoutSpec({
357
+ run: devRun('b'.repeat(40)),
358
+ step,
359
+ workspaceId: project.workspaceId,
360
+ projectId: project.id,
361
+ triggerReference,
362
+ integrations: integrations as IntegrationsModuleClient,
363
+ projects: projects as ProjectsModuleClient,
364
+ });
365
+
366
+ expect(createCheckoutSpec).toHaveBeenCalledWith({
367
+ workspaceId: project.workspaceId,
368
+ connectionId: project.sourceConnectionId,
369
+ externalRepositoryId: project.sourceExternalRepositoryId,
370
+ ref: eventCommit,
371
+ permissions: {contents: 'read'},
372
+ });
373
+ });
374
+
375
+ it('preserves an explicit ref on a dev run', async () => {
376
+ const project = projectFactory.build();
377
+ const explicitRef = 'refs/heads/feature/checkout';
378
+ const step = checkoutStep({ref: explicitRef, permissions: {contents: 'read'}});
379
+ getProjectById.mockResolvedValue({project});
380
+ resolveCheckoutTarget.mockResolvedValue({
381
+ projectId: project.id,
382
+ connectionId: project.sourceConnectionId,
383
+ externalRepositoryId: project.sourceExternalRepositoryId,
384
+ });
385
+ createCheckoutSpec.mockResolvedValue({
386
+ repositoryUrl: 'https://github.com/acme/repo.git',
387
+ ref: explicitRef,
388
+ });
389
+
390
+ await createStepCheckoutSpec({
391
+ run: devRun('b'.repeat(40)),
392
+ step,
393
+ workspaceId: project.workspaceId,
394
+ projectId: project.id,
395
+ triggerReference: triggerReferenceFor(project.id),
396
+ integrations: integrations as IntegrationsModuleClient,
397
+ projects: projects as ProjectsModuleClient,
398
+ });
399
+
400
+ expect(createCheckoutSpec).toHaveBeenCalledWith({
401
+ workspaceId: project.workspaceId,
402
+ connectionId: project.sourceConnectionId,
403
+ externalRepositoryId: project.sourceExternalRepositoryId,
404
+ ref: explicitRef,
405
+ permissions: {contents: 'read'},
406
+ });
407
+ });
408
+
409
+ it('keeps the provider default ref for a cross-project target in a dev run', async () => {
410
+ const project = projectFactory.build();
411
+ const targetProjectId = crypto.randomUUID();
412
+ const step = checkoutStep({project: targetProjectId});
413
+ getProjectById.mockResolvedValue({project});
414
+ resolveCheckoutTarget.mockResolvedValue({
415
+ projectId: targetProjectId,
416
+ connectionId: crypto.randomUUID(),
417
+ externalRepositoryId: 'github:412',
418
+ });
419
+ createCheckoutSpec.mockResolvedValue({
420
+ repositoryUrl: 'https://github.com/acme/target.git',
421
+ ref: 'main',
422
+ });
423
+
424
+ await createStepCheckoutSpec({
425
+ run: devRun('b'.repeat(40)),
235
426
  step,
236
427
  workspaceId: project.workspaceId,
237
428
  projectId: project.id,
@@ -263,6 +454,7 @@ describe('createStepCheckoutSpec', () => {
263
454
  });
264
455
 
265
456
  await createStepCheckoutSpec({
457
+ run: syncedRun,
266
458
  step,
267
459
  workspaceId: project.workspaceId,
268
460
  projectId: project.id,
@@ -294,6 +486,7 @@ describe('createStepCheckoutSpec', () => {
294
486
  });
295
487
 
296
488
  await createStepCheckoutSpec({
489
+ run: syncedRun,
297
490
  step,
298
491
  workspaceId: project.workspaceId,
299
492
  projectId: project.id,
@@ -330,6 +523,7 @@ describe('createStepCheckoutSpec', () => {
330
523
  });
331
524
 
332
525
  await createStepCheckoutSpec({
526
+ run: syncedRun,
333
527
  step,
334
528
  workspaceId: project.workspaceId,
335
529
  projectId: project.id,
@@ -350,6 +544,7 @@ describe('createStepCheckoutSpec', () => {
350
544
  {connection: 'github'},
351
545
  ])('rejects invalid checkout target shape: %j', async (checkout) => {
352
546
  const act = createStepCheckoutSpec({
547
+ run: syncedRun,
353
548
  step: checkoutStep(checkout),
354
549
  workspaceId: crypto.randomUUID(),
355
550
  projectId: crypto.randomUUID(),
@@ -368,6 +563,7 @@ describe('createStepCheckoutSpec', () => {
368
563
  getProjectById.mockResolvedValue({project: null});
369
564
 
370
565
  const act = createStepCheckoutSpec({
566
+ run: syncedRun,
371
567
  step: checkoutStep({}),
372
568
  workspaceId: crypto.randomUUID(),
373
569
  projectId,
@@ -387,6 +583,7 @@ describe('createStepCheckoutSpec', () => {
387
583
  resolveConnection.mockResolvedValue(null);
388
584
 
389
585
  const act = createStepCheckoutSpec({
586
+ run: syncedRun,
390
587
  step,
391
588
  workspaceId: project.workspaceId,
392
589
  projectId: project.id,
@@ -3,7 +3,10 @@ import type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module'
3
3
  import {checkoutTargetValidationIssues} from '@shipfox/workflow-document';
4
4
  import {z} from 'zod';
5
5
  import type {Step} from '#core/entities/step.js';
6
- import type {WorkflowRunTriggerReference} from '#core/entities/workflow-run.js';
6
+ import type {
7
+ WorkflowRunOriginState,
8
+ WorkflowRunTriggerReference,
9
+ } from '#core/entities/workflow-run.js';
7
10
  import {CheckoutConfigInvalidError, CheckoutIntentUnresolvedError} from './errors.js';
8
11
 
9
12
  const checkoutConfigSchema = z
@@ -39,6 +42,7 @@ export async function createStepCheckoutSpec({
39
42
  workspaceId,
40
43
  projectId,
41
44
  triggerReference,
45
+ run,
42
46
  integrations,
43
47
  projects,
44
48
  }: {
@@ -46,6 +50,8 @@ export async function createStepCheckoutSpec({
46
50
  workspaceId: string;
47
51
  projectId: string;
48
52
  triggerReference?: WorkflowRunTriggerReference | null | undefined;
53
+ /** The run's origin state; a dev run checks out its dev commit by default. */
54
+ run: WorkflowRunOriginState;
49
55
  integrations: IntegrationsModuleClient;
50
56
  projects: ProjectsModuleClient;
51
57
  }): Promise<{
@@ -79,11 +85,18 @@ export async function createStepCheckoutSpec({
79
85
  },
80
86
  target,
81
87
  });
82
- const ref =
83
- checkout.ref ??
84
- (triggerReference?.project?.id === resolvedTarget.projectId
88
+ // Explicit step refs win, followed by same-project event commits (including
89
+ // replays), then the pinned dev commit for the run's own project. Other targets
90
+ // intentionally use the provider default branch.
91
+ const triggerCommitRef =
92
+ triggerReference?.project?.id === resolvedTarget.projectId
85
93
  ? (triggerReference.commit ?? undefined)
86
- : undefined);
94
+ : undefined;
95
+ const devCommitRef =
96
+ run.origin === 'dev' && resolvedTarget.projectId === projectId
97
+ ? run.devSource.commit
98
+ : undefined;
99
+ const ref = checkout.ref ?? triggerCommitRef ?? devCommitRef;
87
100
  const response = await integrations.createCheckoutSpec({
88
101
  workspaceId,
89
102
  connectionId: resolvedTarget.connectionId,
@@ -69,7 +69,7 @@ export interface Job {
69
69
 
70
70
  export interface JobListeningTrigger {
71
71
  readonly source: string;
72
- readonly event: string;
72
+ readonly event?: string;
73
73
  readonly inputs?: Readonly<Record<string, unknown>>;
74
74
  readonly filter?: string;
75
75
  }
@@ -5,6 +5,24 @@ import type {WorkflowRunAttempt} from './workflow-run-attempt.js';
5
5
 
6
6
  export type WorkflowRunStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'cancelled';
7
7
 
8
+ export type WorkflowRunOrigin = 'synced' | 'dev';
9
+
10
+ /**
11
+ * Why a dev run was created: the ref and pinned commit the definition came from, the
12
+ * file that ran, the user who started it, and the journaled event it replays when any.
13
+ */
14
+ export interface WorkflowRunDevSource {
15
+ ref: string;
16
+ commit: string;
17
+ configPath: string;
18
+ initiatedByUserId: string;
19
+ replayOfEventId: string | null;
20
+ }
21
+
22
+ export type WorkflowRunOriginState =
23
+ | {origin: 'synced'; devSource: null}
24
+ | {origin: 'dev'; devSource: WorkflowRunDevSource};
25
+
8
26
  export type TerminalWorkflowRunStatus = Extract<
9
27
  WorkflowRunStatus,
10
28
  'succeeded' | 'failed' | 'cancelled'
@@ -60,7 +78,7 @@ export type TriggerPayload =
60
78
  data: unknown;
61
79
  };
62
80
 
63
- export interface WorkflowRun {
81
+ export type WorkflowRun = WorkflowRunOriginState & {
64
82
  id: string;
65
83
  workspaceId: string;
66
84
  projectId: string;
@@ -89,7 +107,7 @@ export interface WorkflowRun {
89
107
  updatedAt: Date;
90
108
  startedAt: Date | null;
91
109
  finishedAt: Date | null;
92
- }
110
+ };
93
111
 
94
112
  export interface StepDetail extends Step {
95
113
  attempts: StepAttempt[];
@@ -103,11 +121,11 @@ export interface WorkflowJobDetail extends Job {
103
121
  jobExecutions: JobExecutionDetail[];
104
122
  }
105
123
 
106
- export interface WorkflowRunDetail extends WorkflowRun {
124
+ export type WorkflowRunDetail = WorkflowRun & {
107
125
  runAttempt: WorkflowRunAttempt;
108
126
  latestAttempt: number;
109
127
  jobs: WorkflowJobDetail[];
110
128
  /** Whether any job execution of this attempt reached its runner, decided here so the detail
111
129
  * and the list cannot answer it differently. */
112
130
  hasStartedJobExecution: boolean;
113
- }
131
+ };
@@ -38,13 +38,28 @@ export class WorkspaceNotFoundError extends Error {
38
38
  }
39
39
  }
40
40
 
41
+ export interface AgentConfigUnresolvableErrorOptions {
42
+ readonly cause?: unknown;
43
+ readonly message?: string;
44
+ readonly code?: string;
45
+ readonly managedProviderId?: string;
46
+ }
47
+
41
48
  export class AgentConfigUnresolvableError extends Error {
49
+ readonly code?: string | undefined;
50
+ readonly managedProviderId?: string | undefined;
51
+
42
52
  constructor(
43
53
  readonly definitionId: string,
44
- options?: ErrorOptions | undefined,
54
+ options?: AgentConfigUnresolvableErrorOptions | undefined,
45
55
  ) {
46
- super(`Agent configuration cannot be resolved for definition ${definitionId}`, options);
56
+ super(
57
+ options?.message ?? `Agent configuration cannot be resolved for definition ${definitionId}`,
58
+ options?.cause === undefined ? undefined : {cause: options.cause},
59
+ );
47
60
  this.name = 'AgentConfigUnresolvableError';
61
+ this.code = options?.code;
62
+ this.managedProviderId = options?.managedProviderId;
48
63
  }
49
64
  }
50
65
 
@@ -1,3 +1,7 @@
1
+ import {
2
+ type AgentInterModuleClient,
3
+ agentInterModuleContract,
4
+ } from '@shipfox/api-agent-dto/inter-module';
1
5
  import {
2
6
  WORKFLOWS_JOB_STEPS_SETTLED,
3
7
  WORKFLOWS_STEP_ATTEMPT_TERMINATED,
@@ -10,6 +14,7 @@ import {
10
14
  parseWorkflowTemplate,
11
15
  planInterpolationField,
12
16
  } from '@shipfox/expression';
17
+ import {createInterModuleKnownError} from '@shipfox/inter-module';
13
18
  import {and, eq, sql} from 'drizzle-orm';
14
19
  import {db} from '#db/db.js';
15
20
  import {workflowsOutbox} from '#db/schema/outbox.js';
@@ -322,6 +327,55 @@ describe('nextStepForJob', () => {
322
327
  });
323
328
  });
324
329
 
330
+ test('reports managed provider policy failures as agent config errors', async () => {
331
+ const {jobId, steps} = await arrangeJobWithSteps(1);
332
+ const pending = steps[0];
333
+ if (!pending) throw new Error('Expected pending step');
334
+ await db()
335
+ .update(stepsTable)
336
+ .set({
337
+ config: {},
338
+ configPlan: {
339
+ agent: {
340
+ prompt: {segments: [{kind: 'literal', value: 'Review the change.'}]},
341
+ },
342
+ },
343
+ })
344
+ .where(eq(stepsTable.id, pending.id));
345
+
346
+ const agent = {
347
+ getValidationCatalog: vi.fn(),
348
+ resolveAgentConfig: vi.fn().mockRejectedValue(
349
+ createInterModuleKnownError(
350
+ agentInterModuleContract.methods.resolveAgentConfig,
351
+ 'agent-config-invalid',
352
+ {
353
+ message: 'This instance only supports provider `shipfox`.',
354
+ managed_provider_id: 'shipfox',
355
+ },
356
+ ),
357
+ ),
358
+ resolveRuntimeCredentials: vi.fn(),
359
+ } as unknown as AgentInterModuleClient;
360
+
361
+ const next = await nextStepForJob(jobId, agent);
362
+
363
+ expect(next).toEqual({kind: 'done', status: 'failed'});
364
+ const after = await getStepsByJobId(jobId);
365
+ expect(after[0]).toMatchObject({
366
+ status: 'failed',
367
+ error: {
368
+ message: 'This instance only supports provider `shipfox`.',
369
+ reason: 'agent_config_invalid',
370
+ field: 'agent',
371
+ source: 'agent',
372
+ code: 'workspace-providers-disabled',
373
+ managedProviderId: 'shipfox',
374
+ agentConfigIssue: 'provider_unsupported',
375
+ },
376
+ });
377
+ });
378
+
325
379
  test('all steps succeeded → {done, succeeded}', async () => {
326
380
  const {jobId, steps} = await arrangeJobWithSteps(2);
327
381
  for (const step of steps) {
@@ -339,11 +339,16 @@ function dispatchConfigError(error: DispatchConfigError): Record<string, unknown
339
339
  };
340
340
  }
341
341
 
342
+ const isManagedProviderPolicyFailure =
343
+ error.code === 'workspace-providers-disabled' && error.managedProviderId !== undefined;
342
344
  return {
343
345
  message: error.message,
344
- reason: 'config_unresolvable',
346
+ reason: isManagedProviderPolicyFailure ? 'agent_config_invalid' : 'config_unresolvable',
345
347
  field: 'agent',
346
348
  source: 'agent',
349
+ ...(error.code === undefined ? {} : {code: error.code}),
350
+ ...(error.managedProviderId === undefined ? {} : {managedProviderId: error.managedProviderId}),
351
+ ...(isManagedProviderPolicyFailure ? {agentConfigIssue: 'provider_unsupported'} : {}),
347
352
  };
348
353
  }
349
354
 
@@ -121,6 +121,8 @@ function workflowRun(): WorkflowRun {
121
121
  workflowName: 'Run',
122
122
  nameOverride: null,
123
123
  status: 'pending',
124
+ origin: 'synced',
125
+ devSource: null,
124
126
  currentAttempt: 1,
125
127
  triggerProvider: null,
126
128
  triggerSource: 'manual',
@@ -23,6 +23,12 @@ type DefinitionForWorkflowRun = NonNullable<
23
23
  let definitionResponse: DefinitionForWorkflowRun | null = null;
24
24
  const definitions: DefinitionsInterModuleClient = {
25
25
  getDefinitionForWorkflowRun: async () => ({definition: definitionResponse}),
26
+ resolveDefinitionAtRef: () => {
27
+ throw new Error('resolveDefinitionAtRef is not used by runWorkflow');
28
+ },
29
+ listDefinitionsAtRef: () => {
30
+ throw new Error('listDefinitionsAtRef is not used by runWorkflow');
31
+ },
26
32
  };
27
33
 
28
34
  function buildDefinition(
@@ -31,6 +37,7 @@ function buildDefinition(
31
37
  const {model = workflowModel(), ...definitionOverrides} = overrides;
32
38
  return {
33
39
  id: crypto.randomUUID(),
40
+ workflowId: crypto.randomUUID(),
34
41
  projectId: crypto.randomUUID(),
35
42
  name: 'Test Workflow',
36
43
  model: createWorkflowModelSnapshot(model),
@@ -79,7 +86,7 @@ describe('runWorkflow', () => {
79
86
 
80
87
  expect(run.id).toBeDefined();
81
88
  expect(run.projectId).toBe(projectId);
82
- expect(run.definitionId).toBe(definition.id);
89
+ expect(run.definitionId).toBe(definition.workflowId);
83
90
  expect(run.name).toBe(definition.name);
84
91
  expect(run.status).toBe('pending');
85
92
  expect(run.triggerProvider).toBeNull();
@@ -151,6 +158,48 @@ describe('runWorkflow', () => {
151
158
  expect(run.sourceSnapshot).toEqual(sourceSnapshot);
152
159
  });
153
160
 
161
+ test('numbers runs by the workflow lineage id, not the definition row id', async () => {
162
+ const lineageId = crypto.randomUUID();
163
+ // A lineage whose id differs from the definition row id, as happens after
164
+ // the backfill when a lineage id diverges from the synced row it was born from.
165
+ const definition = buildDefinition({projectId, id: crypto.randomUUID(), workflowId: lineageId});
166
+ definitionResponse = definition;
167
+
168
+ const first = await runWorkflow(definitions, {
169
+ workspaceId,
170
+ projectId,
171
+ definitionId: definition.id,
172
+ triggerPayload: manualPayload(),
173
+ agent,
174
+ });
175
+ const second = await runWorkflow(definitions, {
176
+ workspaceId,
177
+ projectId,
178
+ definitionId: definition.id,
179
+ triggerPayload: manualPayload(),
180
+ agent,
181
+ });
182
+
183
+ expect(first.definitionId).toBe(lineageId);
184
+ expect(second.definitionId).toBe(lineageId);
185
+ expect(first.number).toBe(1);
186
+ expect(second.number).toBe(2);
187
+
188
+ // A later definition row that resolves to the same lineage continues the sequence.
189
+ const resynced = buildDefinition({projectId, id: crypto.randomUUID(), workflowId: lineageId});
190
+ definitionResponse = resynced;
191
+ const third = await runWorkflow(definitions, {
192
+ workspaceId,
193
+ projectId,
194
+ definitionId: resynced.id,
195
+ triggerPayload: manualPayload(),
196
+ agent,
197
+ });
198
+
199
+ expect(third.definitionId).toBe(lineageId);
200
+ expect(third.number).toBe(3);
201
+ });
202
+
154
203
  test('throws DefinitionNotFoundError for unknown definition', async () => {
155
204
  definitionResponse = null;
156
205