@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.
- package/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +24 -0
- package/dist/core/errors.d.ts +9 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +5 -1
- package/dist/core/errors.js.map +1 -1
- package/dist/core/job-execution.d.ts.map +1 -1
- package/dist/core/job-execution.js +12 -2
- package/dist/core/job-execution.js.map +1 -1
- package/dist/core/step-config/agent.d.ts.map +1 -1
- package/dist/core/step-config/agent.js +9 -1
- package/dist/core/step-config/agent.js.map +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/job-listeners.d.ts.map +1 -1
- package/dist/db/job-listeners.js +17 -1
- package/dist/db/job-listeners.js.map +1 -1
- package/dist/db/workflow-runs/job-executions.d.ts +2 -3
- package/dist/db/workflow-runs/job-executions.d.ts.map +1 -1
- package/dist/db/workflow-runs/job-executions.js +48 -20
- package/dist/db/workflow-runs/job-executions.js.map +1 -1
- package/dist/db/workflow-runs/outbox.d.ts +15 -0
- package/dist/db/workflow-runs/outbox.d.ts.map +1 -1
- package/dist/db/workflow-runs/outbox.js +35 -1
- package/dist/db/workflow-runs/outbox.js.map +1 -1
- package/dist/db/workflow-runs/run-graph.d.ts.map +1 -1
- package/dist/db/workflow-runs/run-graph.js +12 -0
- package/dist/db/workflow-runs/run-graph.js.map +1 -1
- package/dist/db/workflow-runs/run-status.d.ts.map +1 -1
- package/dist/db/workflow-runs/run-status.js +13 -1
- package/dist/db/workflow-runs/run-status.js.map +1 -1
- package/dist/db/workflow-runs.d.ts +2 -2
- package/dist/db/workflow-runs.d.ts.map +1 -1
- package/dist/db/workflow-runs.js +2 -2
- package/dist/db/workflow-runs.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/dist/presentation/dto/step.d.ts.map +1 -1
- package/dist/presentation/dto/step.js +14 -0
- package/dist/presentation/dto/step.js.map +1 -1
- package/dist/presentation/index.d.ts +1 -1
- package/dist/presentation/index.d.ts.map +1 -1
- package/dist/presentation/index.js +1 -1
- package/dist/presentation/index.js.map +1 -1
- package/dist/presentation/routes/agent-runtime-config.d.ts.map +1 -1
- package/dist/presentation/routes/agent-runtime-config.js +22 -0
- package/dist/presentation/routes/agent-runtime-config.js.map +1 -1
- package/dist/presentation/subscribers/index.d.ts +0 -1
- package/dist/presentation/subscribers/index.d.ts.map +1 -1
- package/dist/presentation/subscribers/index.js +0 -1
- package/dist/presentation/subscribers/index.js.map +1 -1
- package/dist/temporal/activities/index.d.ts +2 -18
- package/dist/temporal/activities/index.d.ts.map +1 -1
- package/dist/temporal/activities/index.js +3 -5
- package/dist/temporal/activities/index.js.map +1 -1
- package/dist/temporal/activities/orchestration-activities.d.ts +5 -14
- package/dist/temporal/activities/orchestration-activities.d.ts.map +1 -1
- package/dist/temporal/activities/orchestration-activities.js +13 -34
- package/dist/temporal/activities/orchestration-activities.js.map +1 -1
- package/dist/temporal/workflows/index.bundle.js +20 -68
- package/dist/temporal/workflows/job-execution-orchestration.d.ts +0 -3
- package/dist/temporal/workflows/job-execution-orchestration.d.ts.map +1 -1
- package/dist/temporal/workflows/job-execution-orchestration.js +17 -41
- package/dist/temporal/workflows/job-execution-orchestration.js.map +1 -1
- package/dist/temporal/workflows/job-listener-orchestration.d.ts +0 -3
- package/dist/temporal/workflows/job-listener-orchestration.d.ts.map +1 -1
- package/dist/temporal/workflows/job-listener-orchestration.js +1 -9
- package/dist/temporal/workflows/job-listener-orchestration.js.map +1 -1
- package/dist/temporal/workflows/run-orchestration.d.ts.map +1 -1
- package/dist/temporal/workflows/run-orchestration.js +2 -18
- package/dist/temporal/workflows/run-orchestration.js.map +1 -1
- package/dist/temporal/workflows/test-env.d.ts +4 -6
- package/dist/temporal/workflows/test-env.d.ts.map +1 -1
- package/dist/temporal/workflows/test-env.js +13 -24
- package/dist/temporal/workflows/test-env.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/core/errors.ts +17 -2
- package/src/core/job-execution.test.ts +54 -0
- package/src/core/job-execution.ts +6 -1
- package/src/core/step-config/agent.ts +8 -1
- package/src/core/step-config/complete-step-dispatch-config.test.ts +37 -0
- package/src/db/index.ts +1 -1
- package/src/db/job-listeners.test.ts +21 -0
- package/src/db/job-listeners.ts +18 -0
- package/src/db/workflow-runs/job-executions.test.ts +110 -12
- package/src/db/workflow-runs/job-executions.ts +54 -29
- package/src/db/workflow-runs/outbox.ts +63 -0
- package/src/db/workflow-runs/run-graph.ts +17 -0
- package/src/db/workflow-runs/run-status.test.ts +13 -0
- package/src/db/workflow-runs/run-status.ts +15 -1
- package/src/db/workflow-runs.ts +3 -1
- package/src/index.ts +1 -5
- package/src/presentation/dto/step.test.ts +8 -0
- package/src/presentation/dto/step.ts +9 -0
- package/src/presentation/index.ts +0 -1
- package/src/presentation/routes/agent-runtime-config.test.ts +84 -2
- package/src/presentation/routes/agent-runtime-config.ts +24 -0
- package/src/presentation/subscribers/index.ts +0 -1
- package/src/temporal/activities/index.ts +2 -8
- package/src/temporal/activities/orchestration-activities.test.ts +17 -1
- package/src/temporal/activities/orchestration-activities.ts +13 -46
- package/src/temporal/workflows/job-execution-orchestration.test.ts +6 -25
- package/src/temporal/workflows/job-execution-orchestration.ts +17 -43
- package/src/temporal/workflows/job-listener-orchestration-continuation.test.ts +0 -3
- package/src/temporal/workflows/job-listener-orchestration.test.ts +9 -12
- package/src/temporal/workflows/job-listener-orchestration.ts +0 -8
- package/src/temporal/workflows/run-orchestration.test.ts +22 -33
- package/src/temporal/workflows/run-orchestration.ts +4 -28
- package/src/temporal/workflows/test-env.ts +14 -34
- package/test/fixtures/runners-inter-module.ts +0 -3
- package/test/helpers/workflow-runs.ts +25 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/presentation/subscribers/on-runner-job-queued.d.ts +0 -3
- package/dist/presentation/subscribers/on-runner-job-queued.d.ts.map +0 -1
- package/dist/presentation/subscribers/on-runner-job-queued.js +0 -20
- package/dist/presentation/subscribers/on-runner-job-queued.js.map +0 -1
- package/src/presentation/subscribers/on-runner-job-queued.test.ts +0 -57
- 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({
|
|
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,
|
package/src/db/workflow-runs.ts
CHANGED
|
@@ -9,7 +9,7 @@ export {
|
|
|
9
9
|
getJobExecutionsByJobId,
|
|
10
10
|
getJobExecutionsByWorkflowRunAttemptId,
|
|
11
11
|
getLatestJobExecutionByJobId,
|
|
12
|
-
|
|
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
|
: {}),
|
|
@@ -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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
await
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
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('
|
|
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('
|
|
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
|
|
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('
|
|
404
|
-
expect(callsNamed('releaseLeaseActivity')).toHaveLength(0);
|
|
399
|
+
expect(callsNamed('queueJobExecutionActivity')).toHaveLength(1);
|
|
405
400
|
});
|
|
406
401
|
|
|
407
|
-
test('
|
|
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
|
|