@shipfox/api-workflows 15.0.0 → 16.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 +4 -4
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +36 -0
- package/dist/core/agent-tools.js +8 -3
- package/dist/core/agent-tools.js.map +1 -1
- package/dist/core/entities/step.d.ts +1 -1
- package/dist/core/entities/step.d.ts.map +1 -1
- package/dist/core/entities/step.js +2 -1
- package/dist/core/entities/step.js.map +1 -1
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js.map +1 -1
- package/dist/core/step-config/materialize-job-execution-steps.js +2 -0
- package/dist/core/step-config/materialize-job-execution-steps.js.map +1 -1
- package/dist/core/step-config/resolve-step-config.js +3 -1
- package/dist/core/step-config/resolve-step-config.js.map +1 -1
- package/dist/db/db.d.ts +8 -8
- 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/schema/steps.d.ts +2 -2
- package/dist/db/schema/workflow-run-attempts.d.ts +2 -2
- package/dist/db/workflow-runs/outbox.d.ts +1 -0
- package/dist/db/workflow-runs/outbox.d.ts.map +1 -1
- package/dist/db/workflow-runs/outbox.js +2 -1
- package/dist/db/workflow-runs/outbox.js.map +1 -1
- package/dist/db/workflow-runs/run-create.d.ts.map +1 -1
- package/dist/db/workflow-runs/run-create.js +49 -1
- package/dist/db/workflow-runs/run-create.js.map +1 -1
- package/dist/db/workflow-runs/steps.d.ts +6 -0
- package/dist/db/workflow-runs/steps.d.ts.map +1 -1
- package/dist/db/workflow-runs/steps.js +14 -0
- package/dist/db/workflow-runs/steps.js.map +1 -1
- package/dist/db/workflow-runs.d.ts +1 -1
- package/dist/db/workflow-runs.d.ts.map +1 -1
- package/dist/db/workflow-runs.js +1 -1
- package/dist/db/workflow-runs.js.map +1 -1
- package/dist/presentation/inter-module.d.ts.map +1 -1
- package/dist/presentation/inter-module.js +6 -1
- package/dist/presentation/inter-module.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/core/agent-tools.test.ts +85 -0
- package/src/core/agent-tools.ts +11 -3
- package/src/core/entities/step.ts +1 -1
- package/src/core/errors.ts +2 -0
- package/src/core/job-execution.test.ts +1 -0
- package/src/core/run-workflow.test.ts +4 -0
- package/src/core/step-config/materialize-job-execution-steps.ts +2 -0
- package/src/core/step-config/resolve-step-config.ts +4 -1
- package/src/db/index.ts +1 -0
- package/src/db/workflow-runs/outbox.ts +2 -0
- package/src/db/workflow-runs/run-create.test.ts +41 -0
- package/src/db/workflow-runs/run-create.ts +81 -3
- package/src/db/workflow-runs/steps.ts +19 -0
- package/src/db/workflow-runs.ts +1 -0
- package/src/presentation/inter-module.test.ts +26 -0
- package/src/presentation/inter-module.ts +9 -1
- package/src/presentation/routes/agent-runtime-config.test.ts +2 -0
- package/test/factories/workflow-model.ts +74 -2
- package/test/fixtures/agent-inter-module.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": "
|
|
4
|
+
"version": "16.1.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -25,18 +25,18 @@
|
|
|
25
25
|
"zod": "^4.4.3",
|
|
26
26
|
"@shipfox/api-auth-context": "15.0.0",
|
|
27
27
|
"@shipfox/api-auth-dto": "15.0.0",
|
|
28
|
-
"@shipfox/api-agent-dto": "
|
|
28
|
+
"@shipfox/api-agent-dto": "16.1.0",
|
|
29
29
|
"@shipfox/annotations-dto": "12.3.0",
|
|
30
|
-
"@shipfox/api-definitions-dto": "
|
|
30
|
+
"@shipfox/api-definitions-dto": "16.1.0",
|
|
31
31
|
"@shipfox/api-projects-dto": "15.0.0",
|
|
32
|
-
"@shipfox/api-integration-core-dto": "
|
|
33
|
-
"@shipfox/api-runners-dto": "
|
|
32
|
+
"@shipfox/api-integration-core-dto": "16.0.0",
|
|
33
|
+
"@shipfox/api-runners-dto": "16.0.0",
|
|
34
34
|
"@shipfox/api-secrets-dto": "12.0.0",
|
|
35
|
-
"@shipfox/api-workflows-dto": "
|
|
35
|
+
"@shipfox/api-workflows-dto": "16.1.0",
|
|
36
36
|
"@shipfox/api-workspaces-dto": "15.0.0",
|
|
37
37
|
"@shipfox/config": "1.2.4",
|
|
38
38
|
"@shipfox/inter-module": "0.2.3",
|
|
39
|
-
"@shipfox/expression": "2.
|
|
39
|
+
"@shipfox/expression": "2.4.0",
|
|
40
40
|
"@shipfox/node-drizzle": "0.3.5",
|
|
41
41
|
"@shipfox/node-error-monitoring": "0.3.0",
|
|
42
42
|
"@shipfox/node-fastify": "0.4.3",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@shipfox/node-postgres": "0.5.0",
|
|
47
47
|
"@shipfox/node-temporal": "0.4.6",
|
|
48
48
|
"@shipfox/runner-labels": "0.2.0",
|
|
49
|
-
"@shipfox/workflow-document": "3.
|
|
49
|
+
"@shipfox/workflow-document": "3.3.0"
|
|
50
50
|
},
|
|
51
51
|
"imports": {
|
|
52
52
|
"#*": "./dist/*"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {workflowModel} from '#test/index.js';
|
|
2
|
+
import {loadAgentToolMaterializationContext} from './agent-tools.js';
|
|
3
|
+
import {AgentIntegrationMaterializationError} from './errors.js';
|
|
4
|
+
|
|
5
|
+
describe('loadAgentToolMaterializationContext', () => {
|
|
6
|
+
const workspaceId = crypto.randomUUID();
|
|
7
|
+
const projectId = crypto.randomUUID();
|
|
8
|
+
|
|
9
|
+
test('returns undefined for a model with only run steps', async () => {
|
|
10
|
+
const model = workflowModel({
|
|
11
|
+
name: 'Plain',
|
|
12
|
+
runner: 'ubuntu-latest',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
await expect(
|
|
16
|
+
loadAgentToolMaterializationContext({model, workspaceId, projectId}),
|
|
17
|
+
).resolves.toBeUndefined();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('returns undefined for a model with only checkout steps', async () => {
|
|
21
|
+
const model = workflowModel({
|
|
22
|
+
name: 'Checkout',
|
|
23
|
+
runner: 'ubuntu-latest',
|
|
24
|
+
jobs: {
|
|
25
|
+
build: {
|
|
26
|
+
steps: [
|
|
27
|
+
{
|
|
28
|
+
checkout: {
|
|
29
|
+
repository: 'acme/platform',
|
|
30
|
+
fetchDepth: 1,
|
|
31
|
+
permissions: {contents: 'read'},
|
|
32
|
+
persistCredentials: true,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await expect(
|
|
41
|
+
loadAgentToolMaterializationContext({model, workspaceId, projectId}),
|
|
42
|
+
).resolves.toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('returns undefined for agent steps without integrations', async () => {
|
|
46
|
+
const model = workflowModel({
|
|
47
|
+
name: 'Agent',
|
|
48
|
+
runner: 'ubuntu-latest',
|
|
49
|
+
jobs: {
|
|
50
|
+
build: {
|
|
51
|
+
steps: [{prompt: 'Do the thing'}],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await expect(
|
|
57
|
+
loadAgentToolMaterializationContext({model, workspaceId, projectId}),
|
|
58
|
+
).resolves.toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('does not short-circuit for a tool-step-only model', async () => {
|
|
62
|
+
const model = workflowModel({
|
|
63
|
+
name: 'Tools',
|
|
64
|
+
runner: 'ubuntu-latest',
|
|
65
|
+
jobs: {
|
|
66
|
+
build: {
|
|
67
|
+
steps: [
|
|
68
|
+
{
|
|
69
|
+
tool: 'get_issue',
|
|
70
|
+
connection: 'linear-main',
|
|
71
|
+
with: {id: 'ENG-1'},
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// A tool step is an integration tool reference, so the loader must not
|
|
79
|
+
// return early: with project access missing it reports the materialization
|
|
80
|
+
// setup failure instead of silently skipping tool-step runs.
|
|
81
|
+
await expect(
|
|
82
|
+
loadAgentToolMaterializationContext({model, workspaceId, projectId}),
|
|
83
|
+
).rejects.toThrow(AgentIntegrationMaterializationError);
|
|
84
|
+
});
|
|
85
|
+
});
|
package/src/core/agent-tools.ts
CHANGED
|
@@ -61,7 +61,7 @@ export async function loadAgentToolMaterializationContext(params: {
|
|
|
61
61
|
readonly projects?: ProjectsModuleClient | undefined;
|
|
62
62
|
readonly jobs?: readonly WorkflowModelJob[] | undefined;
|
|
63
63
|
}): Promise<AgentToolMaterializationContext | undefined> {
|
|
64
|
-
if (!
|
|
64
|
+
if (!hasIntegrationToolReferences(params.model, params.jobs)) return undefined;
|
|
65
65
|
|
|
66
66
|
if (params.projects === undefined) {
|
|
67
67
|
throw new AgentIntegrationMaterializationError('Project access is not configured');
|
|
@@ -410,12 +410,20 @@ function isPermissionScope(
|
|
|
410
410
|
);
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
-
|
|
413
|
+
/**
|
|
414
|
+
* Model-level twin of `hasIntegrationToolReferences` in
|
|
415
|
+
* `libs/api/definitions/src/core/has-integration-tool-references.ts`, which
|
|
416
|
+
* runs the same criterion over the authored `WorkflowDocument` at sync time.
|
|
417
|
+
* Keep both in sync.
|
|
418
|
+
*/
|
|
419
|
+
function hasIntegrationToolReferences(
|
|
414
420
|
model: WorkflowModel | null,
|
|
415
421
|
jobs: readonly WorkflowModelJob[] | undefined,
|
|
416
422
|
): boolean {
|
|
417
423
|
if (model === null) return false;
|
|
418
424
|
return (jobs ?? model.jobs).some((job) =>
|
|
419
|
-
job.steps.some(
|
|
425
|
+
job.steps.some(
|
|
426
|
+
(step) => step.kind === 'tool' || (step.kind === 'agent' && step.integrations !== undefined),
|
|
427
|
+
),
|
|
420
428
|
);
|
|
421
429
|
}
|
|
@@ -13,7 +13,7 @@ import type {InterpolationUnresolvableField} from '../errors.js';
|
|
|
13
13
|
|
|
14
14
|
export type StepStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'cancelled' | 'skipped';
|
|
15
15
|
|
|
16
|
-
export const STEP_TYPES = ['setup', 'run', 'agent', 'checkout'] as const;
|
|
16
|
+
export const STEP_TYPES = ['setup', 'run', 'agent', 'checkout', 'tool'] as const;
|
|
17
17
|
|
|
18
18
|
export type StepType = (typeof STEP_TYPES)[number];
|
|
19
19
|
|
package/src/core/errors.ts
CHANGED
|
@@ -62,6 +62,8 @@ describe('runWorkflow', () => {
|
|
|
62
62
|
getValidationCatalog: vi.fn().mockResolvedValue(agentValidationCatalog),
|
|
63
63
|
resolveAgentConfig: mockResolveAgentConfig,
|
|
64
64
|
resolveRuntimeCredentials: vi.fn(),
|
|
65
|
+
claimSession: vi.fn(),
|
|
66
|
+
carryOverSessions: vi.fn(),
|
|
65
67
|
};
|
|
66
68
|
|
|
67
69
|
beforeEach(() => {
|
|
@@ -256,6 +258,8 @@ describe('runDevWorkflow', () => {
|
|
|
256
258
|
getValidationCatalog: vi.fn().mockResolvedValue(agentValidationCatalog),
|
|
257
259
|
resolveAgentConfig: mockResolveAgentConfig,
|
|
258
260
|
resolveRuntimeCredentials: vi.fn(),
|
|
261
|
+
claimSession: vi.fn(),
|
|
262
|
+
carryOverSessions: vi.fn(),
|
|
259
263
|
};
|
|
260
264
|
|
|
261
265
|
beforeEach(() => {
|
|
@@ -168,6 +168,8 @@ function stepDisplayName(step: WorkflowModelStep): string {
|
|
|
168
168
|
: `${step.model} · ${firstLine(step.prompt)}`;
|
|
169
169
|
case 'checkout':
|
|
170
170
|
return 'Checkout';
|
|
171
|
+
case 'tool':
|
|
172
|
+
return step.tool.method === undefined ? step.tool.id : `${step.tool.id}.${step.tool.method}`;
|
|
171
173
|
default:
|
|
172
174
|
return assertNever(step);
|
|
173
175
|
}
|
|
@@ -154,7 +154,10 @@ function resolveWorkingDirectoryConfig(
|
|
|
154
154
|
params: Omit<BuildStepConfigParams, 'context'> & {readonly context: WorkflowEvaluationContext},
|
|
155
155
|
): WorkingDirectoryConfig {
|
|
156
156
|
const value = params.step.workingDirectory;
|
|
157
|
-
|
|
157
|
+
// Tool steps have no working tree, so they never carry a working-directory
|
|
158
|
+
// template; the union narrows the remaining kinds to those that can.
|
|
159
|
+
const template =
|
|
160
|
+
params.step.kind === 'tool' ? undefined : params.step.templates?.workingDirectory;
|
|
158
161
|
if (value === undefined) {
|
|
159
162
|
return {
|
|
160
163
|
config: {},
|
package/src/db/index.ts
CHANGED
|
@@ -99,6 +99,7 @@ export async function writeJobStepsSettledOutbox(
|
|
|
99
99
|
export async function writeStepAttemptTerminatedOutbox(
|
|
100
100
|
tx: Tx,
|
|
101
101
|
params: {
|
|
102
|
+
stepAttemptId: string;
|
|
102
103
|
stepId: string;
|
|
103
104
|
attempt: number;
|
|
104
105
|
status: 'succeeded' | 'failed' | 'cancelled';
|
|
@@ -119,6 +120,7 @@ export async function writeStepAttemptTerminatedOutbox(
|
|
|
119
120
|
attempt: params.attempt,
|
|
120
121
|
status: params.status,
|
|
121
122
|
logOutcome: params.logOutcome,
|
|
123
|
+
stepAttemptId: params.stepAttemptId,
|
|
122
124
|
},
|
|
123
125
|
});
|
|
124
126
|
}
|
|
@@ -1191,6 +1191,47 @@ describe('workflow run queries', () => {
|
|
|
1191
1191
|
}),
|
|
1192
1192
|
expected: {field: 'step.working_directory', source: 'vars.REQUIRED'},
|
|
1193
1193
|
},
|
|
1194
|
+
{
|
|
1195
|
+
field: 'tool.with',
|
|
1196
|
+
model: () =>
|
|
1197
|
+
workflowModel({
|
|
1198
|
+
name: 'Missing tool with var',
|
|
1199
|
+
runner: 'ubuntu-latest',
|
|
1200
|
+
jobs: {
|
|
1201
|
+
build: {
|
|
1202
|
+
steps: [
|
|
1203
|
+
{
|
|
1204
|
+
tool: 'get_issue',
|
|
1205
|
+
connection: 'linear-main',
|
|
1206
|
+
with: {id: template('vars.REQUIRED')},
|
|
1207
|
+
},
|
|
1208
|
+
],
|
|
1209
|
+
},
|
|
1210
|
+
},
|
|
1211
|
+
}),
|
|
1212
|
+
expected: {field: 'tool.with', source: 'vars.REQUIRED'},
|
|
1213
|
+
},
|
|
1214
|
+
{
|
|
1215
|
+
field: 'tool.outputs',
|
|
1216
|
+
model: () =>
|
|
1217
|
+
workflowModel({
|
|
1218
|
+
name: 'Missing tool output var',
|
|
1219
|
+
runner: 'ubuntu-latest',
|
|
1220
|
+
jobs: {
|
|
1221
|
+
build: {
|
|
1222
|
+
steps: [
|
|
1223
|
+
{
|
|
1224
|
+
tool: 'get_issue',
|
|
1225
|
+
connection: 'linear-main',
|
|
1226
|
+
with: {id: 'ENG-1'},
|
|
1227
|
+
outputs: {sev: template('vars.REQUIRED')},
|
|
1228
|
+
},
|
|
1229
|
+
],
|
|
1230
|
+
},
|
|
1231
|
+
},
|
|
1232
|
+
}),
|
|
1233
|
+
expected: {field: 'tool.outputs', envKey: 'sev', source: 'vars.REQUIRED'},
|
|
1234
|
+
},
|
|
1194
1235
|
{
|
|
1195
1236
|
field: 'checkout.repository',
|
|
1196
1237
|
model: () =>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createWorkflowModelSnapshot,
|
|
3
3
|
WORKFLOW_MODEL_CHECKOUT_TARGET_FIELDS,
|
|
4
|
+
type WorkflowJsonTemplateTree,
|
|
4
5
|
type WorkflowModel,
|
|
5
6
|
} from '@shipfox/api-definitions-dto';
|
|
6
7
|
import type {IntegrationsModuleClient} from '@shipfox/api-integration-core-dto/inter-module';
|
|
@@ -455,9 +456,13 @@ function referencedVariables(
|
|
|
455
456
|
|
|
456
457
|
for (const step of job.steps) {
|
|
457
458
|
collectFieldVariableReferences(step.templates?.name, references, {field: 'step.name'});
|
|
458
|
-
collectFieldVariableReferences(
|
|
459
|
-
|
|
460
|
-
|
|
459
|
+
collectFieldVariableReferences(
|
|
460
|
+
step.kind === 'tool' ? undefined : step.templates?.workingDirectory,
|
|
461
|
+
references,
|
|
462
|
+
{
|
|
463
|
+
field: 'step.working_directory',
|
|
464
|
+
},
|
|
465
|
+
);
|
|
461
466
|
if (step.kind === 'run') {
|
|
462
467
|
collectFieldVariableReferences(step.templates?.command, references, {field: 'run'});
|
|
463
468
|
collectTemplateVariableReferences(step.templates?.env, references);
|
|
@@ -467,6 +472,16 @@ function referencedVariables(
|
|
|
467
472
|
collectFieldVariableReferences(step.templates?.provider, references, {
|
|
468
473
|
field: 'agent.provider',
|
|
469
474
|
});
|
|
475
|
+
} else if (step.kind === 'tool') {
|
|
476
|
+
collectTemplateTreeVariableReferences(step.templates?.with, references, {
|
|
477
|
+
field: 'tool.with',
|
|
478
|
+
});
|
|
479
|
+
for (const [key, expression] of Object.entries(step.outputMappings ?? {})) {
|
|
480
|
+
collectExpressionVariableReferences(expression, references, {
|
|
481
|
+
field: 'tool.outputs',
|
|
482
|
+
envKey: key,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
470
485
|
} else if (step.kind === 'checkout') {
|
|
471
486
|
for (const [key, field] of WORKFLOW_MODEL_CHECKOUT_TARGET_FIELDS) {
|
|
472
487
|
collectFieldVariableReferences(step.checkout.templates?.[key], references, {field});
|
|
@@ -534,6 +549,69 @@ function collectFieldVariableReferences(
|
|
|
534
549
|
}
|
|
535
550
|
}
|
|
536
551
|
|
|
552
|
+
/**
|
|
553
|
+
* Collect `vars.*` references from a tool step's `with` template tree: a
|
|
554
|
+
* `WorkflowJsonTemplateTree` mirrors the authored `with` payload with every
|
|
555
|
+
* interpolated string leaf replaced by its parsed template, so walk it like
|
|
556
|
+
* the authored structure and collect from each leaf template.
|
|
557
|
+
*/
|
|
558
|
+
function collectTemplateTreeVariableReferences(
|
|
559
|
+
tree: WorkflowJsonTemplateTree | undefined,
|
|
560
|
+
references: ReferencedVariable[],
|
|
561
|
+
source: {
|
|
562
|
+
readonly field: InterpolationUnresolvableError['field'];
|
|
563
|
+
readonly envKey?: string | undefined;
|
|
564
|
+
},
|
|
565
|
+
): void {
|
|
566
|
+
if (tree === undefined) return;
|
|
567
|
+
|
|
568
|
+
if (Array.isArray(tree)) {
|
|
569
|
+
// A field template is itself an array of segments; a `with` list is an
|
|
570
|
+
// array of child trees. Segments carry a `kind`, so distinguish the two.
|
|
571
|
+
if (tree.every((element) => isFieldTemplateSegment(element))) {
|
|
572
|
+
collectFieldVariableReferences(tree, references, source);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
for (const child of tree) collectTemplateTreeVariableReferences(child, references, source);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (typeof tree === 'object') {
|
|
580
|
+
for (const child of Object.values(tree)) {
|
|
581
|
+
collectTemplateTreeVariableReferences(child, references, source);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function isFieldTemplateSegment(value: unknown): value is ResolvedFieldSegment {
|
|
587
|
+
return (
|
|
588
|
+
typeof value === 'object' &&
|
|
589
|
+
value !== null &&
|
|
590
|
+
((value as {kind?: unknown}).kind === 'literal' ||
|
|
591
|
+
(value as {kind?: unknown}).kind === 'deferred')
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function collectExpressionVariableReferences(
|
|
596
|
+
expression: WorkflowExpression,
|
|
597
|
+
references: ReferencedVariable[],
|
|
598
|
+
source: {
|
|
599
|
+
readonly field: InterpolationUnresolvableError['field'];
|
|
600
|
+
readonly envKey?: string | undefined;
|
|
601
|
+
},
|
|
602
|
+
): void {
|
|
603
|
+
const keyAccess = analyzeContextKeyAccess(expression);
|
|
604
|
+
for (const reference of keyAccess.references) {
|
|
605
|
+
if (reference.root !== 'vars') continue;
|
|
606
|
+
references.push({
|
|
607
|
+
key: reference.key,
|
|
608
|
+
field: source.field,
|
|
609
|
+
source: expression.source,
|
|
610
|
+
envKey: source.envKey,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
537
615
|
function logTemplateDiagnostics(params: {
|
|
538
616
|
readonly workflowRunId: string;
|
|
539
617
|
readonly diagnostics: readonly (WorkflowStepTemplateDiagnostic & {
|
|
@@ -197,6 +197,7 @@ export async function bulkUpdateStepStatuses(
|
|
|
197
197
|
),
|
|
198
198
|
)
|
|
199
199
|
.returning({
|
|
200
|
+
id: stepAttempts.id,
|
|
200
201
|
stepId: stepAttempts.stepId,
|
|
201
202
|
attempt: stepAttempts.attempt,
|
|
202
203
|
logOutcome: stepAttempts.logOutcome,
|
|
@@ -205,6 +206,7 @@ export async function bulkUpdateStepStatuses(
|
|
|
205
206
|
if (finalizedAttempts.length > 0) {
|
|
206
207
|
for (const attempt of finalizedAttempts) {
|
|
207
208
|
await writeStepAttemptTerminatedOutbox(tx, {
|
|
209
|
+
stepAttemptId: attempt.id,
|
|
208
210
|
stepId: attempt.stepId,
|
|
209
211
|
attempt: attempt.attempt,
|
|
210
212
|
status: params.status,
|
|
@@ -459,6 +461,7 @@ export async function finishStepAttempt(params: FinishStepAttemptParams, tx: Tx)
|
|
|
459
461
|
),
|
|
460
462
|
)
|
|
461
463
|
.returning({
|
|
464
|
+
id: stepAttempts.id,
|
|
462
465
|
stepId: stepAttempts.stepId,
|
|
463
466
|
attempt: stepAttempts.attempt,
|
|
464
467
|
logOutcome: stepAttempts.logOutcome,
|
|
@@ -468,6 +471,7 @@ export async function finishStepAttempt(params: FinishStepAttemptParams, tx: Tx)
|
|
|
468
471
|
if (!row) return;
|
|
469
472
|
|
|
470
473
|
await writeStepAttemptTerminatedOutbox(tx, {
|
|
474
|
+
stepAttemptId: row.id,
|
|
471
475
|
stepId: row.stepId,
|
|
472
476
|
attempt: row.attempt,
|
|
473
477
|
status: params.status,
|
|
@@ -531,6 +535,21 @@ export async function getStepAttempts(jobId: string): Promise<StepAttempt[]> {
|
|
|
531
535
|
return rows.map((row) => toStepAttempt(row.stepAttempt));
|
|
532
536
|
}
|
|
533
537
|
|
|
538
|
+
/**
|
|
539
|
+
* Lists only the step attempt ids of a job, without materializing the full
|
|
540
|
+
* attempt entities. The session release sweep only needs the ids to release
|
|
541
|
+
* claims, so this keeps the per-job-terminated query off a full-row projection.
|
|
542
|
+
*/
|
|
543
|
+
export async function listStepAttemptIdsByJobId(jobId: string): Promise<string[]> {
|
|
544
|
+
const rows = await db()
|
|
545
|
+
.select({id: stepAttempts.id})
|
|
546
|
+
.from(stepAttempts)
|
|
547
|
+
.innerJoin(steps, eq(stepAttempts.stepId, steps.id))
|
|
548
|
+
.innerJoin(jobExecutions, eq(steps.jobExecutionId, jobExecutions.id))
|
|
549
|
+
.where(eq(jobExecutions.jobId, jobId));
|
|
550
|
+
return rows.map((row) => row.id);
|
|
551
|
+
}
|
|
552
|
+
|
|
534
553
|
export async function getStepAttemptsByJobIds(jobIds: string[]): Promise<StepAttempt[]> {
|
|
535
554
|
if (jobIds.length === 0) return [];
|
|
536
555
|
const rows = await db()
|
package/src/db/workflow-runs.ts
CHANGED
|
@@ -23,6 +23,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
23
23
|
getJobScope: vi.fn(),
|
|
24
24
|
getStepById: vi.fn(),
|
|
25
25
|
getStepByIdForJobExecution: vi.fn(),
|
|
26
|
+
listStepAttemptIdsByJobId: vi.fn(),
|
|
26
27
|
}));
|
|
27
28
|
|
|
28
29
|
vi.mock('#db/index.js', () => mocks);
|
|
@@ -47,12 +48,37 @@ const input = {
|
|
|
47
48
|
describe('Workflows inter-module presentation', () => {
|
|
48
49
|
beforeEach(() => {
|
|
49
50
|
mocks.getJobScope.mockReset();
|
|
51
|
+
mocks.listStepAttemptIdsByJobId.mockReset();
|
|
50
52
|
mocks.getStepById.mockReset();
|
|
51
53
|
mocks.getStepByIdForJobExecution.mockReset();
|
|
52
54
|
mocks.deliverEventToListener.mockReset();
|
|
53
55
|
mocks.deliverEventToListener.mockResolvedValue({buffered: true, skipped: false});
|
|
54
56
|
});
|
|
55
57
|
|
|
58
|
+
it('returns the step attempt ids of a job for the session release sweep', async () => {
|
|
59
|
+
const stepAttemptId = '00000000-0000-4000-8000-000000000010';
|
|
60
|
+
mocks.listStepAttemptIdsByJobId.mockResolvedValue([stepAttemptId]);
|
|
61
|
+
const presentation = createWorkflowsInterModulePresentation({
|
|
62
|
+
agent: {} as never,
|
|
63
|
+
definitions: {} as never,
|
|
64
|
+
integrations: {} as never,
|
|
65
|
+
projects: {} as never,
|
|
66
|
+
runners: {} as never,
|
|
67
|
+
secrets: {} as never,
|
|
68
|
+
workspaces: {getWorkspaceOperatingState: vi.fn()} as never,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const result = await presentation.handlers.listJobStepAttempts(
|
|
72
|
+
{jobId: '00000000-0000-4000-8000-000000000006'},
|
|
73
|
+
{signal: new AbortController().signal},
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(mocks.listStepAttemptIdsByJobId).toHaveBeenCalledWith(
|
|
77
|
+
'00000000-0000-4000-8000-000000000006',
|
|
78
|
+
);
|
|
79
|
+
expect(result).toEqual({stepAttemptIds: [stepAttemptId]});
|
|
80
|
+
});
|
|
81
|
+
|
|
56
82
|
it('returns only the resolved harness for Logs', async () => {
|
|
57
83
|
mocks.getStepById.mockResolvedValue({config: {harness: 'claude'}});
|
|
58
84
|
const presentation = createWorkflowsInterModulePresentation({
|
|
@@ -32,7 +32,12 @@ import {
|
|
|
32
32
|
} from '#core/index.js';
|
|
33
33
|
import {resolveWorkflowRunTriggerReference} from '#core/resolve-trigger-reference.js';
|
|
34
34
|
import {assertWorkspaceAdmitsNewJobs} from '#core/workspace-admission.js';
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
getJobScope,
|
|
37
|
+
getStepById,
|
|
38
|
+
getStepByIdForJobExecution,
|
|
39
|
+
listStepAttemptIdsByJobId,
|
|
40
|
+
} from '#db/index.js';
|
|
36
41
|
import {deliverEventToListener} from '#db/job-listener-events.js';
|
|
37
42
|
|
|
38
43
|
type WorkspaceAdmissionKnownError = InterModuleKnownErrorFor<
|
|
@@ -151,6 +156,9 @@ export function createWorkflowsInterModulePresentation(params: {
|
|
|
151
156
|
const parsed = harnessSchema.safeParse(step?.config.harness);
|
|
152
157
|
return {harness: parsed.success ? parsed.data : DEFAULT_HARNESS};
|
|
153
158
|
},
|
|
159
|
+
listJobStepAttempts: async ({jobId}) => {
|
|
160
|
+
return {stepAttemptIds: await listStepAttemptIdsByJobId(jobId)};
|
|
161
|
+
},
|
|
154
162
|
getLeasedAgentToolContext: async (input) => {
|
|
155
163
|
const method = workflowsInterModuleContract.methods.getLeasedAgentToolContext;
|
|
156
164
|
const {active: leaseIsActive} = await params.runners.getLeaseState({
|
|
@@ -54,6 +54,8 @@ const agentTestClient: AgentInterModuleClient = {
|
|
|
54
54
|
getValidationCatalog: vi.fn(),
|
|
55
55
|
resolveAgentConfig: vi.fn(),
|
|
56
56
|
resolveRuntimeCredentials,
|
|
57
|
+
claimSession: vi.fn(),
|
|
58
|
+
carryOverSessions: vi.fn(),
|
|
57
59
|
};
|
|
58
60
|
|
|
59
61
|
describe('GET /runs/jobs/current/agent-runtime-config', () => {
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_JOB_CHECKOUT,
|
|
3
|
+
type WorkflowJsonTemplateTree,
|
|
4
|
+
type WorkflowJsonValue,
|
|
5
|
+
type WorkflowModel,
|
|
6
|
+
} from '@shipfox/api-definitions-dto';
|
|
2
7
|
import {
|
|
3
8
|
createWorkflowExpression,
|
|
4
9
|
parseWorkflowTemplate,
|
|
@@ -41,7 +46,14 @@ interface TestCheckoutStep extends TestWorkflowStepBase {
|
|
|
41
46
|
readonly checkout: Checkout;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
interface TestToolStep extends TestWorkflowStepBase {
|
|
50
|
+
readonly tool: string;
|
|
51
|
+
readonly connection?: string | undefined;
|
|
52
|
+
readonly with?: WorkflowJsonValue | undefined;
|
|
53
|
+
readonly outputs?: Readonly<Record<string, string>> | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type TestWorkflowStep = TestRunStep | TestAgentStep | TestCheckoutStep | TestToolStep;
|
|
45
57
|
|
|
46
58
|
const DEFAULT_RUNNER_LABELS = ['ubuntu-latest'] as const;
|
|
47
59
|
|
|
@@ -171,6 +183,18 @@ function normalizeStep(step: TestWorkflowStep, jobId: string, stepIndex: number)
|
|
|
171
183
|
};
|
|
172
184
|
}
|
|
173
185
|
|
|
186
|
+
if ('tool' in step) {
|
|
187
|
+
return {
|
|
188
|
+
...base,
|
|
189
|
+
kind: 'tool',
|
|
190
|
+
tool: splitToolId(step.tool),
|
|
191
|
+
...(step.connection === undefined ? {} : {connection: step.connection}),
|
|
192
|
+
...(step.with === undefined ? {} : {with: step.with}),
|
|
193
|
+
...(step.outputs === undefined ? {} : {outputMappings: outputMappings(step.outputs)}),
|
|
194
|
+
...optionalToolTemplates(step),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
174
198
|
return {
|
|
175
199
|
...base,
|
|
176
200
|
kind: 'checkout',
|
|
@@ -274,6 +298,54 @@ function optionalAgentTemplates(step: TestAgentStep) {
|
|
|
274
298
|
};
|
|
275
299
|
}
|
|
276
300
|
|
|
301
|
+
function optionalToolTemplates(step: TestToolStep) {
|
|
302
|
+
const withTree = step.with === undefined ? undefined : withTemplateTree(step.with);
|
|
303
|
+
const name = step.name === undefined ? undefined : fieldTemplate('step.name', step.name);
|
|
304
|
+
if (withTree === undefined && name === undefined) return {};
|
|
305
|
+
return {
|
|
306
|
+
templates: {
|
|
307
|
+
...(withTree === undefined ? {} : {with: withTree}),
|
|
308
|
+
...(name === undefined ? {} : {name}),
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function withTemplateTree(value: WorkflowJsonValue): WorkflowJsonTemplateTree | undefined {
|
|
314
|
+
if (Array.isArray(value)) {
|
|
315
|
+
const trees = value.map((child) => withTemplateTree(child));
|
|
316
|
+
return trees.every((tree) => tree === undefined) ? undefined : trees;
|
|
317
|
+
}
|
|
318
|
+
if (typeof value === 'object' && value !== null) {
|
|
319
|
+
const entries = Object.entries(value).flatMap(([key, child]) => {
|
|
320
|
+
const tree = withTemplateTree(child);
|
|
321
|
+
return tree === undefined ? [] : [[key, tree] as const];
|
|
322
|
+
});
|
|
323
|
+
return entries.length === 0 ? undefined : Object.fromEntries(entries);
|
|
324
|
+
}
|
|
325
|
+
if (typeof value !== 'string') return undefined;
|
|
326
|
+
return fieldTemplate('tool.with', value);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function outputMappings(outputs: Readonly<Record<string, string>>) {
|
|
330
|
+
return Object.fromEntries(
|
|
331
|
+
Object.entries(outputs).map(([key, source]) => {
|
|
332
|
+
const template = fieldTemplate('tool.outputs', source);
|
|
333
|
+
if (template === undefined || template.length !== 1 || template[0]?.kind !== 'deferred') {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`Expected test tool output mapping to be exactly one expression for ${key}: ${source}`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return [key, template[0].expression];
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function splitToolId(source: string): {readonly id: string; readonly method?: string} {
|
|
344
|
+
const dotIndex = source.indexOf('.');
|
|
345
|
+
if (dotIndex < 1 || dotIndex === source.length - 1) return {id: source};
|
|
346
|
+
return {id: source.slice(0, dotIndex), method: source.slice(dotIndex + 1)};
|
|
347
|
+
}
|
|
348
|
+
|
|
277
349
|
function envTemplates(env: WorkflowModel['env'] | undefined): WorkflowEnvTemplates | undefined {
|
|
278
350
|
if (env === undefined) return undefined;
|
|
279
351
|
|
|
@@ -46,6 +46,8 @@ export const agentTestClient: AgentInterModuleClient = {
|
|
|
46
46
|
credentials: {api_key: 'test-agent-credential'},
|
|
47
47
|
});
|
|
48
48
|
},
|
|
49
|
+
claimSession: vi.fn(),
|
|
50
|
+
carryOverSessions: vi.fn(),
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
export const resolveTestAgentDefaults: AgentDefaultsResolver = (config) => {
|