@shipfox/api-workflows 12.7.0 → 13.1.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 (122) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/CHANGELOG.md +24 -0
  3. package/dist/core/errors.d.ts +9 -1
  4. package/dist/core/errors.d.ts.map +1 -1
  5. package/dist/core/errors.js +5 -1
  6. package/dist/core/errors.js.map +1 -1
  7. package/dist/core/job-execution.d.ts.map +1 -1
  8. package/dist/core/job-execution.js +12 -2
  9. package/dist/core/job-execution.js.map +1 -1
  10. package/dist/core/step-config/agent.d.ts.map +1 -1
  11. package/dist/core/step-config/agent.js +9 -1
  12. package/dist/core/step-config/agent.js.map +1 -1
  13. package/dist/db/index.d.ts +1 -1
  14. package/dist/db/index.d.ts.map +1 -1
  15. package/dist/db/index.js +1 -1
  16. package/dist/db/index.js.map +1 -1
  17. package/dist/db/job-listeners.d.ts.map +1 -1
  18. package/dist/db/job-listeners.js +17 -1
  19. package/dist/db/job-listeners.js.map +1 -1
  20. package/dist/db/workflow-runs/job-executions.d.ts +2 -3
  21. package/dist/db/workflow-runs/job-executions.d.ts.map +1 -1
  22. package/dist/db/workflow-runs/job-executions.js +48 -20
  23. package/dist/db/workflow-runs/job-executions.js.map +1 -1
  24. package/dist/db/workflow-runs/outbox.d.ts +15 -0
  25. package/dist/db/workflow-runs/outbox.d.ts.map +1 -1
  26. package/dist/db/workflow-runs/outbox.js +35 -1
  27. package/dist/db/workflow-runs/outbox.js.map +1 -1
  28. package/dist/db/workflow-runs/run-graph.d.ts.map +1 -1
  29. package/dist/db/workflow-runs/run-graph.js +12 -0
  30. package/dist/db/workflow-runs/run-graph.js.map +1 -1
  31. package/dist/db/workflow-runs/run-status.d.ts.map +1 -1
  32. package/dist/db/workflow-runs/run-status.js +13 -1
  33. package/dist/db/workflow-runs/run-status.js.map +1 -1
  34. package/dist/db/workflow-runs.d.ts +2 -2
  35. package/dist/db/workflow-runs.d.ts.map +1 -1
  36. package/dist/db/workflow-runs.js +2 -2
  37. package/dist/db/workflow-runs.js.map +1 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +2 -4
  40. package/dist/index.js.map +1 -1
  41. package/dist/presentation/dto/step.d.ts.map +1 -1
  42. package/dist/presentation/dto/step.js +14 -0
  43. package/dist/presentation/dto/step.js.map +1 -1
  44. package/dist/presentation/index.d.ts +1 -1
  45. package/dist/presentation/index.d.ts.map +1 -1
  46. package/dist/presentation/index.js +1 -1
  47. package/dist/presentation/index.js.map +1 -1
  48. package/dist/presentation/routes/agent-runtime-config.d.ts.map +1 -1
  49. package/dist/presentation/routes/agent-runtime-config.js +22 -0
  50. package/dist/presentation/routes/agent-runtime-config.js.map +1 -1
  51. package/dist/presentation/subscribers/index.d.ts +0 -1
  52. package/dist/presentation/subscribers/index.d.ts.map +1 -1
  53. package/dist/presentation/subscribers/index.js +0 -1
  54. package/dist/presentation/subscribers/index.js.map +1 -1
  55. package/dist/temporal/activities/index.d.ts +2 -18
  56. package/dist/temporal/activities/index.d.ts.map +1 -1
  57. package/dist/temporal/activities/index.js +3 -5
  58. package/dist/temporal/activities/index.js.map +1 -1
  59. package/dist/temporal/activities/orchestration-activities.d.ts +5 -14
  60. package/dist/temporal/activities/orchestration-activities.d.ts.map +1 -1
  61. package/dist/temporal/activities/orchestration-activities.js +13 -34
  62. package/dist/temporal/activities/orchestration-activities.js.map +1 -1
  63. package/dist/temporal/workflows/index.bundle.js +20 -68
  64. package/dist/temporal/workflows/job-execution-orchestration.d.ts +0 -3
  65. package/dist/temporal/workflows/job-execution-orchestration.d.ts.map +1 -1
  66. package/dist/temporal/workflows/job-execution-orchestration.js +17 -41
  67. package/dist/temporal/workflows/job-execution-orchestration.js.map +1 -1
  68. package/dist/temporal/workflows/job-listener-orchestration.d.ts +0 -3
  69. package/dist/temporal/workflows/job-listener-orchestration.d.ts.map +1 -1
  70. package/dist/temporal/workflows/job-listener-orchestration.js +1 -9
  71. package/dist/temporal/workflows/job-listener-orchestration.js.map +1 -1
  72. package/dist/temporal/workflows/run-orchestration.d.ts.map +1 -1
  73. package/dist/temporal/workflows/run-orchestration.js +2 -18
  74. package/dist/temporal/workflows/run-orchestration.js.map +1 -1
  75. package/dist/temporal/workflows/test-env.d.ts +4 -6
  76. package/dist/temporal/workflows/test-env.d.ts.map +1 -1
  77. package/dist/temporal/workflows/test-env.js +13 -24
  78. package/dist/temporal/workflows/test-env.js.map +1 -1
  79. package/dist/tsconfig.test.tsbuildinfo +1 -1
  80. package/package.json +4 -4
  81. package/src/core/errors.ts +17 -2
  82. package/src/core/job-execution.test.ts +54 -0
  83. package/src/core/job-execution.ts +6 -1
  84. package/src/core/step-config/agent.ts +8 -1
  85. package/src/core/step-config/complete-step-dispatch-config.test.ts +37 -0
  86. package/src/db/index.ts +1 -1
  87. package/src/db/job-listeners.test.ts +21 -0
  88. package/src/db/job-listeners.ts +18 -0
  89. package/src/db/workflow-runs/job-executions.test.ts +110 -12
  90. package/src/db/workflow-runs/job-executions.ts +54 -29
  91. package/src/db/workflow-runs/outbox.ts +63 -0
  92. package/src/db/workflow-runs/run-graph.ts +17 -0
  93. package/src/db/workflow-runs/run-status.test.ts +13 -0
  94. package/src/db/workflow-runs/run-status.ts +15 -1
  95. package/src/db/workflow-runs.ts +3 -1
  96. package/src/index.ts +1 -5
  97. package/src/presentation/dto/step.test.ts +8 -0
  98. package/src/presentation/dto/step.ts +9 -0
  99. package/src/presentation/index.ts +0 -1
  100. package/src/presentation/routes/agent-runtime-config.test.ts +84 -2
  101. package/src/presentation/routes/agent-runtime-config.ts +24 -0
  102. package/src/presentation/subscribers/index.ts +0 -1
  103. package/src/temporal/activities/index.ts +2 -8
  104. package/src/temporal/activities/orchestration-activities.test.ts +17 -1
  105. package/src/temporal/activities/orchestration-activities.ts +13 -46
  106. package/src/temporal/workflows/job-execution-orchestration.test.ts +6 -25
  107. package/src/temporal/workflows/job-execution-orchestration.ts +17 -43
  108. package/src/temporal/workflows/job-listener-orchestration-continuation.test.ts +0 -3
  109. package/src/temporal/workflows/job-listener-orchestration.test.ts +9 -12
  110. package/src/temporal/workflows/job-listener-orchestration.ts +0 -8
  111. package/src/temporal/workflows/run-orchestration.test.ts +22 -33
  112. package/src/temporal/workflows/run-orchestration.ts +4 -28
  113. package/src/temporal/workflows/test-env.ts +14 -34
  114. package/test/fixtures/runners-inter-module.ts +0 -3
  115. package/test/helpers/workflow-runs.ts +25 -0
  116. package/tsconfig.build.tsbuildinfo +1 -1
  117. package/dist/presentation/subscribers/on-runner-job-queued.d.ts +0 -3
  118. package/dist/presentation/subscribers/on-runner-job-queued.d.ts.map +0 -1
  119. package/dist/presentation/subscribers/on-runner-job-queued.js +0 -20
  120. package/dist/presentation/subscribers/on-runner-job-queued.js.map +0 -1
  121. package/src/presentation/subscribers/on-runner-job-queued.test.ts +0 -57
  122. package/src/presentation/subscribers/on-runner-job-queued.ts +0 -23
