@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.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +36 -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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-workflows",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "12.
|
|
4
|
+
"version": "12.2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -22,29 +22,29 @@
|
|
|
22
22
|
"@temporalio/workflow": "1.18.1",
|
|
23
23
|
"drizzle-orm": "^0.45.2",
|
|
24
24
|
"zod": "^4.4.3",
|
|
25
|
+
"@shipfox/api-auth-context": "12.2.0",
|
|
25
26
|
"@shipfox/api-auth-dto": "12.0.0",
|
|
27
|
+
"@shipfox/api-agent-dto": "12.2.0",
|
|
26
28
|
"@shipfox/annotations-dto": "12.0.0",
|
|
27
|
-
"@shipfox/api-definitions-dto": "12.
|
|
28
|
-
"@shipfox/api-projects-dto": "12.
|
|
29
|
-
"@shipfox/api-
|
|
30
|
-
"@shipfox/api-
|
|
31
|
-
"@shipfox/api-integration-core-dto": "12.0.0",
|
|
32
|
-
"@shipfox/api-runners-dto": "12.0.0",
|
|
33
|
-
"@shipfox/api-workflows-dto": "12.0.0",
|
|
34
|
-
"@shipfox/api-workspaces-dto": "12.0.0",
|
|
29
|
+
"@shipfox/api-definitions-dto": "12.2.0",
|
|
30
|
+
"@shipfox/api-projects-dto": "12.2.0",
|
|
31
|
+
"@shipfox/api-integration-core-dto": "12.2.0",
|
|
32
|
+
"@shipfox/api-runners-dto": "12.2.0",
|
|
35
33
|
"@shipfox/api-secrets-dto": "12.0.0",
|
|
34
|
+
"@shipfox/api-workflows-dto": "12.2.0",
|
|
36
35
|
"@shipfox/inter-module": "0.2.3",
|
|
37
|
-
"@shipfox/
|
|
36
|
+
"@shipfox/api-workspaces-dto": "12.0.0",
|
|
37
|
+
"@shipfox/expression": "2.1.0",
|
|
38
38
|
"@shipfox/node-drizzle": "0.3.5",
|
|
39
39
|
"@shipfox/node-error-monitoring": "0.3.0",
|
|
40
|
-
"@shipfox/node-fastify": "0.4.
|
|
41
|
-
"@shipfox/node-module": "1.0.
|
|
40
|
+
"@shipfox/node-fastify": "0.4.2",
|
|
41
|
+
"@shipfox/node-module": "1.0.6",
|
|
42
|
+
"@shipfox/node-opentelemetry": "0.6.4",
|
|
42
43
|
"@shipfox/node-outbox": "0.2.6",
|
|
43
|
-
"@shipfox/node-opentelemetry": "0.6.3",
|
|
44
44
|
"@shipfox/node-postgres": "0.5.0",
|
|
45
|
-
"@shipfox/node-temporal": "0.4.
|
|
46
|
-
"@shipfox/workflow-document": "3.0.
|
|
47
|
-
"@shipfox/runner-labels": "0.
|
|
45
|
+
"@shipfox/node-temporal": "0.4.5",
|
|
46
|
+
"@shipfox/workflow-document": "3.0.1",
|
|
47
|
+
"@shipfox/runner-labels": "0.2.0"
|
|
48
48
|
},
|
|
49
49
|
"imports": {
|
|
50
50
|
"#*": "./dist/*"
|
package/src/core/errors.ts
CHANGED
|
@@ -140,6 +140,41 @@ export class InvalidJobRunnerLabelsError extends Error {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
export class JobOutputTooLargeError extends Error {
|
|
144
|
+
constructor(
|
|
145
|
+
readonly outputKey: string,
|
|
146
|
+
readonly limitBytes: number,
|
|
147
|
+
readonly scope: 'value' | 'total',
|
|
148
|
+
) {
|
|
149
|
+
super(
|
|
150
|
+
scope === 'total'
|
|
151
|
+
? `Job outputs exceed the ${limitBytes}-byte total limit (at "${outputKey}")`
|
|
152
|
+
: `Job output "${outputKey}" exceeds the ${limitBytes}-byte size limit`,
|
|
153
|
+
);
|
|
154
|
+
this.name = 'JobOutputTooLargeError';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export class JobOutputTooManyEntriesError extends Error {
|
|
159
|
+
constructor(
|
|
160
|
+
readonly entryCount: number,
|
|
161
|
+
readonly limitEntries: number,
|
|
162
|
+
) {
|
|
163
|
+
super(`Job outputs cannot define more than ${limitEntries} entries (found ${entryCount})`);
|
|
164
|
+
this.name = 'JobOutputTooManyEntriesError';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export class JobOutputNotJsonSafeError extends Error {
|
|
169
|
+
constructor(
|
|
170
|
+
readonly outputKey: string,
|
|
171
|
+
readonly reason: string,
|
|
172
|
+
) {
|
|
173
|
+
super(`Job output "${outputKey}" cannot be persisted as JSON: ${reason}`);
|
|
174
|
+
this.name = 'JobOutputNotJsonSafeError';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
143
178
|
// The job named by a lease is terminal, so it must not exchange its lease for
|
|
144
179
|
// fresh checkout credentials. Server state is the final gate, not the token.
|
|
145
180
|
export class JobNotActiveError extends Error {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createWorkflowExpression,
|
|
3
|
+
evaluateWorkflowExpression,
|
|
4
|
+
getWorkflowPredicateContextRoots,
|
|
5
|
+
} from '@shipfox/expression';
|
|
2
6
|
import type {JobExecution} from '#core/entities/job-execution.js';
|
|
3
7
|
import type {Step, StepAttempt} from '#core/entities/step.js';
|
|
4
8
|
import {
|
|
@@ -9,14 +13,94 @@ import {
|
|
|
9
13
|
assembleGateContext,
|
|
10
14
|
assembleJobActivationContext,
|
|
11
15
|
assembleJobResolutionContext,
|
|
16
|
+
assembleJobsContext,
|
|
12
17
|
assembleListenerSnapshotContext,
|
|
13
18
|
assembleStepDispatchContext,
|
|
14
19
|
assembleWorkflowRunContext,
|
|
20
|
+
listenerFilterOutputTypesForJobs,
|
|
15
21
|
planListenerFilterSnapshots,
|
|
16
22
|
} from './assemble-run-context.js';
|
|
17
23
|
|
|
18
24
|
const date = new Date('2026-06-30T12:00:00.000Z');
|
|
19
25
|
|
|
26
|
+
describe('assembleJobsContext', () => {
|
|
27
|
+
it.each([
|
|
28
|
+
{persistedValue: 42, expectedValue: 43n},
|
|
29
|
+
{persistedValue: '9007199254740993', expectedValue: 9007199254740994n},
|
|
30
|
+
])('rehydrates persisted int outputs for CEL arithmetic', ({persistedValue, expectedValue}) => {
|
|
31
|
+
const context = assembleJobsContext([
|
|
32
|
+
{
|
|
33
|
+
job: {key: 'review', status: 'succeeded', outputs: {count: persistedValue}},
|
|
34
|
+
outputTypes: {count: 'int'},
|
|
35
|
+
executions: [],
|
|
36
|
+
},
|
|
37
|
+
]);
|
|
38
|
+
const expression = createWorkflowExpression({
|
|
39
|
+
source: `jobs.review.outputs.count + 1 == ${expectedValue.toString()}`,
|
|
40
|
+
check: {mode: 'syntax'},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const result = evaluateWorkflowExpression(expression, context);
|
|
44
|
+
|
|
45
|
+
expect(result).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('rehydrates persisted timestamp outputs for CEL comparison', () => {
|
|
49
|
+
const persistedValue = '2026-06-30T12:00:00.000Z';
|
|
50
|
+
const context = assembleJobsContext([
|
|
51
|
+
{
|
|
52
|
+
job: {key: 'review', status: 'succeeded', outputs: {createdAt: persistedValue}},
|
|
53
|
+
outputTypes: {createdAt: 'timestamp'},
|
|
54
|
+
executions: [],
|
|
55
|
+
},
|
|
56
|
+
]);
|
|
57
|
+
const expression = createWorkflowExpression({
|
|
58
|
+
source: 'jobs.review.outputs.createdAt < timestamp("2026-07-01T00:00:00Z")',
|
|
59
|
+
check: {mode: 'syntax'},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const result = evaluateWorkflowExpression(expression, context);
|
|
63
|
+
|
|
64
|
+
expect(result).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('rehydrates persisted execution int outputs for CEL arithmetic', () => {
|
|
68
|
+
const context = assembleJobsContext([
|
|
69
|
+
{
|
|
70
|
+
job: {key: 'review', status: 'succeeded', outputs: {}},
|
|
71
|
+
outputTypes: {count: 'int'},
|
|
72
|
+
executions: [jobExecution({outputs: {count: 42}})],
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
const expression = createWorkflowExpression({
|
|
76
|
+
source: 'jobs.review.executions[0].outputs.count + 1 == 43',
|
|
77
|
+
check: {mode: 'syntax'},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const result = evaluateWorkflowExpression(expression, context);
|
|
81
|
+
|
|
82
|
+
expect(result).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('rehydrates persisted execution timestamp outputs for CEL comparison', () => {
|
|
86
|
+
const context = assembleJobsContext([
|
|
87
|
+
{
|
|
88
|
+
job: {key: 'review', status: 'succeeded', outputs: {}},
|
|
89
|
+
outputTypes: {createdAt: 'timestamp'},
|
|
90
|
+
executions: [jobExecution({outputs: {createdAt: '2026-06-30T12:00:00.000Z'}})],
|
|
91
|
+
},
|
|
92
|
+
]);
|
|
93
|
+
const expression = createWorkflowExpression({
|
|
94
|
+
source: 'jobs.review.executions[0].outputs.createdAt < timestamp("2026-07-01T00:00:00Z")',
|
|
95
|
+
check: {mode: 'syntax'},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const result = evaluateWorkflowExpression(expression, context);
|
|
99
|
+
|
|
100
|
+
expect(result).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
20
104
|
describe('assembleWorkflowRunContext', () => {
|
|
21
105
|
const run = {
|
|
22
106
|
id: 'run-1',
|
|
@@ -48,7 +132,7 @@ describe('assembleWorkflowRunContext', () => {
|
|
|
48
132
|
},
|
|
49
133
|
run: {
|
|
50
134
|
id: 'run-1',
|
|
51
|
-
number:
|
|
135
|
+
number: 1n,
|
|
52
136
|
name: 'Build',
|
|
53
137
|
project_id: 'proj-1',
|
|
54
138
|
workspace_id: 'workspace-1',
|
|
@@ -224,16 +308,16 @@ describe('assembleExecutionCreationContext', () => {
|
|
|
224
308
|
}),
|
|
225
309
|
executions: [
|
|
226
310
|
{
|
|
227
|
-
index:
|
|
311
|
+
index: 0n,
|
|
228
312
|
name: 'Build #1',
|
|
229
313
|
status: 'failed',
|
|
230
314
|
started_at: date,
|
|
231
315
|
finished_at: date,
|
|
232
|
-
events: prior.triggerEvents,
|
|
316
|
+
events: prior.triggerEvents.map((event) => ({...event, received_at: date})),
|
|
233
317
|
outputs: {},
|
|
234
318
|
},
|
|
235
319
|
{
|
|
236
|
-
index:
|
|
320
|
+
index: 1n,
|
|
237
321
|
name: 'Build #2',
|
|
238
322
|
status: 'pending',
|
|
239
323
|
started_at: null,
|
|
@@ -243,7 +327,7 @@ describe('assembleExecutionCreationContext', () => {
|
|
|
243
327
|
source: 'github',
|
|
244
328
|
event: 'deployment',
|
|
245
329
|
delivery_id: 'delivery-2',
|
|
246
|
-
received_at: '2026-06-30T12:01:00.000Z',
|
|
330
|
+
received_at: new Date('2026-06-30T12:01:00.000Z'),
|
|
247
331
|
project: null,
|
|
248
332
|
repository: null,
|
|
249
333
|
ref: null,
|
|
@@ -256,7 +340,7 @@ describe('assembleExecutionCreationContext', () => {
|
|
|
256
340
|
],
|
|
257
341
|
job: {key: 'build', name: 'Build'},
|
|
258
342
|
execution: {
|
|
259
|
-
index:
|
|
343
|
+
index: 1n,
|
|
260
344
|
name: 'Build #2',
|
|
261
345
|
status: 'pending',
|
|
262
346
|
started_at: null,
|
|
@@ -266,7 +350,7 @@ describe('assembleExecutionCreationContext', () => {
|
|
|
266
350
|
source: 'github',
|
|
267
351
|
event: 'deployment',
|
|
268
352
|
delivery_id: 'delivery-2',
|
|
269
|
-
received_at: '2026-06-30T12:01:00.000Z',
|
|
353
|
+
received_at: new Date('2026-06-30T12:01:00.000Z'),
|
|
270
354
|
project: null,
|
|
271
355
|
repository: null,
|
|
272
356
|
ref: null,
|
|
@@ -346,12 +430,15 @@ describe('assembleJobActivationContext', () => {
|
|
|
346
430
|
outputs: {image: 'app:123'},
|
|
347
431
|
executions: [
|
|
348
432
|
{
|
|
349
|
-
index:
|
|
433
|
+
index: 0n,
|
|
350
434
|
name: 'Build #1',
|
|
351
435
|
status: 'succeeded',
|
|
352
436
|
started_at: date,
|
|
353
437
|
finished_at: null,
|
|
354
|
-
events: buildExecution.triggerEvents
|
|
438
|
+
events: buildExecution.triggerEvents.map((event) => ({
|
|
439
|
+
...event,
|
|
440
|
+
received_at: date,
|
|
441
|
+
})),
|
|
355
442
|
outputs: {sha: 'abc123'},
|
|
356
443
|
},
|
|
357
444
|
],
|
|
@@ -370,12 +457,15 @@ describe('assembleJobActivationContext', () => {
|
|
|
370
457
|
outputs: {image: 'app:123'},
|
|
371
458
|
executions: [
|
|
372
459
|
{
|
|
373
|
-
index:
|
|
460
|
+
index: 0n,
|
|
374
461
|
name: 'Build #1',
|
|
375
462
|
status: 'succeeded',
|
|
376
463
|
started_at: date,
|
|
377
464
|
finished_at: null,
|
|
378
|
-
events: buildExecution.triggerEvents
|
|
465
|
+
events: buildExecution.triggerEvents.map((event) => ({
|
|
466
|
+
...event,
|
|
467
|
+
received_at: date,
|
|
468
|
+
})),
|
|
379
469
|
outputs: {sha: 'abc123'},
|
|
380
470
|
},
|
|
381
471
|
],
|
|
@@ -501,6 +591,59 @@ describe('listener filter snapshots', () => {
|
|
|
501
591
|
expect(matcher?.filter_snapshot).toEqual({vars: {ENABLED: 'true'}});
|
|
502
592
|
});
|
|
503
593
|
|
|
594
|
+
it('persists output types beside JSON-safe listener snapshots', () => {
|
|
595
|
+
const plan = planListenerFilterSnapshots({
|
|
596
|
+
on: [
|
|
597
|
+
{
|
|
598
|
+
source: 'github',
|
|
599
|
+
event: 'pull_request',
|
|
600
|
+
filter: 'jobs.build.outputs.details.count + 1 == 43',
|
|
601
|
+
},
|
|
602
|
+
],
|
|
603
|
+
until: null,
|
|
604
|
+
});
|
|
605
|
+
const dependencyJobs = [
|
|
606
|
+
{
|
|
607
|
+
job: {
|
|
608
|
+
key: 'build',
|
|
609
|
+
status: 'succeeded' as const,
|
|
610
|
+
outputs: {details: {count: 42}},
|
|
611
|
+
},
|
|
612
|
+
outputTypes: {
|
|
613
|
+
details: {kind: 'object' as const, fields: {count: 'int' as const}},
|
|
614
|
+
},
|
|
615
|
+
executions: [],
|
|
616
|
+
},
|
|
617
|
+
];
|
|
618
|
+
const context = assembleListenerSnapshotContext({
|
|
619
|
+
job: {key: 'await'},
|
|
620
|
+
run,
|
|
621
|
+
triggerPayload,
|
|
622
|
+
plan,
|
|
623
|
+
dependencyJobs,
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
const [matcher] = applyListenerFilterSnapshots(
|
|
627
|
+
plan.on,
|
|
628
|
+
context,
|
|
629
|
+
listenerFilterOutputTypesForJobs(dependencyJobs),
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
expect(matcher).toEqual({
|
|
633
|
+
source: 'github',
|
|
634
|
+
event: 'pull_request',
|
|
635
|
+
filter: 'jobs.build.outputs.details.count + 1 == 43',
|
|
636
|
+
filter_snapshot: {
|
|
637
|
+
jobs: {
|
|
638
|
+
build: expect.objectContaining({outputs: {details: {count: 42}}}),
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
filter_output_types: {
|
|
642
|
+
build: {details: {kind: 'object', fields: {count: 'int'}}},
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
|
|
504
647
|
it('omits snapshots for event-only filters and malformed filters', () => {
|
|
505
648
|
const plan = planListenerFilterSnapshots({
|
|
506
649
|
on: [
|
|
@@ -606,6 +749,39 @@ describe('listener filter snapshots', () => {
|
|
|
606
749
|
},
|
|
607
750
|
});
|
|
608
751
|
});
|
|
752
|
+
|
|
753
|
+
it('keeps snapshots JSON-serializable so they survive the outbox payload', () => {
|
|
754
|
+
const plan = planListenerFilterSnapshots({
|
|
755
|
+
on: [
|
|
756
|
+
{
|
|
757
|
+
source: 'github',
|
|
758
|
+
event: 'pull_request',
|
|
759
|
+
filter: 'jobs.build.executions[0].index == 0 && run.number == 1',
|
|
760
|
+
},
|
|
761
|
+
],
|
|
762
|
+
until: null,
|
|
763
|
+
});
|
|
764
|
+
const context = assembleListenerSnapshotContext({
|
|
765
|
+
job: {key: 'await'},
|
|
766
|
+
run,
|
|
767
|
+
triggerPayload,
|
|
768
|
+
plan,
|
|
769
|
+
dependencyJobs: [
|
|
770
|
+
{
|
|
771
|
+
job: {key: 'build', status: 'succeeded', outputs: {}},
|
|
772
|
+
executions: [jobExecution({id: 'exec-build', jobId: 'job-build'})],
|
|
773
|
+
},
|
|
774
|
+
],
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
const [matcher] = applyListenerFilterSnapshots(plan.on, context);
|
|
778
|
+
const jobs = matcher?.filter_snapshot?.jobs as Record<string, {executions: {index: unknown}[]}>;
|
|
779
|
+
const snapshotRun = matcher?.filter_snapshot?.run as {number: unknown};
|
|
780
|
+
|
|
781
|
+
expect(typeof jobs.build?.executions[0]?.index).toBe('number');
|
|
782
|
+
expect(typeof snapshotRun.number).toBe('number');
|
|
783
|
+
expect(() => JSON.stringify(matcher?.filter_snapshot)).not.toThrow();
|
|
784
|
+
});
|
|
609
785
|
});
|
|
610
786
|
|
|
611
787
|
describe('assembleStepDispatchContext', () => {
|
|
@@ -642,7 +818,7 @@ describe('assembleStepDispatchContext', () => {
|
|
|
642
818
|
site: 'step-dispatch',
|
|
643
819
|
values: {
|
|
644
820
|
execution: {
|
|
645
|
-
index:
|
|
821
|
+
index: 1n,
|
|
646
822
|
name: 'Deploy',
|
|
647
823
|
status: 'running',
|
|
648
824
|
failed: false,
|
|
@@ -653,7 +829,7 @@ describe('assembleStepDispatchContext', () => {
|
|
|
653
829
|
source: 'github',
|
|
654
830
|
event: 'push',
|
|
655
831
|
delivery_id: 'delivery-1',
|
|
656
|
-
received_at:
|
|
832
|
+
received_at: date,
|
|
657
833
|
project: null,
|
|
658
834
|
repository: null,
|
|
659
835
|
ref: null,
|
|
@@ -980,7 +1156,7 @@ describe('assembleJobResolutionContext', () => {
|
|
|
980
1156
|
vars: {},
|
|
981
1157
|
executions: [
|
|
982
1158
|
{
|
|
983
|
-
index:
|
|
1159
|
+
index: 0n,
|
|
984
1160
|
name: 'First',
|
|
985
1161
|
status: 'failed',
|
|
986
1162
|
started_at: date,
|
|
@@ -990,7 +1166,7 @@ describe('assembleJobResolutionContext', () => {
|
|
|
990
1166
|
source: 'github',
|
|
991
1167
|
event: 'push',
|
|
992
1168
|
delivery_id: 'delivery-1',
|
|
993
|
-
received_at:
|
|
1169
|
+
received_at: date,
|
|
994
1170
|
project: null,
|
|
995
1171
|
repository: null,
|
|
996
1172
|
ref: null,
|
|
@@ -1001,7 +1177,7 @@ describe('assembleJobResolutionContext', () => {
|
|
|
1001
1177
|
outputs: {},
|
|
1002
1178
|
},
|
|
1003
1179
|
{
|
|
1004
|
-
index:
|
|
1180
|
+
index: 1n,
|
|
1005
1181
|
name: 'Second',
|
|
1006
1182
|
status: 'succeeded',
|
|
1007
1183
|
started_at: date,
|
|
@@ -1011,7 +1187,7 @@ describe('assembleJobResolutionContext', () => {
|
|
|
1011
1187
|
source: 'github',
|
|
1012
1188
|
event: 'push',
|
|
1013
1189
|
delivery_id: 'delivery-1',
|
|
1014
|
-
received_at:
|
|
1190
|
+
received_at: date,
|
|
1015
1191
|
project: null,
|
|
1016
1192
|
repository: null,
|
|
1017
1193
|
ref: null,
|
|
@@ -1069,15 +1245,51 @@ describe('assembleExecutionResolutionContext', () => {
|
|
|
1069
1245
|
});
|
|
1070
1246
|
|
|
1071
1247
|
expect(context.values.execution).toEqual({
|
|
1072
|
-
index:
|
|
1248
|
+
index: 0n,
|
|
1073
1249
|
name: 'Build #2',
|
|
1074
1250
|
status: 'running',
|
|
1075
1251
|
started_at: date,
|
|
1076
1252
|
finished_at: null,
|
|
1077
|
-
events: targetExecution.triggerEvents,
|
|
1253
|
+
events: targetExecution.triggerEvents.map((event) => ({...event, received_at: date})),
|
|
1078
1254
|
outputs: {sha: 'target'},
|
|
1079
1255
|
});
|
|
1080
1256
|
});
|
|
1257
|
+
|
|
1258
|
+
it('exposes execution indices and event timestamps as CEL-native values', () => {
|
|
1259
|
+
const targetExecution = jobExecution({
|
|
1260
|
+
id: 'exec-2',
|
|
1261
|
+
sequence: 2,
|
|
1262
|
+
outputs: {sha: 'target'},
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
const context = assembleExecutionResolutionContext({
|
|
1266
|
+
run,
|
|
1267
|
+
triggerPayload: {
|
|
1268
|
+
source: 'manual',
|
|
1269
|
+
event: 'fire',
|
|
1270
|
+
subscriptionId: 'sub-1',
|
|
1271
|
+
userId: 'user-1',
|
|
1272
|
+
},
|
|
1273
|
+
job: {key: 'build'},
|
|
1274
|
+
jobExecution: targetExecution,
|
|
1275
|
+
executions: [targetExecution],
|
|
1276
|
+
steps: [],
|
|
1277
|
+
attempts: [],
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
const expressions = [
|
|
1281
|
+
'executions[0].index + 1 == 1',
|
|
1282
|
+
'execution.index + 1 == 1',
|
|
1283
|
+
'run.number + 1 == 2',
|
|
1284
|
+
'executions[0].events[0].received_at < timestamp("2026-07-01T00:00:00Z")',
|
|
1285
|
+
];
|
|
1286
|
+
|
|
1287
|
+
for (const source of expressions) {
|
|
1288
|
+
const expression = createWorkflowExpression({source, check: {mode: 'syntax'}});
|
|
1289
|
+
|
|
1290
|
+
expect(evaluateWorkflowExpression(expression, context.values)).toBe(true);
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1081
1293
|
});
|
|
1082
1294
|
|
|
1083
1295
|
function step(overrides: Partial<Step> = {}): Step {
|