@shipfox/api-workflows 12.0.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.
Files changed (73) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +36 -0
  3. package/dist/core/entities/job.d.ts +1 -1
  4. package/dist/core/entities/job.d.ts.map +1 -1
  5. package/dist/core/entities/step.d.ts +2 -2
  6. package/dist/core/entities/step.d.ts.map +1 -1
  7. package/dist/core/errors.d.ts +16 -0
  8. package/dist/core/errors.d.ts.map +1 -1
  9. package/dist/core/errors.js +18 -0
  10. package/dist/core/errors.js.map +1 -1
  11. package/dist/core/step-config/assemble-run-context.d.ts +15 -5
  12. package/dist/core/step-config/assemble-run-context.d.ts.map +1 -1
  13. package/dist/core/step-config/assemble-run-context.js +77 -27
  14. package/dist/core/step-config/assemble-run-context.js.map +1 -1
  15. package/dist/core/step-config/fields.d.ts +5 -0
  16. package/dist/core/step-config/fields.d.ts.map +1 -1
  17. package/dist/core/step-config/fields.js +21 -3
  18. package/dist/core/step-config/fields.js.map +1 -1
  19. package/dist/core/step-config/index.d.ts +1 -1
  20. package/dist/core/step-config/index.d.ts.map +1 -1
  21. package/dist/core/step-config/index.js +1 -1
  22. package/dist/core/step-config/index.js.map +1 -1
  23. package/dist/core/step-config/job-output-limits.d.ts +11 -0
  24. package/dist/core/step-config/job-output-limits.d.ts.map +1 -0
  25. package/dist/core/step-config/job-output-limits.js +88 -0
  26. package/dist/core/step-config/job-output-limits.js.map +1 -0
  27. package/dist/core/step-config/materialize-workflow-model.d.ts +1 -1
  28. package/dist/core/step-config/materialize-workflow-model.d.ts.map +1 -1
  29. package/dist/core/step-config/materialize-workflow-model.js +41 -14
  30. package/dist/core/step-config/materialize-workflow-model.js.map +1 -1
  31. package/dist/db/db.d.ts +50 -50
  32. package/dist/db/job-listeners.d.ts.map +1 -1
  33. package/dist/db/job-listeners.js +6 -5
  34. package/dist/db/job-listeners.js.map +1 -1
  35. package/dist/db/schema/job-executions.d.ts +3 -3
  36. package/dist/db/schema/job-listener-events.d.ts +1 -1
  37. package/dist/db/schema/jobs.d.ts +7 -7
  38. package/dist/db/schema/outbox.d.ts +1 -1
  39. package/dist/db/schema/step-attempts.d.ts +2 -2
  40. package/dist/db/schema/steps.d.ts +5 -5
  41. package/dist/db/schema/workflow-run-attempts.d.ts +3 -3
  42. package/dist/db/schema/workflow-run-counters.d.ts +1 -1
  43. package/dist/db/schema/workflow-runs.d.ts +2 -2
  44. package/dist/db/workflow-runs/job-executions.d.ts.map +1 -1
  45. package/dist/db/workflow-runs/job-executions.js +4 -2
  46. package/dist/db/workflow-runs/job-executions.js.map +1 -1
  47. package/dist/db/workflow-runs/jobs.d.ts.map +1 -1
  48. package/dist/db/workflow-runs/jobs.js +29 -3
  49. package/dist/db/workflow-runs/jobs.js.map +1 -1
  50. package/dist/presentation/routes/project-access.d.ts +1 -1
  51. package/dist/temporal/activities/index.d.ts +1 -1
  52. package/dist/temporal/activities/index.d.ts.map +1 -1
  53. package/dist/temporal/activities/orchestration-activities.d.ts +1 -1
  54. package/dist/temporal/activities/orchestration-activities.d.ts.map +1 -1
  55. package/dist/temporal/workflows/test-env.d.ts +2 -2
  56. package/dist/tsconfig.test.tsbuildinfo +1 -1
  57. package/package.json +16 -16
  58. package/src/core/errors.ts +35 -0
  59. package/src/core/step-config/assemble-run-context.test.ts +232 -20
  60. package/src/core/step-config/assemble-run-context.ts +126 -28
  61. package/src/core/step-config/fields.ts +42 -3
  62. package/src/core/step-config/index.ts +2 -0
  63. package/src/core/step-config/job-output-limits.ts +130 -0
  64. package/src/core/step-config/materialize-workflow-model.test.ts +306 -1
  65. package/src/core/step-config/materialize-workflow-model.ts +60 -15
  66. package/src/db/job-listeners.ts +14 -3
  67. package/src/db/workflow-runs/job-executions.test.ts +207 -1
  68. package/src/db/workflow-runs/job-executions.ts +15 -2
  69. package/src/db/workflow-runs/jobs.test.ts +49 -0
  70. package/src/db/workflow-runs/jobs.ts +46 -6
  71. package/src/db/workflow-runs/run-create.test.ts +56 -0
  72. package/test/factories/workflow-model.ts +2 -0
  73. 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 {InterpolationUnresolvableError, JobNotFoundError} from '#core/errors.js';
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 (!(error instanceof InterpolationUnresolvableError)) throw error;
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()).select().from(jobs).where(eq(jobs.id, jobId)).limit(1);
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
- context = {job: toJob(row.job), executions: []};
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
- context = {job: toJob(row.job), executions: []};
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
  ? {}