@shipfox/api-workflows 14.0.0 → 15.0.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 +30 -0
- package/dist/core/entities/workflow-run.d.ts +2 -2
- package/dist/core/entities/workflow-run.d.ts.map +1 -1
- package/dist/core/entities/workflow-run.js.map +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/run-workflow.d.ts +26 -1
- package/dist/core/run-workflow.d.ts.map +1 -1
- package/dist/core/run-workflow.js +26 -0
- package/dist/core/run-workflow.js.map +1 -1
- package/dist/db/workflow-runs/run-create.d.ts +4 -1
- package/dist/db/workflow-runs/run-create.d.ts.map +1 -1
- package/dist/db/workflow-runs/run-create.js +14 -1
- package/dist/db/workflow-runs/run-create.js.map +1 -1
- package/dist/presentation/inter-module.d.ts +1 -0
- package/dist/presentation/inter-module.d.ts.map +1 -1
- package/dist/presentation/inter-module.js +41 -4
- package/dist/presentation/inter-module.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +16 -16
- package/src/core/entities/workflow-run.ts +4 -2
- package/src/core/index.ts +2 -2
- package/src/core/run-workflow.test.ts +101 -2
- package/src/core/run-workflow.ts +58 -2
- package/src/db/workflow-runs/run-create.test.ts +48 -0
- package/src/db/workflow-runs/run-create.ts +23 -1
- package/src/presentation/inter-module.test.ts +131 -1
- package/src/presentation/inter-module.ts +48 -3
- 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": "15.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -23,30 +23,30 @@
|
|
|
23
23
|
"drizzle-orm": "^0.45.2",
|
|
24
24
|
"js-yaml": "^4.1.0",
|
|
25
25
|
"zod": "^4.4.3",
|
|
26
|
-
"@shipfox/api-auth-context": "
|
|
27
|
-
"@shipfox/api-auth-dto": "
|
|
26
|
+
"@shipfox/api-auth-context": "15.0.0",
|
|
27
|
+
"@shipfox/api-auth-dto": "15.0.0",
|
|
28
|
+
"@shipfox/api-agent-dto": "15.0.0",
|
|
28
29
|
"@shipfox/annotations-dto": "12.3.0",
|
|
29
|
-
"@shipfox/api-
|
|
30
|
-
"@shipfox/api-
|
|
31
|
-
"@shipfox/api-integration-core-dto": "
|
|
30
|
+
"@shipfox/api-definitions-dto": "15.0.0",
|
|
31
|
+
"@shipfox/api-projects-dto": "15.0.0",
|
|
32
|
+
"@shipfox/api-integration-core-dto": "15.0.0",
|
|
32
33
|
"@shipfox/api-runners-dto": "14.0.0",
|
|
33
|
-
"@shipfox/api-projects-dto": "14.0.0",
|
|
34
|
-
"@shipfox/api-workflows-dto": "14.0.0",
|
|
35
34
|
"@shipfox/api-secrets-dto": "12.0.0",
|
|
36
|
-
"@shipfox/api-
|
|
35
|
+
"@shipfox/api-workflows-dto": "15.0.0",
|
|
36
|
+
"@shipfox/api-workspaces-dto": "15.0.0",
|
|
37
|
+
"@shipfox/config": "1.2.4",
|
|
37
38
|
"@shipfox/inter-module": "0.2.3",
|
|
38
|
-
"@shipfox/expression": "2.
|
|
39
|
+
"@shipfox/expression": "2.3.0",
|
|
39
40
|
"@shipfox/node-drizzle": "0.3.5",
|
|
40
41
|
"@shipfox/node-error-monitoring": "0.3.0",
|
|
41
|
-
"@shipfox/node-fastify": "0.4.
|
|
42
|
-
"@shipfox/node-module": "1.0.
|
|
43
|
-
"@shipfox/node-opentelemetry": "0.6.
|
|
42
|
+
"@shipfox/node-fastify": "0.4.3",
|
|
43
|
+
"@shipfox/node-module": "1.0.7",
|
|
44
|
+
"@shipfox/node-opentelemetry": "0.6.5",
|
|
44
45
|
"@shipfox/node-outbox": "0.2.6",
|
|
45
|
-
"@shipfox/config": "1.2.4",
|
|
46
46
|
"@shipfox/node-postgres": "0.5.0",
|
|
47
|
-
"@shipfox/node-temporal": "0.4.
|
|
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.2.0"
|
|
50
50
|
},
|
|
51
51
|
"imports": {
|
|
52
52
|
"#*": "./dist/*"
|
|
@@ -58,14 +58,16 @@ export type TriggerPayload =
|
|
|
58
58
|
source: 'manual';
|
|
59
59
|
provider?: 'manual' | undefined;
|
|
60
60
|
event: 'fire';
|
|
61
|
-
|
|
61
|
+
// A dev trigger has no subscription row, so the id is optional here.
|
|
62
|
+
subscriptionId?: string | undefined;
|
|
62
63
|
userId: string;
|
|
63
64
|
}
|
|
64
65
|
| {
|
|
65
66
|
source: 'cron';
|
|
66
67
|
provider?: 'cron' | undefined;
|
|
67
68
|
event: 'tick';
|
|
68
|
-
|
|
69
|
+
// A dev trigger has no schedule row, so the id is optional here.
|
|
70
|
+
scheduleId?: string | undefined;
|
|
69
71
|
}
|
|
70
72
|
// Integration sources (github, gitlab, sentry, …) flow through opaquely: the
|
|
71
73
|
// run records what fired it and carries the raw event payload, without the
|
package/src/core/index.ts
CHANGED
|
@@ -35,8 +35,8 @@ export {
|
|
|
35
35
|
deriveJobSuccess,
|
|
36
36
|
type JobActivationDecision,
|
|
37
37
|
} from './job-transition/index.js';
|
|
38
|
-
export type {RunWorkflowParams} from './run-workflow.js';
|
|
39
|
-
export {runWorkflow} from './run-workflow.js';
|
|
38
|
+
export type {RunDevWorkflowParams, RunWorkflowParams} from './run-workflow.js';
|
|
39
|
+
export {runDevWorkflow, runWorkflow} from './run-workflow.js';
|
|
40
40
|
export {
|
|
41
41
|
type MaterializedWorkflowJob,
|
|
42
42
|
type MaterializedWorkflowStep,
|
|
@@ -3,9 +3,9 @@ import {createWorkflowModelSnapshot, type WorkflowModel} from '@shipfox/api-defi
|
|
|
3
3
|
import type {DefinitionsInterModuleClient} from '@shipfox/api-definitions-dto/inter-module';
|
|
4
4
|
import {agentValidationCatalog} from '#test/fixtures/agent-inter-module.js';
|
|
5
5
|
import {workflowModel} from '#test/index.js';
|
|
6
|
-
import type {TriggerPayload} from './entities/workflow-run.js';
|
|
6
|
+
import type {TriggerPayload, WorkflowRunDevSource} from './entities/workflow-run.js';
|
|
7
7
|
import {DefinitionNotFoundError, ProjectMismatchError} from './errors.js';
|
|
8
|
-
import {runWorkflow} from './run-workflow.js';
|
|
8
|
+
import {runDevWorkflow, runWorkflow} from './run-workflow.js';
|
|
9
9
|
|
|
10
10
|
const mockResolveAgentConfig = vi.hoisted(() =>
|
|
11
11
|
vi.fn().mockResolvedValue({
|
|
@@ -248,3 +248,102 @@ describe('runWorkflow', () => {
|
|
|
248
248
|
expect(run.inputs).toEqual({env: 'staging'});
|
|
249
249
|
});
|
|
250
250
|
});
|
|
251
|
+
|
|
252
|
+
describe('runDevWorkflow', () => {
|
|
253
|
+
let workspaceId: string;
|
|
254
|
+
let projectId: string;
|
|
255
|
+
const agent: AgentInterModuleClient = {
|
|
256
|
+
getValidationCatalog: vi.fn().mockResolvedValue(agentValidationCatalog),
|
|
257
|
+
resolveAgentConfig: mockResolveAgentConfig,
|
|
258
|
+
resolveRuntimeCredentials: vi.fn(),
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
beforeEach(() => {
|
|
262
|
+
workspaceId = crypto.randomUUID();
|
|
263
|
+
projectId = crypto.randomUUID();
|
|
264
|
+
mockResolveAgentConfig.mockClear();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const devSource: WorkflowRunDevSource = {
|
|
268
|
+
ref: 'fix-triage-prompt',
|
|
269
|
+
commit: 'a'.repeat(40),
|
|
270
|
+
configPath: '.shipfox/workflows/triage-sentry.yml',
|
|
271
|
+
initiatedByUserId: crypto.randomUUID(),
|
|
272
|
+
replayOfEventId: null,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
function devPayload(): TriggerPayload {
|
|
276
|
+
return {source: 'manual', event: 'fire', userId: crypto.randomUUID()};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function buildDevRun(workflowId: string, triggerPayload: TriggerPayload = devPayload()) {
|
|
280
|
+
return runDevWorkflow(agent, {
|
|
281
|
+
workspaceId,
|
|
282
|
+
projectId,
|
|
283
|
+
workflowId,
|
|
284
|
+
model: createWorkflowModelSnapshot(workflowModel({name: 'Dev Workflow'})),
|
|
285
|
+
sourceSnapshot: {content: 'name: Dev Workflow\n', format: 'yaml'},
|
|
286
|
+
devSource,
|
|
287
|
+
triggerPayload,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
test('creates a dev run with the lineage id as definition_id and dev provenance', async () => {
|
|
292
|
+
const workflowId = crypto.randomUUID();
|
|
293
|
+
const triggerPayload = devPayload();
|
|
294
|
+
const run = await buildDevRun(workflowId, triggerPayload);
|
|
295
|
+
|
|
296
|
+
expect(run.definitionId).toBe(workflowId);
|
|
297
|
+
expect(run.origin).toBe('dev');
|
|
298
|
+
expect(run.devSource).toEqual(devSource);
|
|
299
|
+
expect(run.name).toBe('Dev Workflow');
|
|
300
|
+
expect(run.workflowName).toBe('Dev Workflow');
|
|
301
|
+
expect(run.sourceSnapshot).toEqual({content: 'name: Dev Workflow\n', format: 'yaml'});
|
|
302
|
+
expect(run.triggerPayload).toEqual(triggerPayload);
|
|
303
|
+
expect(run.triggerIdempotencyKey).toBeNull();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('continues the lineage number sequence across dev and synced runs of one workflow file', async () => {
|
|
307
|
+
const workflowId = crypto.randomUUID();
|
|
308
|
+
// The file exists on the branch only: the dev run opens the sequence.
|
|
309
|
+
const devRun = await buildDevRun(workflowId);
|
|
310
|
+
expect(devRun.number).toBe(1);
|
|
311
|
+
|
|
312
|
+
// After merge, the synced run of the same lineage continues the sequence.
|
|
313
|
+
const definition = buildDefinition({
|
|
314
|
+
projectId,
|
|
315
|
+
id: crypto.randomUUID(),
|
|
316
|
+
workflowId,
|
|
317
|
+
model: workflowModel({name: 'Dev Workflow'}),
|
|
318
|
+
});
|
|
319
|
+
definitionResponse = definition;
|
|
320
|
+
const syncedRun = await runWorkflow(definitions, {
|
|
321
|
+
workspaceId,
|
|
322
|
+
projectId,
|
|
323
|
+
definitionId: definition.id,
|
|
324
|
+
triggerPayload: manualPayload(),
|
|
325
|
+
agent,
|
|
326
|
+
});
|
|
327
|
+
expect(syncedRun.definitionId).toBe(workflowId);
|
|
328
|
+
expect(syncedRun.origin).toBe('synced');
|
|
329
|
+
expect(syncedRun.number).toBe(2);
|
|
330
|
+
|
|
331
|
+
// And a later dev run of the same file keeps the sequence going.
|
|
332
|
+
const secondDevRun = await buildDevRun(workflowId);
|
|
333
|
+
expect(secondDevRun.number).toBe(3);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('accepts a manual payload without a subscription id, as a dev trigger has no row', async () => {
|
|
337
|
+
const workflowId = crypto.randomUUID();
|
|
338
|
+
const triggerPayload = {
|
|
339
|
+
source: 'manual' as const,
|
|
340
|
+
event: 'fire' as const,
|
|
341
|
+
userId: crypto.randomUUID(),
|
|
342
|
+
};
|
|
343
|
+
const run = await buildDevRun(workflowId, triggerPayload);
|
|
344
|
+
|
|
345
|
+
expect(run.triggerProvider).toBeNull();
|
|
346
|
+
expect(run.triggerSource).toBe('manual');
|
|
347
|
+
expect(run.triggerPayload).toEqual(triggerPayload);
|
|
348
|
+
});
|
|
349
|
+
});
|
package/src/core/run-workflow.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import type {AgentInterModuleClient} from '@shipfox/api-agent-dto/inter-module';
|
|
2
|
-
import {workflowModelFromSnapshot} from '@shipfox/api-definitions-dto';
|
|
2
|
+
import {type WorkflowModelSnapshot, workflowModelFromSnapshot} from '@shipfox/api-definitions-dto';
|
|
3
3
|
import type {DefinitionsInterModuleClient} from '@shipfox/api-definitions-dto/inter-module';
|
|
4
4
|
import type {IntegrationsModuleClient} from '@shipfox/api-integration-core-dto/inter-module';
|
|
5
5
|
import type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module';
|
|
6
6
|
import type {SecretsInterModuleClient} from '@shipfox/api-secrets-dto/inter-module';
|
|
7
7
|
import {createWorkflowRun} from '#db/workflow-runs.js';
|
|
8
8
|
import {createAgentDefaultsResolver} from './agent-defaults.js';
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
TriggerPayload,
|
|
11
|
+
WorkflowRun,
|
|
12
|
+
WorkflowRunDevSource,
|
|
13
|
+
WorkflowSourceSnapshot,
|
|
14
|
+
} from './entities/workflow-run.js';
|
|
10
15
|
import {DefinitionNotFoundError, ProjectMismatchError} from './errors.js';
|
|
11
16
|
import {modelHasAgentStep} from './step-config/materialize-workflow-model.js';
|
|
12
17
|
|
|
@@ -58,3 +63,54 @@ export async function runWorkflow(
|
|
|
58
63
|
projects: params.projects,
|
|
59
64
|
});
|
|
60
65
|
}
|
|
66
|
+
|
|
67
|
+
export interface RunDevWorkflowParams {
|
|
68
|
+
workspaceId: string;
|
|
69
|
+
projectId: string;
|
|
70
|
+
/** Workflow lineage id, becomes the run's definition_id. */
|
|
71
|
+
workflowId: string;
|
|
72
|
+
model: WorkflowModelSnapshot;
|
|
73
|
+
sourceSnapshot: WorkflowSourceSnapshot;
|
|
74
|
+
devSource: WorkflowRunDevSource;
|
|
75
|
+
triggerPayload: TriggerPayload;
|
|
76
|
+
triggerConnectionId?: string | undefined;
|
|
77
|
+
inputs?: Record<string, unknown> | undefined;
|
|
78
|
+
integrations?: IntegrationsModuleClient | undefined;
|
|
79
|
+
projects?: ProjectsModuleClient | undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Creates a dev run from an inline model and snapshot. Unlike
|
|
84
|
+
* trigger-originated runs, dev runs do not carry an idempotency key because
|
|
85
|
+
* each call represents a distinct intent. The lineage id is used as the run's
|
|
86
|
+
* definition_id so dev runs share the workflow numbering sequence before and
|
|
87
|
+
* after the file merges.
|
|
88
|
+
*/
|
|
89
|
+
export function runDevWorkflow(
|
|
90
|
+
agent: AgentInterModuleClient,
|
|
91
|
+
params: RunDevWorkflowParams,
|
|
92
|
+
options: {secrets?: Pick<SecretsInterModuleClient, 'getVariablesByNamespace'>} = {},
|
|
93
|
+
): Promise<WorkflowRun> {
|
|
94
|
+
const model = workflowModelFromSnapshot(params.model);
|
|
95
|
+
const resolveAgentDefaults = createAgentDefaultsResolver(
|
|
96
|
+
agent,
|
|
97
|
+
modelHasAgentStep(model) ? params.workspaceId : null,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
return createWorkflowRun({
|
|
101
|
+
workspaceId: params.workspaceId,
|
|
102
|
+
projectId: params.projectId,
|
|
103
|
+
definitionId: params.workflowId,
|
|
104
|
+
model,
|
|
105
|
+
triggerPayload: params.triggerPayload,
|
|
106
|
+
triggerConnectionId: params.triggerConnectionId,
|
|
107
|
+
inputs: params.inputs,
|
|
108
|
+
sourceSnapshot: params.sourceSnapshot,
|
|
109
|
+
origin: 'dev',
|
|
110
|
+
devSource: params.devSource,
|
|
111
|
+
resolveAgentDefaults,
|
|
112
|
+
secrets: options.secrets,
|
|
113
|
+
integrations: params.integrations,
|
|
114
|
+
projects: params.projects,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -87,6 +87,54 @@ describe('workflow run queries', () => {
|
|
|
87
87
|
.where(eq(workflowRuns.id, run.id));
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
test('persists a dev origin with its dev source', async () => {
|
|
91
|
+
const devSource = {
|
|
92
|
+
ref: 'fix-triage-prompt',
|
|
93
|
+
commit: 'a'.repeat(40),
|
|
94
|
+
configPath: '.shipfox/workflows/triage-sentry.yml',
|
|
95
|
+
initiatedByUserId: crypto.randomUUID(),
|
|
96
|
+
replayOfEventId: null,
|
|
97
|
+
};
|
|
98
|
+
const run = await createWorkflowRun({
|
|
99
|
+
workspaceId,
|
|
100
|
+
projectId,
|
|
101
|
+
definitionId,
|
|
102
|
+
model: buildModel(),
|
|
103
|
+
triggerPayload: {
|
|
104
|
+
source: 'manual',
|
|
105
|
+
event: 'fire',
|
|
106
|
+
userId: crypto.randomUUID(),
|
|
107
|
+
},
|
|
108
|
+
origin: 'dev',
|
|
109
|
+
devSource,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(run.origin).toBe('dev');
|
|
113
|
+
expect(run.devSource).toEqual(devSource);
|
|
114
|
+
await expect(getWorkflowRunById(run.id)).resolves.toMatchObject({
|
|
115
|
+
origin: 'dev',
|
|
116
|
+
devSource,
|
|
117
|
+
});
|
|
118
|
+
// The persisted jsonb carries snake_case keys, matching the read boundary.
|
|
119
|
+
await expect(
|
|
120
|
+
db()
|
|
121
|
+
.select({origin: workflowRuns.origin, devSource: workflowRuns.devSource})
|
|
122
|
+
.from(workflowRuns)
|
|
123
|
+
.where(eq(workflowRuns.id, run.id)),
|
|
124
|
+
).resolves.toEqual([
|
|
125
|
+
{
|
|
126
|
+
origin: 'dev',
|
|
127
|
+
devSource: {
|
|
128
|
+
ref: devSource.ref,
|
|
129
|
+
commit: devSource.commit,
|
|
130
|
+
config_path: devSource.configPath,
|
|
131
|
+
initiated_by_user_id: devSource.initiatedByUserId,
|
|
132
|
+
replay_of_event_id: devSource.replayOfEventId,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
|
|
90
138
|
test('persists normalized integration trigger facts alongside the payload', async () => {
|
|
91
139
|
const triggerConnectionId = crypto.randomUUID();
|
|
92
140
|
const integrations = {
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
import type {
|
|
22
22
|
TriggerPayload,
|
|
23
23
|
WorkflowRun,
|
|
24
|
+
WorkflowRunDevSource,
|
|
25
|
+
WorkflowRunOrigin,
|
|
24
26
|
WorkflowSourceSnapshot,
|
|
25
27
|
} from '#core/entities/workflow-run.js';
|
|
26
28
|
import {InterpolationUnresolvableError} from '#core/errors.js';
|
|
@@ -40,7 +42,7 @@ import {
|
|
|
40
42
|
import {db, type Tx} from '../db.js';
|
|
41
43
|
import {workflowRunAttempts} from '../schema/workflow-run-attempts.js';
|
|
42
44
|
import {workflowRunCounters} from '../schema/workflow-run-counters.js';
|
|
43
|
-
import {toWorkflowRun, workflowRuns} from '../schema/workflow-runs.js';
|
|
45
|
+
import {toWorkflowRun, type WorkflowRunDevSourceDb, workflowRuns} from '../schema/workflow-runs.js';
|
|
44
46
|
import {type MaterializedRunGraphJob, persistMaterializedRunGraph} from './run-graph.js';
|
|
45
47
|
|
|
46
48
|
export type WorkflowModelJob = WorkflowModel['jobs'][number];
|
|
@@ -63,6 +65,9 @@ export interface CreateWorkflowRunParams {
|
|
|
63
65
|
inputs?: Record<string, unknown> | undefined;
|
|
64
66
|
sourceSnapshot?: WorkflowSourceSnapshot | null | undefined;
|
|
65
67
|
triggerIdempotencyKey?: string | undefined;
|
|
68
|
+
/** Run provenance. Defaults to a synced run with no dev source. */
|
|
69
|
+
origin?: WorkflowRunOrigin | undefined;
|
|
70
|
+
devSource?: WorkflowRunDevSource | null | undefined;
|
|
66
71
|
resolveAgentDefaults?: AgentDefaultsResolver | undefined;
|
|
67
72
|
secrets?: Pick<SecretsInterModuleClient, 'getVariablesByNamespace'> | undefined;
|
|
68
73
|
integrations?: IntegrationsModuleClient | undefined;
|
|
@@ -132,6 +137,11 @@ export async function createWorkflowRun(params: CreateWorkflowRunParams): Promis
|
|
|
132
137
|
inputs: params.inputs ?? null,
|
|
133
138
|
sourceSnapshot: params.sourceSnapshot ?? null,
|
|
134
139
|
triggerIdempotencyKey: params.triggerIdempotencyKey ?? null,
|
|
140
|
+
origin: params.origin ?? 'synced',
|
|
141
|
+
devSource:
|
|
142
|
+
params.devSource === undefined || params.devSource === null
|
|
143
|
+
? null
|
|
144
|
+
: toWorkflowRunDevSourceDb(params.devSource),
|
|
135
145
|
})
|
|
136
146
|
.onConflictDoNothing({target: workflowRuns.triggerIdempotencyKey})
|
|
137
147
|
.returning();
|
|
@@ -281,6 +291,18 @@ async function allocateWorkflowRunNumber(tx: Tx, definitionId: string): Promise<
|
|
|
281
291
|
return counterRow.number;
|
|
282
292
|
}
|
|
283
293
|
|
|
294
|
+
// The run entity carries camelCase dev-source fields while the persisted jsonb uses
|
|
295
|
+
// snake_case keys, so the write boundary maps explicitly rather than spreading.
|
|
296
|
+
function toWorkflowRunDevSourceDb(source: WorkflowRunDevSource): WorkflowRunDevSourceDb {
|
|
297
|
+
return {
|
|
298
|
+
ref: source.ref,
|
|
299
|
+
commit: source.commit,
|
|
300
|
+
config_path: source.configPath,
|
|
301
|
+
initiated_by_user_id: source.initiatedByUserId,
|
|
302
|
+
replay_of_event_id: source.replayOfEventId,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
284
306
|
export async function loadReferencedVariables(params: {
|
|
285
307
|
readonly model: WorkflowModel;
|
|
286
308
|
readonly jobs?: readonly WorkflowModelJob[] | undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {createWorkflowModelSnapshot} from '@shipfox/api-definitions-dto';
|
|
1
2
|
import {workflowsInterModuleContract} from '@shipfox/api-workflows-dto/inter-module';
|
|
2
3
|
import {workspacesInterModuleContract} from '@shipfox/api-workspaces-dto/inter-module';
|
|
3
4
|
import {createInterModuleKnownError, isInterModuleKnownError} from '@shipfox/inter-module';
|
|
@@ -9,7 +10,13 @@ import {
|
|
|
9
10
|
InterpolationUnresolvableError,
|
|
10
11
|
ProjectMismatchError,
|
|
11
12
|
} from '#core/index.js';
|
|
12
|
-
import {
|
|
13
|
+
import {getWorkflowRunById} from '#db/workflow-runs.js';
|
|
14
|
+
import {workflowModel} from '#test/index.js';
|
|
15
|
+
import {
|
|
16
|
+
createWorkflowsInterModulePresentation,
|
|
17
|
+
toStartDevRunKnownError,
|
|
18
|
+
toStartRunKnownError,
|
|
19
|
+
} from './inter-module.js';
|
|
13
20
|
|
|
14
21
|
const mocks = vi.hoisted(() => ({
|
|
15
22
|
deliverEventToListener: vi.fn(),
|
|
@@ -188,6 +195,129 @@ describe('Workflows inter-module presentation', () => {
|
|
|
188
195
|
).toBe('workspace-not-found');
|
|
189
196
|
});
|
|
190
197
|
|
|
198
|
+
const devInput = {
|
|
199
|
+
workspaceId: input.workspaceId,
|
|
200
|
+
projectId: input.projectId,
|
|
201
|
+
workflowId: '00000000-0000-4000-8000-000000000006',
|
|
202
|
+
model: createWorkflowModelSnapshot(
|
|
203
|
+
workflowModel({name: 'Dev Workflow', jobs: {build: {steps: [{run: 'echo dev'}]}}}),
|
|
204
|
+
),
|
|
205
|
+
sourceSnapshot: {content: 'name: Dev Workflow\n', format: 'yaml' as const},
|
|
206
|
+
devSource: {
|
|
207
|
+
ref: 'fix-triage-prompt',
|
|
208
|
+
commit: 'a'.repeat(40),
|
|
209
|
+
configPath: '.shipfox/workflows/triage-sentry.yml',
|
|
210
|
+
initiatedByUserId: input.triggerPayload.userId,
|
|
211
|
+
},
|
|
212
|
+
triggerPayload: {
|
|
213
|
+
source: 'manual' as const,
|
|
214
|
+
event: 'fire' as const,
|
|
215
|
+
userId: input.triggerPayload.userId,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
test.each([
|
|
220
|
+
['agent-config-unresolvable', () => new AgentConfigUnresolvableError(devInput.workflowId)],
|
|
221
|
+
[
|
|
222
|
+
'agent-integration-materialization-failed',
|
|
223
|
+
() => new AgentIntegrationMaterializationError('integration unavailable'),
|
|
224
|
+
],
|
|
225
|
+
[
|
|
226
|
+
'interpolation-unresolvable',
|
|
227
|
+
() =>
|
|
228
|
+
new InterpolationUnresolvableError(devInput.workflowId, {
|
|
229
|
+
field: 'env',
|
|
230
|
+
source: 'event.ref',
|
|
231
|
+
envKey: 'REF',
|
|
232
|
+
}),
|
|
233
|
+
],
|
|
234
|
+
['invalid-job-runner-labels', () => new InvalidJobRunnerLabelsError(['gpu'])],
|
|
235
|
+
] as const)('maps %s to the published dev-run contract error', (code, error) => {
|
|
236
|
+
const result = toStartDevRunKnownError(error());
|
|
237
|
+
|
|
238
|
+
expect(
|
|
239
|
+
isInterModuleKnownError(workflowsInterModuleContract.methods.startDevRun, result) &&
|
|
240
|
+
result.code,
|
|
241
|
+
).toBe(code);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test.each([
|
|
245
|
+
['definition-not-found', () => new DefinitionNotFoundError(devInput.workflowId)],
|
|
246
|
+
['project-mismatch', () => new ProjectMismatchError(devInput.projectId, devInput.workflowId)],
|
|
247
|
+
] as const)('does not map %s on the dev-run contract', (_code, error) => {
|
|
248
|
+
const result = toStartDevRunKnownError(error());
|
|
249
|
+
|
|
250
|
+
expect(isInterModuleKnownError(workflowsInterModuleContract.methods.startDevRun, result)).toBe(
|
|
251
|
+
false,
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test.each([
|
|
256
|
+
['suspended', 'workspace-suspended'],
|
|
257
|
+
['deleted', 'workspace-deleted'],
|
|
258
|
+
] as const)('rejects dev runs for %s workspaces before creating a run', async (status, code) => {
|
|
259
|
+
const getWorkspaceOperatingState = vi.fn().mockResolvedValue({status});
|
|
260
|
+
const presentation = createWorkflowsInterModulePresentation({
|
|
261
|
+
agent: {} as never,
|
|
262
|
+
definitions: {} as never,
|
|
263
|
+
integrations: {} as never,
|
|
264
|
+
projects: {} as never,
|
|
265
|
+
runners: {} as never,
|
|
266
|
+
secrets: {} as never,
|
|
267
|
+
workspaces: {getWorkspaceOperatingState} as never,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const error = await Promise.resolve(
|
|
271
|
+
presentation.handlers.startDevRun(devInput, {
|
|
272
|
+
signal: new AbortController().signal,
|
|
273
|
+
}),
|
|
274
|
+
).catch((caught: unknown) => caught);
|
|
275
|
+
|
|
276
|
+
expect(getWorkspaceOperatingState).toHaveBeenCalledWith({workspaceId: devInput.workspaceId});
|
|
277
|
+
expect(
|
|
278
|
+
isInterModuleKnownError(workflowsInterModuleContract.methods.startDevRun, error) &&
|
|
279
|
+
error.code,
|
|
280
|
+
).toBe(code);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('creates a dev run with dev provenance and lineage numbering', async () => {
|
|
284
|
+
const presentation = createWorkflowsInterModulePresentation({
|
|
285
|
+
agent: {} as never,
|
|
286
|
+
definitions: {} as never,
|
|
287
|
+
integrations: {} as never,
|
|
288
|
+
projects: {} as never,
|
|
289
|
+
runners: {} as never,
|
|
290
|
+
secrets: {} as never,
|
|
291
|
+
workspaces: {
|
|
292
|
+
getWorkspaceOperatingState: vi.fn().mockResolvedValue({status: 'active'}),
|
|
293
|
+
} as never,
|
|
294
|
+
});
|
|
295
|
+
// A fresh lineage id keeps the assertion independent of counter rows left
|
|
296
|
+
// behind by earlier suite runs against the shared test database.
|
|
297
|
+
const workflowId = crypto.randomUUID();
|
|
298
|
+
const runInput = {...devInput, workflowId};
|
|
299
|
+
|
|
300
|
+
const first = await presentation.handlers.startDevRun(runInput, {
|
|
301
|
+
signal: new AbortController().signal,
|
|
302
|
+
});
|
|
303
|
+
const second = await presentation.handlers.startDevRun(runInput, {
|
|
304
|
+
signal: new AbortController().signal,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const firstRun = await getWorkflowRunById(first.id);
|
|
308
|
+
const secondRun = await getWorkflowRunById(second.id);
|
|
309
|
+
expect(firstRun).toMatchObject({
|
|
310
|
+
definitionId: workflowId,
|
|
311
|
+
origin: 'dev',
|
|
312
|
+
devSource: {...devInput.devSource, replayOfEventId: null},
|
|
313
|
+
number: 1,
|
|
314
|
+
name: 'Dev Workflow',
|
|
315
|
+
sourceSnapshot: devInput.sourceSnapshot,
|
|
316
|
+
});
|
|
317
|
+
// Each call is a distinct intent, so the lineage number sequence continues.
|
|
318
|
+
expect(secondRun).toMatchObject({definitionId: workflowId, origin: 'dev', number: 2});
|
|
319
|
+
});
|
|
320
|
+
|
|
191
321
|
test.each([
|
|
192
322
|
['suspended', 'workspace-suspended'],
|
|
193
323
|
['deleted', 'workspace-deleted'],
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
DefinitionNotFoundError,
|
|
28
28
|
InterpolationUnresolvableError,
|
|
29
29
|
ProjectMismatchError,
|
|
30
|
+
runDevWorkflow,
|
|
30
31
|
runWorkflow,
|
|
31
32
|
} from '#core/index.js';
|
|
32
33
|
import {resolveWorkflowRunTriggerReference} from '#core/resolve-trigger-reference.js';
|
|
@@ -72,6 +73,34 @@ export function createWorkflowsInterModulePresentation(params: {
|
|
|
72
73
|
throw toStartRunKnownError(error, input.definitionId);
|
|
73
74
|
}
|
|
74
75
|
},
|
|
76
|
+
startDevRun: async (input) => {
|
|
77
|
+
try {
|
|
78
|
+
await assertWorkspaceAdmitsNewJobs(params.workspaces, input.workspaceId);
|
|
79
|
+
const run = await runDevWorkflow(
|
|
80
|
+
params.agent,
|
|
81
|
+
{
|
|
82
|
+
workspaceId: input.workspaceId,
|
|
83
|
+
projectId: input.projectId,
|
|
84
|
+
workflowId: input.workflowId,
|
|
85
|
+
model: input.model,
|
|
86
|
+
sourceSnapshot: input.sourceSnapshot,
|
|
87
|
+
devSource: {
|
|
88
|
+
...input.devSource,
|
|
89
|
+
replayOfEventId: input.devSource.replayOfEventId ?? null,
|
|
90
|
+
},
|
|
91
|
+
triggerPayload: input.triggerPayload,
|
|
92
|
+
triggerConnectionId: input.triggerConnectionId,
|
|
93
|
+
inputs: input.inputs,
|
|
94
|
+
integrations: params.integrations,
|
|
95
|
+
projects: params.projects,
|
|
96
|
+
},
|
|
97
|
+
{secrets: params.secrets},
|
|
98
|
+
);
|
|
99
|
+
return {id: run.id, name: run.name};
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw toStartDevRunKnownError(error);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
75
104
|
resolveWorkflowRunTriggerReference: async (input) =>
|
|
76
105
|
await resolveWorkflowRunTriggerReference({
|
|
77
106
|
workspaceId: input.workspaceId,
|
|
@@ -158,14 +187,29 @@ export function createWorkflowsInterModulePresentation(params: {
|
|
|
158
187
|
|
|
159
188
|
export function toStartRunKnownError(error: unknown, definitionId: string): unknown {
|
|
160
189
|
const method = workflowsInterModuleContract.methods.startRunFromTrigger;
|
|
161
|
-
const
|
|
162
|
-
if (
|
|
190
|
+
const mapped = toRunCreationKnownError(method, error);
|
|
191
|
+
if (mapped !== undefined) return mapped;
|
|
163
192
|
if (error instanceof DefinitionNotFoundError) {
|
|
164
193
|
return createInterModuleKnownError(method, 'definition-not-found', {definitionId});
|
|
165
194
|
}
|
|
166
195
|
if (error instanceof ProjectMismatchError) {
|
|
167
196
|
return createInterModuleKnownError(method, 'project-mismatch', {});
|
|
168
197
|
}
|
|
198
|
+
return error;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function toStartDevRunKnownError(error: unknown): unknown {
|
|
202
|
+
return toRunCreationKnownError(workflowsInterModuleContract.methods.startDevRun, error) ?? error;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function toRunCreationKnownError(
|
|
206
|
+
method:
|
|
207
|
+
| typeof workflowsInterModuleContract.methods.startRunFromTrigger
|
|
208
|
+
| typeof workflowsInterModuleContract.methods.startDevRun,
|
|
209
|
+
error: unknown,
|
|
210
|
+
): unknown {
|
|
211
|
+
const workspaceError = toWorkspaceAdmissionKnownError(method, error);
|
|
212
|
+
if (workspaceError !== undefined) return workspaceError;
|
|
169
213
|
if (error instanceof AgentConfigUnresolvableError) {
|
|
170
214
|
return createInterModuleKnownError(method, 'agent-config-unresolvable', {
|
|
171
215
|
definitionId: error.definitionId,
|
|
@@ -187,12 +231,13 @@ export function toStartRunKnownError(error: unknown, definitionId: string): unkn
|
|
|
187
231
|
labels: [...error.labels],
|
|
188
232
|
});
|
|
189
233
|
}
|
|
190
|
-
return
|
|
234
|
+
return undefined;
|
|
191
235
|
}
|
|
192
236
|
|
|
193
237
|
function toWorkspaceAdmissionKnownError(
|
|
194
238
|
method:
|
|
195
239
|
| typeof workflowsInterModuleContract.methods.startRunFromTrigger
|
|
240
|
+
| typeof workflowsInterModuleContract.methods.startDevRun
|
|
196
241
|
| typeof workflowsInterModuleContract.methods.deliverEventToJobListener,
|
|
197
242
|
error: unknown,
|
|
198
243
|
): WorkspaceAdmissionKnownError | undefined {
|