@@ -10,6 +10,7 @@ import {
10
10
  } from '../schema/job-executions.js';
11
11
  import {type JobCreateDb, type JobDb, jobs} from '../schema/jobs.js';
12
12
  import {type StepCreateDb, steps} from '../schema/steps.js';
13
+ import {writeJobExecutionTerminatedOutbox} from './outbox.js';
13
14
 
14
15
  type MaterializedRunGraphJobExecution = Omit<JobExecutionCreateDb, 'finishedAt' | 'jobId'> & {
15
16
  readonly finishedAt?: JobExecutionCreateDb['finishedAt'] | SQL | undefined;
@@ -59,6 +60,22 @@ export async function persistMaterializedRunGraph(
59
60
  ? []
60
61
  : await tx.insert(jobExecutions).values(jobExecutionValues).returning();
61
62
 
63
+ for (const jobExecution of jobExecutionRows) {
64
+ if (
65
+ jobExecution.status === 'succeeded' ||
66
+ jobExecution.status === 'failed' ||
67
+ jobExecution.status === 'cancelled'
68
+ ) {
69
+ await writeJobExecutionTerminatedOutbox(tx, {
70
+ jobId: jobExecution.jobId,
71
+ jobExecutionId: jobExecution.id,
72
+ status: jobExecution.status,
73
+ statusReason: jobExecution.statusReason,
74
+ statusReasonMessage: jobExecution.statusReasonMessage,
75
+ });
76
+ }
77
+ }
78
+
62
79
  const jobById = new Map(jobRows.map((job) => [job.id, job]));
63
80
  const materializedJobByJobId = new Map(
64
81
  jobRows.flatMap((jobRow, index) => {
@@ -4,6 +4,7 @@ import {nextStepForJob} from '#core/job-execution.js';
4
4
  import {
5
5
  buildModel,
6
6
  createTestRun,
7
+ jobExecutionTerminatedEvents,
7
8
  jobTerminatedEvents,
8
9
  runCancelledEvents,
9
10
  runTerminatedEvents,
@@ -16,6 +17,7 @@ import {
16
17
  cancelWorkflowRun,
17
18
  createRerunWorkflowRun,
18
19
  createWorkflowRun,
20
+ getFirstJobExecutionByJobId,
19
21
  getJobsByWorkflowRunId,
20
22
  getStepsByJobId,
21
23
  getWorkflowRunById,
@@ -257,6 +259,8 @@ describe('workflow run queries', () => {
257
259
  const [runningJobExecution, succeededJob, skippedJob] = await getJobsByWorkflowRunId(run.id);
258
260
  if (!runningJobExecution || !succeededJob || !skippedJob) throw new Error('Expected jobs');
259
261
  await updateJobStatus({jobId: runningJobExecution.id, status: 'running', expectedVersion: 1});
262
+ const execution = await getFirstJobExecutionByJobId(runningJobExecution.id);
263
+ if (!execution) throw new Error('Expected running job execution');
260
264
  await nextStepForJob(runningJobExecution.id);
261
265
  await updateJobStatus({jobId: succeededJob.id, status: 'succeeded', expectedVersion: 1});
262
266
  await updateJobStatus({
@@ -299,6 +303,15 @@ describe('workflow run queries', () => {
299
303
  statusReason: 'run_cancelled',
300
304
  }),
301
305
  ]);
306
+ expect(await jobExecutionTerminatedEvents(execution.id)).toEqual([
307
+ expect.objectContaining({
308
+ jobId: runningJobExecution.id,
309
+ jobExecutionId: execution.id,
310
+ workflowRunId: run.id,
311
+ status: 'cancelled',
312
+ statusReason: 'run_cancelled',
313
+ }),
314
+ ]);
302
315
  expect(await stepAttemptTerminatedEvents(runningJobExecution.id)).toHaveLength(1);
303
316
  expect(await jobTerminatedEvents(succeededJob.id)).toHaveLength(1);
304
317
  expect(await jobTerminatedEvents(skippedJob.id)).toHaveLength(1);
@@ -19,6 +19,7 @@ import {steps} from '../schema/steps.js';
19
19
  import {workflowRunAttempts} from '../schema/workflow-run-attempts.js';
20
20
  import {toWorkflowRun, workflowRuns} from '../schema/workflow-runs.js';
21
21
  import {updateJobStatusAtVersion} from './jobs.js';
22
+ import {writeJobExecutionTerminatedOutbox} from './outbox.js';
22
23
  import {
23
24
  lockWorkflowRun,
24
25
  TERMINAL_EXECUTION_STATUSES,
@@ -114,8 +115,21 @@ async function terminateRunAttempt(
114
115
  notInArray(jobExecutions.status, TERMINAL_EXECUTION_STATUSES),
115
116
  ),
116
117
  )
117
- .returning({id: jobExecutions.id});
118
+ .returning({
119
+ id: jobExecutions.id,
120
+ jobId: jobExecutions.jobId,
121
+ status: jobExecutions.status,
122
+ statusReason: jobExecutions.statusReason,
123
+ statusReasonMessage: jobExecutions.statusReasonMessage,
124
+ });
118
125
  for (const jobExecution of terminatedExecutions) {
126
+ await writeJobExecutionTerminatedOutbox(tx, {
127
+ jobId: jobExecution.jobId,
128
+ jobExecutionId: jobExecution.id,
129
+ status: jobExecution.status,
130
+ statusReason: jobExecution.statusReason,
131
+ statusReasonMessage: jobExecution.statusReasonMessage,
132
+ });
119
133
  await bulkUpdateStepStatuses(
120
134
  {jobExecutionId: jobExecution.id, status: spec.terminalStatus},
121
135
  tx,
@@ -9,7 +9,7 @@ export {
9
9
  getJobExecutionsByJobId,
10
10
  getJobExecutionsByWorkflowRunAttemptId,
11
11
  getLatestJobExecutionByJobId,
12
- recordJobExecutionQueuedAt,
12
+ queueJobExecution,
13
13
  recordJobExecutionStartedAt,
14
14
  resolveJobExecutionAfterLeaseExpiry,
15
15
  updateJobExecutionStatus,
@@ -35,6 +35,8 @@ export {
35
35
  updateJobStatusAtVersion,
36
36
  } from './workflow-runs/jobs.js';
37
37
  export {
38
+ writeJobExecutionQueuedOutbox,
39
+ writeJobExecutionTerminatedOutbox,
38
40
  writeJobStepsSettledOutbox,
39
41
  writeStepAttemptTerminatedOutbox,
40
42
  writeStepRestartEnqueuedOutbox,
package/src/index.ts CHANGED
@@ -9,7 +9,6 @@ import type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module'
9
9
  import {
10
10
  RUNNER_JOB_CLAIMED,
11
11
  RUNNER_JOB_LEASE_EXPIRED,
12
- RUNNER_JOB_QUEUED,
13
12
  type RunnersEventMap,
14
13
  } from '@shipfox/api-runners-dto';
15
14
  import type {RunnersInterModuleClient} from '@shipfox/api-runners-dto/inter-module';
@@ -35,7 +34,6 @@ import {
35
34
  onJobTerminatedFailureAnnotation,
36
35
  onRunnerJobClaimed,
37
36
  onRunnerJobLeaseExpired,
38
- onRunnerJobQueued,
39
37
  onStepAttemptTerminatedFailureAnnotation,
40
38
  onWorkflowRunAttemptCreated,
41
39
  onWorkflowRunCancelled,
@@ -128,15 +126,13 @@ export function createWorkflowsModule({
128
126
  ),
129
127
  subscriber(WORKFLOWS_JOB_TERMINATED, onJobTerminatedFailureAnnotation(annotations)),
130
128
  subscriber(RUNNER_JOB_LEASE_EXPIRED, onRunnerJobLeaseExpired),
131
- subscriber(RUNNER_JOB_QUEUED, onRunnerJobQueued),
132
129
  subscriber(RUNNER_JOB_CLAIMED, onRunnerJobClaimed),
133
130
  ],
134
131
  workers: [
135
132
  {
136
133
  taskQueue: WORKFLOWS_TASK_QUEUE,
137
134
  workflowsPath,
138
- activities: () =>
139
- createOrchestrationActivities({agent, integrations, projects, runners, secrets}),
135
+ activities: () => createOrchestrationActivities({agent, integrations, projects, secrets}),
140
136
  workflows: [],
141
137
  },
142
138
  ],
@@ -43,12 +43,16 @@ describe('fromStepErrorDto', () => {
43
43
  it('persists the machine-readable agent config issue in camelCase', () => {
44
44
  const persisted = fromStepErrorDto({
45
45
  message: 'Missing credentials',
46
+ code: 'workspace-providers-disabled',
47
+ managed_provider_id: 'shipfox',
46
48
  reason: 'agent_config_invalid',
47
49
  agent_config_issue: 'provider_not_configured',
48
50
  });
49
51
 
50
52
  expect(persisted).toEqual({
51
53
  message: 'Missing credentials',
54
+ code: 'workspace-providers-disabled',
55
+ managedProviderId: 'shipfox',
52
56
  reason: 'agent_config_invalid',
53
57
  agentConfigIssue: 'provider_not_configured',
54
58
  });
@@ -180,6 +184,8 @@ describe('toStepDto error category', () => {
180
184
  type: 'agent',
181
185
  error: {
182
186
  message: 'Unknown provider "foo" for agent step.',
187
+ code: 'workspace-providers-disabled',
188
+ managedProviderId: 'shipfox',
183
189
  reason: 'agent_config_invalid',
184
190
  agentConfigIssue: 'provider_unsupported',
185
191
  },
@@ -188,6 +194,8 @@ describe('toStepDto error category', () => {
188
194
 
189
195
  expect(dto.error).toEqual({
190
196
  message: 'Unknown provider "foo" for agent step.',
197
+ code: 'workspace-providers-disabled',
198
+ managed_provider_id: 'shipfox',
191
199
  reason: 'agent_config_invalid',
192
200
  agent_config_issue: 'provider_unsupported',
193
201
  category: 'user',
@@ -22,6 +22,9 @@ function toStepErrorDto(
22
22
  ): StepErrorDto {
23
23
  if (error === null) return null;
24
24
  const message = typeof error.message === 'string' ? error.message : '';
25
+ const code = typeof error.code === 'string' ? error.code : undefined;
26
+ const managedProviderId =
27
+ typeof error.managedProviderId === 'string' ? error.managedProviderId : undefined;
25
28
  const exitCode = error.exitCode;
26
29
  const signal = typeof error.signal === 'string' ? error.signal : undefined;
27
30
  const field = typeof error.field === 'string' ? error.field : undefined;
@@ -30,6 +33,8 @@ function toStepErrorDto(
30
33
  const agentConfigIssue = agentConfigIssueSchema.safeParse(error.agentConfigIssue);
31
34
  return {
32
35
  message,
36
+ ...(code === undefined ? {} : {code}),
37
+ ...(managedProviderId === undefined ? {} : {managed_provider_id: managedProviderId}),
33
38
  ...(exitCode === null || typeof exitCode === 'number' ? {exit_code: exitCode} : {}),
34
39
  ...(signal === undefined ? {} : {signal}),
35
40
  ...(reason.success ? {reason: reason.data} : {}),
@@ -48,6 +53,10 @@ export function fromStepErrorDto(error: StepErrorDto | undefined): Record<string
48
53
  if (!error) return null;
49
54
  return {
50
55
  message: error.message,
56
+ ...(error.code === undefined ? {} : {code: error.code}),
57
+ ...(error.managed_provider_id === undefined
58
+ ? {}
59
+ : {managedProviderId: error.managed_provider_id}),
51
60
  ...(error.exit_code === null || typeof error.exit_code === 'number'
52
61
  ? {exitCode: error.exit_code}
53
62
  : {}),
@@ -5,7 +5,6 @@ export {
5
5
  onJobTerminatedFailureAnnotation,
6
6
  onRunnerJobClaimed,
7
7
  onRunnerJobLeaseExpired,
8
- onRunnerJobQueued,
9
8
  onStepAttemptTerminatedFailureAnnotation,
10
9
  onWorkflowRunAttemptCreated,
11
10
  onWorkflowRunCancelled,
@@ -8,10 +8,15 @@ import {closeApp, createApp, type FastifyInstance} from '@shipfox/node-fastify';
8
8
  import {createCapturingLogger} from '@shipfox/node-log/test';
9
9
  import {eq} from 'drizzle-orm';
10
10
  import type {StepStatus} from '#core/entities/step.js';
11
- import {db} from '#db/db.js';
11
+ import {db, withTransaction} from '#db/db.js';
12
12
  import {jobs} from '#db/schema/jobs.js';
13
13
  import {steps as stepsTable} from '#db/schema/steps.js';
14
- import {createWorkflowRun, getJobsByWorkflowRunId, getStepsByJobId} from '#db/workflow-runs.js';
14
+ import {
15
+ createWorkflowRun,
16
+ getJobsByWorkflowRunId,
17
+ getStepsByJobId,
18
+ insertRunningStepAttempt,
19
+ } from '#db/workflow-runs.js';
15
20
  import {workflowModel} from '#test/factories/workflow-model.js';
16
21
  import {insertRunningJobLease, mintActiveLeaseToken} from '#test/fixtures/active-lease-token.js';
17
22
  import {resolveTestAgentDefaults} from '#test/fixtures/agent-inter-module.js';
@@ -115,6 +120,13 @@ describe('GET /runs/jobs/current/agent-runtime-config', () => {
115
120
  thinking: 'xhigh',
116
121
  credentials: {api_key: 'sk-workspace-secret'},
117
122
  });
123
+ expect(resolveRuntimeCredentials).toHaveBeenCalledWith(
124
+ expect.objectContaining({
125
+ workspaceId: run.workspaceId,
126
+ runId: run.id,
127
+ stepAttemptId: expect.any(String),
128
+ }),
129
+ );
118
130
  });
119
131
 
120
132
  test('derives the credential workspace from the leased job instead of hostile lease claims', async () => {
@@ -288,6 +300,63 @@ describe('GET /runs/jobs/current/agent-runtime-config', () => {
288
300
  expect(res.json().code).toBe('model-provider-not-configured');
289
301
  });
290
302
 
303
+ test('returns managed provider policy details when workspace providers are disabled', async () => {
304
+ const {job, step} = await createRunningAgentStep();
305
+ resolveRuntimeCredentials.mockRejectedValueOnce(
306
+ createInterModuleKnownError(
307
+ agentInterModuleContract.methods.resolveRuntimeCredentials,
308
+ 'workspace-providers-disabled',
309
+ {
310
+ message: 'This instance only supports provider `shipfox`.',
311
+ managed_provider_id: 'shipfox',
312
+ },
313
+ ),
314
+ );
315
+ const token = await mintActiveLeaseToken({jobId: job.id});
316
+
317
+ const res = await app.inject({
318
+ method: 'GET',
319
+ url: runtimeConfigUrl(step.id, step.currentAttempt),
320
+ headers: {authorization: `Bearer ${token}`},
321
+ });
322
+
323
+ expect(res.statusCode).toBe(422);
324
+ expect(res.json()).toMatchObject({
325
+ code: 'workspace-providers-disabled',
326
+ details: {
327
+ message: 'This instance only supports provider `shipfox`.',
328
+ managed_provider_id: 'shipfox',
329
+ },
330
+ });
331
+ });
332
+
333
+ test('includes a fallback message when managed provider details omit one', async () => {
334
+ const {job, step} = await createRunningAgentStep();
335
+ resolveRuntimeCredentials.mockRejectedValueOnce(
336
+ createInterModuleKnownError(
337
+ agentInterModuleContract.methods.resolveRuntimeCredentials,
338
+ 'workspace-providers-disabled',
339
+ {managed_provider_id: 'shipfox'},
340
+ ),
341
+ );
342
+ const token = await mintActiveLeaseToken({jobId: job.id});
343
+
344
+ const res = await app.inject({
345
+ method: 'GET',
346
+ url: runtimeConfigUrl(step.id, step.currentAttempt),
347
+ headers: {authorization: `Bearer ${token}`},
348
+ });
349
+
350
+ expect(res.statusCode).toBe(422);
351
+ expect(res.json()).toMatchObject({
352
+ code: 'workspace-providers-disabled',
353
+ details: {
354
+ message: 'Workspace provider configuration is disabled',
355
+ managed_provider_id: 'shipfox',
356
+ },
357
+ });
358
+ });
359
+
291
360
  test('returns 409 and reports when workspace credentials cannot be decrypted', async () => {
292
361
  const {job, step} = await createRunningAgentStep();
293
362
  resolveRuntimeCredentials.mockRejectedValueOnce(
@@ -420,6 +489,19 @@ async function createStep(params: {
420
489
  const step = stepRows.find((candidate) => candidate.type === params.targetType);
421
490
  if (!step) throw new Error(`createStep: ${params.targetType} step not found`);
422
491
  await db().update(stepsTable).set({status: params.status}).where(eq(stepsTable.id, step.id));
492
+ if (params.status === 'running') {
493
+ await withTransaction((tx) =>
494
+ insertRunningStepAttempt(
495
+ {
496
+ jobExecutionId: step.jobExecutionId,
497
+ stepId: step.id,
498
+ attempt: step.currentAttempt,
499
+ config: step.config,
500
+ },
501
+ tx,
502
+ ),
503
+ );
504
+ }
423
505
 
424
506
  return {
425
507
  run,
@@ -13,6 +13,7 @@ import {isInterModuleKnownError} from '@shipfox/inter-module';
13
13
  import {captureException} from '@shipfox/node-error-monitoring';
14
14
  import {ClientError, defineRoute} from '@shipfox/node-fastify';
15
15
  import {ZodError} from 'zod';
16
+ import {getStepAttemptDetail} from '#db/index.js';
16
17
  import {loadRunningLeasedStep} from './leased-step.js';
17
18
 
18
19
  export function createAgentRuntimeConfigRoute(params: {
@@ -63,6 +64,22 @@ export function createAgentRuntimeConfigRoute(params: {
63
64
  },
64
65
  );
65
66
  }
67
+ if (
68
+ isInterModuleKnownError(
69
+ agentInterModuleContract.methods.resolveRuntimeCredentials,
70
+ error,
71
+ ) &&
72
+ error.code === 'workspace-providers-disabled'
73
+ ) {
74
+ const message = error.details.message ?? 'Workspace provider configuration is disabled';
75
+ throw new ClientError(message, 'workspace-providers-disabled', {
76
+ status: 422,
77
+ details: {
78
+ managed_provider_id: error.details.managed_provider_id,
79
+ message,
80
+ },
81
+ });
82
+ }
66
83
  throw error;
67
84
  },
68
85
  handler: async (request, reply) => {
@@ -91,8 +108,15 @@ export function createAgentRuntimeConfigRoute(params: {
91
108
  throw error;
92
109
  }
93
110
 
111
+ const stepAttempt = await getStepAttemptDetail({stepId, attempt});
112
+ if (!stepAttempt) {
113
+ throw new ClientError('Step attempt not found', 'step-attempt-not-found', {status: 409});
114
+ }
115
+
94
116
  const runtimeConfig = await params.agent.resolveRuntimeCredentials({
95
117
  workspaceId,
118
+ runId: stepAttempt.workflowRunId,
119
+ stepAttemptId: stepAttempt.attempt.id,
96
120
  harness: agentConfig.harness,
97
121
  provider: agentConfig.provider,
98
122
  model: agentConfig.model,
@@ -6,6 +6,5 @@ export {onJobEventDelivered} from './on-job-event-delivered.js';
6
6
  export {onJobStepsSettled} from './on-job-steps-settled.js';
7
7
  export {onRunnerJobClaimed} from './on-runner-job-claimed.js';
8
8
  export {onRunnerJobLeaseExpired} from './on-runner-job-lease-expired.js';
9
- export {onRunnerJobQueued} from './on-runner-job-queued.js';
10
9
  export {onWorkflowRunAttemptCreated} from './on-workflow-run-attempt-created.js';
11
10
  export {onWorkflowRunCancelled} from './on-workflow-run-cancelled.js';
@@ -1,20 +1,17 @@
1
1
  import type {AgentInterModuleClient} from '@shipfox/api-agent-dto/inter-module';
2
2
  import type {IntegrationsModuleClient} from '@shipfox/api-integration-core-dto/inter-module';
3
3
  import type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module';
4
- import type {RunnersInterModuleClient} from '@shipfox/api-runners-dto/inter-module';
5
4
  import type {SecretsInterModuleClient} from '@shipfox/api-secrets-dto/inter-module';
6
5
  import {
7
6
  activateJobListenerActivity,
8
7
  bulkSetStepStatuses,
9
- createCancelRunnerJobsActivity,
10
8
  createDrainListenerEventsActivity,
11
- createEnqueueJobExecutionForRunner,
12
- createReleaseLeaseActivity,
13
9
  evaluateJobActivationsActivity,
14
10
  failJobExecutionAsTimedOutActivity,
15
11
  failRunAsTimedOutActivity,
16
12
  loadRunAttemptDag,
17
13
  peekListenerBufferActivity,
14
+ queueJobExecutionActivity,
18
15
  recordListenerFiringOutcomeActivity,
19
16
  resolveJobListenerActivity,
20
17
  resolveJobStatusFromJobExecutionsActivity,
@@ -29,7 +26,6 @@ export function createOrchestrationActivities(params: {
29
26
  agent: AgentInterModuleClient;
30
27
  integrations: IntegrationsModuleClient;
31
28
  projects: ProjectsModuleClient;
32
- runners: RunnersInterModuleClient;
33
29
  secrets: Pick<SecretsInterModuleClient, 'getVariablesByNamespace'>;
34
30
  }) {
35
31
  return {
@@ -39,8 +35,7 @@ export function createOrchestrationActivities(params: {
39
35
  setJobExecutionStatus: async (activityParams: Parameters<typeof setJobExecutionStatus>[0]) =>
40
36
  await setJobExecutionStatus(activityParams, params.secrets),
41
37
  bulkSetStepStatuses,
42
- cancelRunnerJobsActivity: createCancelRunnerJobsActivity(params.runners),
43
- enqueueJobExecutionForRunner: createEnqueueJobExecutionForRunner(params.runners),
38
+ queueJobExecutionActivity,
44
39
  evaluateJobActivationsActivity,
45
40
  failJobExecutionAsTimedOutActivity: async (
46
41
  activityParams: Parameters<typeof failJobExecutionAsTimedOutActivity>[0],
@@ -61,6 +56,5 @@ export function createOrchestrationActivities(params: {
61
56
  activityParams: Parameters<typeof resolveLeaseExpiredJobExecutionActivity>[0],
62
57
  ) => await resolveLeaseExpiredJobExecutionActivity(activityParams, params.secrets),
63
58
  resolveJobStatusFromJobExecutionsActivity,
64
- releaseLeaseActivity: createReleaseLeaseActivity(params.runners),
65
59
  };
66
60
  }
@@ -8,7 +8,11 @@ import {
8
8
  import {createTestSecretsClient} from '#test/fixtures/secrets-inter-module.js';
9
9
  import {stripSetupStep} from '#test/fixtures/strip-setup-step.js';
10
10
  import {workflowModel} from '#test/index.js';
11
- import {resolveLeaseExpiredJobExecutionActivity, setJobStatus} from './orchestration-activities.js';
11
+ import {
12
+ queueJobExecutionActivity,
13
+ resolveLeaseExpiredJobExecutionActivity,
14
+ setJobStatus,
15
+ } from './orchestration-activities.js';
12
16
 
13
17
  let workspaceId: string;
14
18
  let projectId: string;
@@ -74,6 +78,18 @@ describe('resolveLeaseExpiredJobExecutionActivity', () => {
74
78
  });
75
79
  });
76
80
 
81
+ describe('queueJobExecutionActivity', () => {
82
+ test('a missing execution fails non-retryably', async () => {
83
+ const error = await queueJobExecutionActivity({
84
+ jobId: crypto.randomUUID(),
85
+ jobExecutionId: crypto.randomUUID(),
86
+ }).catch((err: unknown) => err);
87
+
88
+ expect(error).toBeInstanceOf(ApplicationFailure);
89
+ expect((error as ApplicationFailure).nonRetryable).toBe(true);
90
+ });
91
+ });
92
+
77
93
  describe('setJobStatus', () => {
78
94
  test('default-gate skipped jobs record an evaluation trace', async () => {
79
95
  const run = await createWorkflowRun({
@@ -1,12 +1,7 @@
1
1
  import type {AgentInterModuleClient} from '@shipfox/api-agent-dto/inter-module';
2
2
  import type {IntegrationsModuleClient} from '@shipfox/api-integration-core-dto/inter-module';
3
3
  import type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module';
4
- import {
5
- type RunnersInterModuleClient,
6
- runnersInterModuleContract,
7
- } from '@shipfox/api-runners-dto/inter-module';
8
4
  import type {SecretsInterModuleClient} from '@shipfox/api-secrets-dto/inter-module';
9
- import {isInterModuleKnownError} from '@shipfox/inter-module';
10
5
  import {ApplicationFailure} from '@temporalio/common';
11
6
  import {defaultJobConditionTrace} from '#core/condition-trace.js';
12
7
  import type {JobStatus, JobStatusReason, ResolutionReason} from '#core/entities/job.js';
@@ -31,6 +26,7 @@ import {
31
26
  getWorkflowRunByAttemptId,
32
27
  type JobActivationDecision,
33
28
  peekListenerBuffer,
29
+ queueJobExecution,
34
30
  resolveJobExecutionAfterLeaseExpiry,
35
31
  resolveJobListener,
36
32
  resolveJobStatusFromJobExecutions,
@@ -209,48 +205,19 @@ export async function resolveLeaseExpiredJobExecutionActivity(
209
205
  }
210
206
  }
211
207
 
212
- // Best-effort lease cleanup on job finalization. Idempotent: deleting an
213
- // already-released (or already-reaped) lease is a no-op. The workflow wraps the
214
- // call so a persistent failure never blocks the DAG result.
215
- export function createReleaseLeaseActivity(runners: RunnersInterModuleClient) {
216
- return async function releaseLeaseActivity(params: {jobExecutionId: string}): Promise<void> {
217
- await runners.releaseJobExecution({jobExecutionId: params.jobExecutionId});
218
- };
219
- }
220
-
221
- export function createEnqueueJobExecutionForRunner(runners: RunnersInterModuleClient) {
222
- return async function enqueueJobExecutionForRunner(params: {
223
- workspaceId: string;
224
- workflowRunId: string;
225
- jobId: string;
226
- jobExecutionId: string;
227
- runAttemptId: string;
228
- projectId: string;
229
- requiredLabels: string[];
230
- }): Promise<void> {
231
- try {
232
- await runners.enqueueJobExecution({
233
- workspaceId: params.workspaceId,
234
- workflowRunId: params.workflowRunId,
235
- workflowRunAttemptId: params.runAttemptId,
236
- jobId: params.jobId,
237
- jobExecutionId: params.jobExecutionId,
238
- projectId: params.projectId,
239
- requiredLabels: params.requiredLabels,
240
- });
241
- } catch (err) {
242
- if (isInterModuleKnownError(runnersInterModuleContract.methods.enqueueJobExecution, err)) {
243
- throw ApplicationFailure.nonRetryable(err.message, err.name);
244
- }
245
- throw err;
208
+ export async function queueJobExecutionActivity(params: {
209
+ jobId: string;
210
+ jobExecutionId: string;
211
+ }): Promise<{newVersion: number; status: Exclude<JobStatus, 'skipped'>}> {
212
+ try {
213
+ const execution = await queueJobExecution({jobExecutionId: params.jobExecutionId});
214
+ return {newVersion: execution.version, status: execution.status};
215
+ } catch (err) {
216
+ if (err instanceof JobNotFoundError) {
217
+ throw ApplicationFailure.nonRetryable(err.message, err.name);
246
218
  }
247
- };
248
- }
249
-
250
- export function createCancelRunnerJobsActivity(runners: RunnersInterModuleClient) {
251
- return async function cancelRunnerJobsActivity(params: {jobIds: string[]}): Promise<void> {
252
- await runners.cancelJobs(params);
253
- };
219
+ throw err;
220
+ }
254
221
  }
255
222
 
256
223
  export async function failJobExecutionAsTimedOutActivity(
@@ -107,7 +107,7 @@ async function expectEmptyRequiredLabelsFailure(input: typeof defaultJobInput):
107
107
  }
108
108
 
109
109
  describe('jobExecutionOrchestration', () => {
110
- test('runner claim flips status to running, then finished releases the lease', async () => {
110
+ test('runner claim flips status to running, then the terminal fact finishes the execution', async () => {
111
111
  setCfg({
112
112
  dag: makeDag([dagJob('job-1', 'build')]),
113
113
  jobResults: new Map([['job-1', 'succeeded']]),
@@ -117,7 +117,6 @@ describe('jobExecutionOrchestration', () => {
117
117
 
118
118
  expect(result.status).toBe('succeeded');
119
119
  expect(finalStatusesFor('job-1')).toEqual(['running', 'succeeded']);
120
- expect(callsNamed('releaseLeaseActivity')).toHaveLength(1);
121
120
  expect(callsNamed('resolveLeaseExpiredJobExecutionActivity')).toHaveLength(0);
122
121
  expect(callsNamed('bulkSetStepStatuses')).toHaveLength(0);
123
122
  });
@@ -238,7 +237,7 @@ describe('jobExecutionOrchestration', () => {
238
237
  });
239
238
 
240
239
  expect(setExecutionStatusCalls()).toHaveLength(0);
241
- expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(0);
240
+ expect(callsNamed('queueJobExecutionActivity')).toHaveLength(0);
242
241
  });
243
242
 
244
243
  test('whitespace-only required labels fail before the job is marked running', async () => {
@@ -254,7 +253,7 @@ describe('jobExecutionOrchestration', () => {
254
253
  });
255
254
 
256
255
  expect(setExecutionStatusCalls()).toHaveLength(0);
257
- expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(0);
256
+ expect(callsNamed('queueJobExecutionActivity')).toHaveLength(0);
258
257
  });
259
258
 
260
259
  test('finished signal (failed) flips status without sweeping steps', async () => {
@@ -265,7 +264,6 @@ describe('jobExecutionOrchestration', () => {
265
264
  expect(result.status).toBe('failed');
266
265
  expect(finalStatusesFor('job-2')).toEqual(['running', 'failed']);
267
266
  expect(terminalSetJobCall('job-2')?.params.statusReason).toBe('step_failed');
268
- expect(callsNamed('releaseLeaseActivity')).toHaveLength(1);
269
267
  expect(callsNamed('bulkSetStepStatuses')).toHaveLength(0);
270
268
  });
271
269
 
@@ -289,10 +287,9 @@ describe('jobExecutionOrchestration', () => {
289
287
  statusReason: 'step_failed',
290
288
  },
291
289
  });
292
- expect(callsNamed('releaseLeaseActivity')).toHaveLength(1);
293
290
  });
294
291
 
295
- test('lease-expired signal resolves via the activity and releases the lease', async () => {
292
+ test('lease-expired signal resolves via the activity', async () => {
296
293
  setCfg({
297
294
  dag: makeDag([]),
298
295
  jobResults: new Map(),
@@ -304,7 +301,6 @@ describe('jobExecutionOrchestration', () => {
304
301
 
305
302
  expect(result.status).toBe('failed');
306
303
  expect(callsNamed('resolveLeaseExpiredJobExecutionActivity')).toHaveLength(1);
307
- expect(callsNamed('releaseLeaseActivity')).toHaveLength(1);
308
304
  expect(callsNamed('failJobExecutionAsTimedOutActivity')).toHaveLength(0);
309
305
  });
310
306
 
@@ -400,24 +396,10 @@ describe('jobExecutionOrchestration', () => {
400
396
  const result = await executeJob({...defaultJobInput, jobId: 'job-cancelled'});
401
397
 
402
398
  expect(result).toMatchObject({status: 'cancelled'});
403
- expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(1);
404
- expect(callsNamed('releaseLeaseActivity')).toHaveLength(0);
399
+ expect(callsNamed('queueJobExecutionActivity')).toHaveLength(1);
405
400
  });
406
401
 
407
- test('releaseLease failure does not block the result (best-effort)', async () => {
408
- setCfg({
409
- dag: makeDag([]),
410
- jobResults: new Map([['job-rl', 'succeeded']]),
411
- releaseLeaseError: 'runners db down',
412
- });
413
-
414
- const result = await executeJob({...defaultJobInput, jobId: 'job-rl'});
415
-
416
- expect(result.status).toBe('succeeded');
417
- expect(callsNamed('releaseLeaseActivity').length).toBeGreaterThanOrEqual(1);
418
- });
419
-
420
- test('times out after the execution deadline and fails its steps without releasing the lease', async () => {
402
+ test('times out after the execution deadline and fails its steps', async () => {
421
403
  setCfg({
422
404
  dag: makeDag([dagJob('job-timeout', 'build')]),
423
405
  jobResults: new Map(),
@@ -437,7 +419,6 @@ describe('jobExecutionOrchestration', () => {
437
419
  name: 'bulkSetStepStatuses',
438
420
  params: {jobExecutionId: 'job-timeout', status: 'failed'},
439
421
  });
440
- expect(callsNamed('releaseLeaseActivity')).toHaveLength(0);
441
422
  expect(callsNamed('resolveLeaseExpiredJobExecutionActivity')).toHaveLength(0);
442
423
  });
443
424