@shipfox/api-workflows 12.1.0 → 12.2.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 +4 -4
- package/CHANGELOG.md +29 -0
- package/dist/core/entities/job.d.ts +1 -1
- package/dist/core/entities/job.d.ts.map +1 -1
- package/dist/core/entities/step.d.ts +2 -2
- package/dist/core/entities/step.d.ts.map +1 -1
- package/dist/core/errors.d.ts +16 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +18 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/step-config/assemble-run-context.d.ts +15 -5
- package/dist/core/step-config/assemble-run-context.d.ts.map +1 -1
- package/dist/core/step-config/assemble-run-context.js +77 -27
- package/dist/core/step-config/assemble-run-context.js.map +1 -1
- package/dist/core/step-config/fields.d.ts +5 -0
- package/dist/core/step-config/fields.d.ts.map +1 -1
- package/dist/core/step-config/fields.js +21 -3
- package/dist/core/step-config/fields.js.map +1 -1
- package/dist/core/step-config/index.d.ts +1 -1
- package/dist/core/step-config/index.d.ts.map +1 -1
- package/dist/core/step-config/index.js +1 -1
- package/dist/core/step-config/index.js.map +1 -1
- package/dist/core/step-config/job-output-limits.d.ts +11 -0
- package/dist/core/step-config/job-output-limits.d.ts.map +1 -0
- package/dist/core/step-config/job-output-limits.js +88 -0
- package/dist/core/step-config/job-output-limits.js.map +1 -0
- package/dist/core/step-config/materialize-workflow-model.d.ts +1 -1
- package/dist/core/step-config/materialize-workflow-model.d.ts.map +1 -1
- package/dist/core/step-config/materialize-workflow-model.js +41 -14
- package/dist/core/step-config/materialize-workflow-model.js.map +1 -1
- package/dist/db/db.d.ts +50 -50
- package/dist/db/job-listeners.d.ts.map +1 -1
- package/dist/db/job-listeners.js +6 -5
- package/dist/db/job-listeners.js.map +1 -1
- package/dist/db/schema/job-executions.d.ts +3 -3
- package/dist/db/schema/job-listener-events.d.ts +1 -1
- package/dist/db/schema/jobs.d.ts +7 -7
- package/dist/db/schema/outbox.d.ts +1 -1
- package/dist/db/schema/step-attempts.d.ts +2 -2
- package/dist/db/schema/steps.d.ts +5 -5
- package/dist/db/schema/workflow-run-attempts.d.ts +3 -3
- package/dist/db/schema/workflow-run-counters.d.ts +1 -1
- package/dist/db/schema/workflow-runs.d.ts +2 -2
- package/dist/db/workflow-runs/job-executions.d.ts.map +1 -1
- package/dist/db/workflow-runs/job-executions.js +4 -2
- package/dist/db/workflow-runs/job-executions.js.map +1 -1
- package/dist/db/workflow-runs/jobs.d.ts.map +1 -1
- package/dist/db/workflow-runs/jobs.js +29 -3
- package/dist/db/workflow-runs/jobs.js.map +1 -1
- package/dist/presentation/routes/project-access.d.ts +1 -1
- package/dist/temporal/activities/index.d.ts +1 -1
- package/dist/temporal/activities/index.d.ts.map +1 -1
- package/dist/temporal/activities/orchestration-activities.d.ts +1 -1
- package/dist/temporal/activities/orchestration-activities.d.ts.map +1 -1
- package/dist/temporal/workflows/test-env.d.ts +2 -2
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +16 -16
- package/src/core/errors.ts +35 -0
- package/src/core/step-config/assemble-run-context.test.ts +232 -20
- package/src/core/step-config/assemble-run-context.ts +126 -28
- package/src/core/step-config/fields.ts +42 -3
- package/src/core/step-config/index.ts +2 -0
- package/src/core/step-config/job-output-limits.ts +130 -0
- package/src/core/step-config/materialize-workflow-model.test.ts +306 -1
- package/src/core/step-config/materialize-workflow-model.ts +60 -15
- package/src/db/job-listeners.ts +14 -3
- package/src/db/workflow-runs/job-executions.test.ts +207 -1
- package/src/db/workflow-runs/job-executions.ts +15 -2
- package/src/db/workflow-runs/jobs.test.ts +49 -0
- package/src/db/workflow-runs/jobs.ts +46 -6
- package/src/db/workflow-runs/run-create.test.ts +56 -0
- package/test/factories/workflow-model.ts +2 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -4,7 +4,13 @@ import {WORKFLOWS_JOB_EXECUTION_TIMED_OUT} from '@shipfox/api-workflows-dto';
|
|
|
4
4
|
import {and, asc, desc, eq, isNull, notInArray, sql} from 'drizzle-orm';
|
|
5
5
|
import type {JobStatusReason} from '#core/entities/job.js';
|
|
6
6
|
import type {JobExecution, JobExecutionStatus} from '#core/entities/job-execution.js';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
InterpolationUnresolvableError,
|
|
9
|
+
JobNotFoundError,
|
|
10
|
+
JobOutputNotJsonSafeError,
|
|
11
|
+
JobOutputTooLargeError,
|
|
12
|
+
JobOutputTooManyEntriesError,
|
|
13
|
+
} from '#core/errors.js';
|
|
8
14
|
import {deriveJobExecutionOutputs} from '#core/job-transition/index.js';
|
|
9
15
|
import {deriveCompletion, isTerminal} from '#core/step-transition/decide-step-transition.js';
|
|
10
16
|
import type {RuntimeCompletionStatus} from '#core/workflow-scheduling/runtime-dag.js';
|
|
@@ -207,7 +213,14 @@ async function updateJobExecutionStatusAtVersion(
|
|
|
207
213
|
secrets: params.secrets,
|
|
208
214
|
});
|
|
209
215
|
} catch (error) {
|
|
210
|
-
if (
|
|
216
|
+
if (
|
|
217
|
+
!(error instanceof InterpolationUnresolvableError) &&
|
|
218
|
+
!(error instanceof JobOutputNotJsonSafeError) &&
|
|
219
|
+
!(error instanceof JobOutputTooLargeError) &&
|
|
220
|
+
!(error instanceof JobOutputTooManyEntriesError)
|
|
221
|
+
) {
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
211
224
|
status = 'failed';
|
|
212
225
|
statusReason = 'unknown';
|
|
213
226
|
outputs = null;
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
buildModel,
|
|
4
4
|
conditionTrace,
|
|
5
5
|
jobByKey,
|
|
6
|
+
template,
|
|
6
7
|
workflowRunAttemptId,
|
|
7
8
|
} from '#test/helpers/workflow-runs.js';
|
|
8
9
|
import {db} from '../db.js';
|
|
@@ -148,6 +149,54 @@ describe('evaluateJobActivations', () => {
|
|
|
148
149
|
expect(result).toEqual([{kind: 'start-job', jobId: notify.id}]);
|
|
149
150
|
});
|
|
150
151
|
|
|
152
|
+
it('rehydrates typed dependency outputs before activation evaluation', async () => {
|
|
153
|
+
const run = await createWorkflowRun({
|
|
154
|
+
...scope,
|
|
155
|
+
name: 'typed dependency outputs',
|
|
156
|
+
model: buildModel({
|
|
157
|
+
jobs: {
|
|
158
|
+
build: {
|
|
159
|
+
steps: [{key: 'collect', run: 'npm run build'}],
|
|
160
|
+
outputs: {
|
|
161
|
+
count: template('steps.collect.outputs.count'),
|
|
162
|
+
createdAt: template('run.created_at'),
|
|
163
|
+
},
|
|
164
|
+
outputTypes: {count: 'int', createdAt: 'timestamp'},
|
|
165
|
+
},
|
|
166
|
+
notify: {
|
|
167
|
+
needs: 'build',
|
|
168
|
+
if: 'jobs.build.outputs.count + 1 == 43 && jobs.build.outputs.createdAt < timestamp("2026-07-01T00:00:00Z")',
|
|
169
|
+
steps: [{run: './notify.sh'}],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
}),
|
|
173
|
+
triggerPayload: {
|
|
174
|
+
source: 'manual',
|
|
175
|
+
event: 'fire',
|
|
176
|
+
subscriptionId: crypto.randomUUID(),
|
|
177
|
+
userId: crypto.randomUUID(),
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
const build = await jobByKey(run.id, 'build');
|
|
181
|
+
const notify = await jobByKey(run.id, 'notify');
|
|
182
|
+
await updateJobStatus({
|
|
183
|
+
jobId: build.id,
|
|
184
|
+
status: 'succeeded',
|
|
185
|
+
expectedVersion: build.version,
|
|
186
|
+
});
|
|
187
|
+
await db()
|
|
188
|
+
.update(jobs)
|
|
189
|
+
.set({outputs: {count: '42', createdAt: '2026-06-30T12:00:00.000Z'}})
|
|
190
|
+
.where(eq(jobs.id, build.id));
|
|
191
|
+
|
|
192
|
+
const result = await evaluateJobActivations({
|
|
193
|
+
runAttemptId: await workflowRunAttemptId(run.id),
|
|
194
|
+
jobs: [{jobId: notify.id, expectedVersion: notify.version}],
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(result).toEqual([{kind: 'start-job', jobId: notify.id}]);
|
|
198
|
+
});
|
|
199
|
+
|
|
151
200
|
it('records condition_errored when predicate evaluation fails closed', async () => {
|
|
152
201
|
const run = await createWorkflowRun({
|
|
153
202
|
...scope,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {readPersistedWorkflowModel} from '@shipfox/api-definitions-dto';
|
|
2
2
|
import {WORKFLOWS_JOB_TERMINATED} from '@shipfox/api-workflows-dto';
|
|
3
|
+
import type {ExpressionType} from '@shipfox/expression';
|
|
3
4
|
import {and, asc, desc, eq, inArray, notInArray, sql} from 'drizzle-orm';
|
|
4
5
|
import {isJobTerminal, type Job, type JobStatus, type JobStatusReason} from '#core/entities/job.js';
|
|
5
6
|
import type {JobExecution} from '#core/entities/job-execution.js';
|
|
@@ -24,6 +25,16 @@ import {toWorkflowRun, workflowRuns} from '../schema/workflow-runs.js';
|
|
|
24
25
|
import {getWorkflowRunById} from './queries.js';
|
|
25
26
|
import {getWorkflowContextForJob, optimisticLockRetry, TERMINAL_JOB_STATUSES} from './shared.js';
|
|
26
27
|
|
|
28
|
+
function outputTypesByJobKey(
|
|
29
|
+
model: ReturnType<typeof readPersistedWorkflowModel> | null,
|
|
30
|
+
): ReadonlyMap<string, Readonly<Record<string, ExpressionType>>> {
|
|
31
|
+
return new Map(
|
|
32
|
+
(model?.jobs ?? []).flatMap((job) =>
|
|
33
|
+
job.outputTypes === undefined ? [] : [[job.key, job.outputTypes] as const],
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
export async function getJobsByWorkflowRunAttemptId(workflowRunAttemptId: string): Promise<Job[]> {
|
|
28
39
|
const rows = await db()
|
|
29
40
|
.select()
|
|
@@ -81,9 +92,17 @@ export async function getDirectDependencyJobContexts(
|
|
|
81
92
|
jobId: string,
|
|
82
93
|
tx?: Tx,
|
|
83
94
|
): Promise<JobContextInput[]> {
|
|
84
|
-
const targetRows = await (tx ?? db())
|
|
95
|
+
const targetRows = await (tx ?? db())
|
|
96
|
+
.select({job: jobs, attempt: workflowRunAttempts})
|
|
97
|
+
.from(jobs)
|
|
98
|
+
.innerJoin(workflowRunAttempts, eq(jobs.workflowRunAttemptId, workflowRunAttempts.id))
|
|
99
|
+
.where(eq(jobs.id, jobId))
|
|
100
|
+
.limit(1);
|
|
85
101
|
const target = targetRows[0];
|
|
86
|
-
if (!target || target.dependencies.length === 0) return [];
|
|
102
|
+
if (!target || target.job.dependencies.length === 0) return [];
|
|
103
|
+
const model =
|
|
104
|
+
target.attempt.model === null ? null : readPersistedWorkflowModel(target.attempt.model);
|
|
105
|
+
const outputTypes = outputTypesByJobKey(model);
|
|
87
106
|
|
|
88
107
|
const rows = await (tx ?? db())
|
|
89
108
|
.select({job: jobs, execution: jobExecutions})
|
|
@@ -91,8 +110,8 @@ export async function getDirectDependencyJobContexts(
|
|
|
91
110
|
.leftJoin(jobExecutions, eq(jobExecutions.jobId, jobs.id))
|
|
92
111
|
.where(
|
|
93
112
|
and(
|
|
94
|
-
eq(jobs.workflowRunAttemptId, target.workflowRunAttemptId),
|
|
95
|
-
inArray(jobs.key, target.dependencies),
|
|
113
|
+
eq(jobs.workflowRunAttemptId, target.job.workflowRunAttemptId),
|
|
114
|
+
inArray(jobs.key, target.job.dependencies),
|
|
96
115
|
),
|
|
97
116
|
)
|
|
98
117
|
.orderBy(asc(jobs.position), asc(jobs.id), asc(jobExecutions.sequence), asc(jobExecutions.id));
|
|
@@ -101,7 +120,12 @@ export async function getDirectDependencyJobContexts(
|
|
|
101
120
|
for (const row of rows) {
|
|
102
121
|
let context = contextsByJobId.get(row.job.id);
|
|
103
122
|
if (!context) {
|
|
104
|
-
|
|
123
|
+
const jobOutputTypes = outputTypes.get(row.job.key);
|
|
124
|
+
context = {
|
|
125
|
+
job: toJob(row.job),
|
|
126
|
+
...(jobOutputTypes === undefined ? {} : {outputTypes: jobOutputTypes}),
|
|
127
|
+
executions: [],
|
|
128
|
+
};
|
|
105
129
|
contextsByJobId.set(row.job.id, context);
|
|
106
130
|
}
|
|
107
131
|
if (row.execution)
|
|
@@ -243,6 +267,17 @@ async function directDependencyContextsByJobKey(
|
|
|
243
267
|
const dependencyKeys = new Set(targetJobs.flatMap((job) => job.dependencies));
|
|
244
268
|
if (dependencyKeys.size === 0) return new Map();
|
|
245
269
|
|
|
270
|
+
const [attempt] = await tx
|
|
271
|
+
.select({model: workflowRunAttempts.model})
|
|
272
|
+
.from(workflowRunAttempts)
|
|
273
|
+
.where(eq(workflowRunAttempts.id, runAttemptId))
|
|
274
|
+
.limit(1);
|
|
275
|
+
const model =
|
|
276
|
+
attempt === undefined || attempt.model === null
|
|
277
|
+
? null
|
|
278
|
+
: readPersistedWorkflowModel(attempt.model);
|
|
279
|
+
const outputTypes = outputTypesByJobKey(model);
|
|
280
|
+
|
|
246
281
|
const rows = await tx
|
|
247
282
|
.select({job: jobs, execution: jobExecutions})
|
|
248
283
|
.from(jobs)
|
|
@@ -254,7 +289,12 @@ async function directDependencyContextsByJobKey(
|
|
|
254
289
|
for (const row of rows) {
|
|
255
290
|
let context = contextsByJobKey.get(row.job.key);
|
|
256
291
|
if (!context) {
|
|
257
|
-
|
|
292
|
+
const jobOutputTypes = outputTypes.get(row.job.key);
|
|
293
|
+
context = {
|
|
294
|
+
job: toJob(row.job),
|
|
295
|
+
...(jobOutputTypes === undefined ? {} : {outputTypes: jobOutputTypes}),
|
|
296
|
+
executions: [],
|
|
297
|
+
};
|
|
258
298
|
contextsByJobKey.set(row.job.key, context);
|
|
259
299
|
}
|
|
260
300
|
if (row.execution)
|
|
@@ -578,6 +578,62 @@ describe('workflow run queries', () => {
|
|
|
578
578
|
});
|
|
579
579
|
});
|
|
580
580
|
|
|
581
|
+
test('creates a run with indexed structured dependency output config', async () => {
|
|
582
|
+
const run = await createWorkflowRun({
|
|
583
|
+
workspaceId,
|
|
584
|
+
projectId,
|
|
585
|
+
definitionId,
|
|
586
|
+
model: buildModel({
|
|
587
|
+
jobs: {
|
|
588
|
+
review: {
|
|
589
|
+
steps: [{key: 'inspect', run: 'echo inspect'}],
|
|
590
|
+
outputs: {findings: template('steps.inspect.outputs.findings')},
|
|
591
|
+
outputTypes: {
|
|
592
|
+
findings: {
|
|
593
|
+
kind: 'list',
|
|
594
|
+
element: {kind: 'object', fields: {severity: 'string'}},
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
summarize: {
|
|
599
|
+
needs: 'review',
|
|
600
|
+
steps: [
|
|
601
|
+
{
|
|
602
|
+
key: 'consume',
|
|
603
|
+
run: `test "${template('jobs.review.outputs.findings[0].severity')}" = high\necho "structured severity=${template('jobs.review.outputs.findings[0].severity')}"`,
|
|
604
|
+
},
|
|
605
|
+
],
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
}),
|
|
609
|
+
triggerPayload: {
|
|
610
|
+
source: 'manual',
|
|
611
|
+
event: 'fire',
|
|
612
|
+
subscriptionId: crypto.randomUUID(),
|
|
613
|
+
userId: crypto.randomUUID(),
|
|
614
|
+
},
|
|
615
|
+
resolveAgentDefaults: resolveTestAgentDefaults,
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const summarize = (await getJobsByWorkflowRunId(run.id)).find(
|
|
619
|
+
(job) => job.key === 'summarize',
|
|
620
|
+
);
|
|
621
|
+
if (!summarize) throw new Error('Expected summarize job');
|
|
622
|
+
const [, consume] = await getStepsByJobId(summarize.id);
|
|
623
|
+
expect(consume?.configPlan).toMatchObject({
|
|
624
|
+
env: {
|
|
625
|
+
__sf_0: {
|
|
626
|
+
segments: [
|
|
627
|
+
{
|
|
628
|
+
kind: 'deferred',
|
|
629
|
+
expression: {source: 'jobs.review.outputs.findings[0].severity'},
|
|
630
|
+
},
|
|
631
|
+
],
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
581
637
|
test('persists the parsed model on the run attempt', async () => {
|
|
582
638
|
const model = buildModel({
|
|
583
639
|
env: {RUN_ID: template('run.id')},
|
|
@@ -55,6 +55,7 @@ interface TestWorkflowJob {
|
|
|
55
55
|
readonly if?: string | undefined;
|
|
56
56
|
readonly success?: string | undefined;
|
|
57
57
|
readonly outputs?: Readonly<Record<string, string>> | undefined;
|
|
58
|
+
readonly outputTypes?: WorkflowModel['jobs'][number]['outputTypes'] | undefined;
|
|
58
59
|
readonly env?: WorkflowModel['env'] | undefined;
|
|
59
60
|
readonly listening?: WorkflowModel['jobs'][number]['listening'] | undefined;
|
|
60
61
|
readonly steps: readonly TestWorkflowStep[];
|
|
@@ -92,6 +93,7 @@ export function workflowModel(input: TestWorkflowModelInput = {}): WorkflowModel
|
|
|
92
93
|
...(job.if === undefined ? {} : {if: workflowExpression(job.if)}),
|
|
93
94
|
...(job.success === undefined ? {} : {success: job.success}),
|
|
94
95
|
...(job.outputs === undefined ? {} : {outputs: outputTemplates(job.outputs)}),
|
|
96
|
+
...(job.outputTypes === undefined ? {} : {outputTypes: job.outputTypes}),
|
|
95
97
|
...(job.name === undefined ? {} : {name: job.name}),
|
|
96
98
|
...(job.executionName === undefined
|
|
97
99
|
? {}
|