@shipfox/api-workflows 13.1.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 +61 -0
- package/README.md +12 -5
- package/dist/core/checkout.d.ts +4 -2
- package/dist/core/checkout.d.ts.map +1 -1
- package/dist/core/checkout.js +7 -2
- package/dist/core/checkout.js.map +1 -1
- package/dist/core/entities/job.d.ts +1 -1
- package/dist/core/entities/job.d.ts.map +1 -1
- package/dist/core/entities/job.js.map +1 -1
- package/dist/core/entities/workflow-run.d.ts +25 -6
- 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 +27 -1
- package/dist/core/run-workflow.js.map +1 -1
- package/dist/db/db.d.ts +72 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/schema/workflow-runs.d.ts +45 -1
- package/dist/db/schema/workflow-runs.d.ts.map +1 -1
- package/dist/db/schema/workflow-runs.js +42 -1
- package/dist/db/schema/workflow-runs.js.map +1 -1
- package/dist/db/workflow-runs/jobs.d.ts +3 -1
- package/dist/db/workflow-runs/jobs.d.ts.map +1 -1
- package/dist/db/workflow-runs/jobs.js +12 -3
- package/dist/db/workflow-runs/jobs.js.map +1 -1
- package/dist/db/workflow-runs/queries.d.ts +3 -2
- package/dist/db/workflow-runs/queries.d.ts.map +1 -1
- package/dist/db/workflow-runs/queries.js +3 -0
- package/dist/db/workflow-runs/queries.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/dto/workflow-run.d.ts.map +1 -1
- package/dist/presentation/dto/workflow-run.js +14 -0
- package/dist/presentation/dto/workflow-run.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/presentation/routes/checkout-token.d.ts.map +1 -1
- package/dist/presentation/routes/checkout-token.js +2 -1
- package/dist/presentation/routes/checkout-token.js.map +1 -1
- package/dist/presentation/routes/get-run-aggregates.d.ts.map +1 -1
- package/dist/presentation/routes/get-run-aggregates.js +2 -1
- package/dist/presentation/routes/get-run-aggregates.js.map +1 -1
- package/dist/presentation/routes/leased-step.d.ts +3 -1
- package/dist/presentation/routes/leased-step.d.ts.map +1 -1
- package/dist/presentation/routes/leased-step.js +2 -1
- package/dist/presentation/routes/leased-step.js.map +1 -1
- package/dist/presentation/routes/list-runs.d.ts.map +1 -1
- package/dist/presentation/routes/list-runs.js +2 -1
- package/dist/presentation/routes/list-runs.js.map +1 -1
- package/dist/presentation/routes/project-access.d.ts +1 -0
- package/dist/presentation/routes/project-access.d.ts.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0004_dev_run_origin.sql +12 -0
- package/drizzle/meta/0004_snapshot.json +1773 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +16 -16
- package/src/core/checkout.test.ts +198 -1
- package/src/core/checkout.ts +18 -5
- package/src/core/entities/job.ts +1 -1
- package/src/core/entities/workflow-run.ts +26 -6
- package/src/core/index.ts +2 -2
- package/src/core/job-transition/decide-job-activation.test.ts +2 -0
- package/src/core/run-workflow.test.ts +151 -3
- package/src/core/run-workflow.ts +59 -3
- package/src/core/workflow-run-creation.test.ts +2 -0
- package/src/db/job-listeners.test.ts +17 -0
- package/src/db/schema/workflow-runs.ts +58 -0
- package/src/db/workflow-runs/jobs.ts +17 -3
- package/src/db/workflow-runs/queries.ts +6 -1
- package/src/db/workflow-runs/run-create.test.ts +87 -0
- package/src/db/workflow-runs/run-create.ts +23 -1
- package/src/presentation/dto/workflow-run.ts +23 -1
- package/src/presentation/inter-module.test.ts +131 -1
- package/src/presentation/inter-module.ts +48 -3
- package/src/presentation/routes/checkout-token.test.ts +45 -0
- package/src/presentation/routes/checkout-token.ts +2 -1
- package/src/presentation/routes/get-run-aggregates.test.ts +56 -0
- package/src/presentation/routes/get-run-aggregates.ts +2 -0
- package/src/presentation/routes/get-step-secrets.test.ts +1 -0
- package/src/presentation/routes/leased-step.ts +7 -1
- package/src/presentation/routes/list-runs.test.ts +152 -0
- package/src/presentation/routes/list-runs.ts +2 -0
- package/test/factories/workflow-run.ts +2 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
WorkflowRunAttemptDto,
|
|
3
|
+
WorkflowRunDevSourceDto,
|
|
3
4
|
WorkflowRunDto,
|
|
4
5
|
WorkflowRunListItemDto,
|
|
5
6
|
WorkflowRunTriggerReferenceDto,
|
|
6
7
|
} from '@shipfox/api-workflows-dto';
|
|
7
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
WorkflowRun,
|
|
10
|
+
WorkflowRunDevSource,
|
|
11
|
+
WorkflowRunTriggerReference,
|
|
12
|
+
} from '#core/entities/workflow-run.js';
|
|
8
13
|
import type {WorkflowRunAttempt} from '#core/entities/workflow-run-attempt.js';
|
|
9
14
|
import type {WorkflowRunJobsSummary} from '#db/index.js';
|
|
10
15
|
|
|
@@ -17,6 +22,8 @@ export function toRunDto(run: WorkflowRun, latestAttempt = run.currentAttempt):
|
|
|
17
22
|
name: run.name,
|
|
18
23
|
workflow_name: run.workflowName,
|
|
19
24
|
status: run.status,
|
|
25
|
+
origin: run.origin,
|
|
26
|
+
dev_source: toDevSourceDto(run.devSource),
|
|
20
27
|
current_attempt: run.currentAttempt,
|
|
21
28
|
latest_attempt: latestAttempt,
|
|
22
29
|
trigger_provider: run.triggerProvider,
|
|
@@ -76,6 +83,21 @@ function toTriggerReferenceDto(
|
|
|
76
83
|
};
|
|
77
84
|
}
|
|
78
85
|
|
|
86
|
+
// The persisted dev source uses camelCase field names while the API contract is snake_case,
|
|
87
|
+
// so each field is mapped explicitly rather than spread onto the response.
|
|
88
|
+
function toDevSourceDto(
|
|
89
|
+
devSource: WorkflowRunDevSource | null | undefined,
|
|
90
|
+
): WorkflowRunDevSourceDto | null {
|
|
91
|
+
if (!devSource) return null;
|
|
92
|
+
return {
|
|
93
|
+
ref: devSource.ref,
|
|
94
|
+
commit: devSource.commit,
|
|
95
|
+
config_path: devSource.configPath,
|
|
96
|
+
initiated_by_user_id: devSource.initiatedByUserId,
|
|
97
|
+
replay_of_event_id: devSource.replayOfEventId,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
79
101
|
export function toRunAttemptDto(attempt: WorkflowRunAttempt): WorkflowRunAttemptDto {
|
|
80
102
|
return {
|
|
81
103
|
id: attempt.id,
|
|
@@ -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 {
|
|
@@ -179,6 +179,35 @@ describe('POST /runs/jobs/current/steps/:stepId/checkout-token', () => {
|
|
|
179
179
|
});
|
|
180
180
|
});
|
|
181
181
|
|
|
182
|
+
test('defaults a dev run checkout to its pinned source commit', async () => {
|
|
183
|
+
const devCommit = 'b'.repeat(40);
|
|
184
|
+
const {project, job, step} = await createRunningCheckoutStep({devCommit});
|
|
185
|
+
getProjectById.mockResolvedValue({project});
|
|
186
|
+
resolveCheckoutTarget.mockResolvedValue({
|
|
187
|
+
projectId: project.id,
|
|
188
|
+
connectionId: project.sourceConnectionId,
|
|
189
|
+
externalRepositoryId: project.sourceExternalRepositoryId,
|
|
190
|
+
});
|
|
191
|
+
createCheckoutSpec.mockResolvedValue({...githubSpec('ghs-dev-token'), ref: devCommit});
|
|
192
|
+
const token = await mintActiveLeaseToken({jobId: job.id});
|
|
193
|
+
|
|
194
|
+
const res = await app.inject({
|
|
195
|
+
method: 'POST',
|
|
196
|
+
url: checkoutUrl(step.id, step.currentAttempt),
|
|
197
|
+
headers: {authorization: `Bearer ${token}`},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(res.statusCode).toBe(200);
|
|
201
|
+
expect(res.json()).toMatchObject({ref: devCommit});
|
|
202
|
+
expect(createCheckoutSpec).toHaveBeenCalledWith({
|
|
203
|
+
workspaceId: project.workspaceId,
|
|
204
|
+
connectionId: project.sourceConnectionId,
|
|
205
|
+
externalRepositoryId: project.sourceExternalRepositoryId,
|
|
206
|
+
ref: devCommit,
|
|
207
|
+
permissions: {contents: 'read'},
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
182
211
|
test('returns checkout-unavailable when the run project no longer resolves', async () => {
|
|
183
212
|
const {job, step} = await createRunningCheckoutStep({kind: 'checkout'});
|
|
184
213
|
getProjectById.mockResolvedValue({project: null});
|
|
@@ -521,6 +550,7 @@ async function createRunningCheckoutStep(
|
|
|
521
550
|
options: {
|
|
522
551
|
kind?: 'setup' | 'checkout' | 'run';
|
|
523
552
|
checkout?: CheckoutConfig;
|
|
553
|
+
devCommit?: string;
|
|
524
554
|
status?: StepStatus;
|
|
525
555
|
} = {},
|
|
526
556
|
) {
|
|
@@ -546,6 +576,21 @@ async function createRunningCheckoutStep(
|
|
|
546
576
|
userId: crypto.randomUUID(),
|
|
547
577
|
},
|
|
548
578
|
});
|
|
579
|
+
if (options.devCommit) {
|
|
580
|
+
await db()
|
|
581
|
+
.update(workflowRuns)
|
|
582
|
+
.set({
|
|
583
|
+
origin: 'dev',
|
|
584
|
+
devSource: {
|
|
585
|
+
ref: 'feature/checkout',
|
|
586
|
+
commit: options.devCommit,
|
|
587
|
+
config_path: '.shipfox/workflows/checkout.yml',
|
|
588
|
+
initiated_by_user_id: crypto.randomUUID(),
|
|
589
|
+
replay_of_event_id: null,
|
|
590
|
+
},
|
|
591
|
+
})
|
|
592
|
+
.where(eq(workflowRuns.id, run.id));
|
|
593
|
+
}
|
|
549
594
|
const [job] = await getJobsByWorkflowRunId(run.id);
|
|
550
595
|
if (!job) throw new Error('Expected workflow job');
|
|
551
596
|
await db().update(jobsTable).set({status: 'running'}).where(eq(jobsTable.id, job.id));
|
|
@@ -122,7 +122,7 @@ export function createCheckoutTokenRoute(clients: {
|
|
|
122
122
|
handler: async (request, reply) => {
|
|
123
123
|
const {stepId} = request.params;
|
|
124
124
|
const {attempt} = request.query;
|
|
125
|
-
const {step, workspaceId, projectId, triggerReference} = await loadRunningLeasedStep({
|
|
125
|
+
const {step, workspaceId, projectId, triggerReference, run} = await loadRunningLeasedStep({
|
|
126
126
|
runners: clients.runners,
|
|
127
127
|
request,
|
|
128
128
|
stepId,
|
|
@@ -138,6 +138,7 @@ export function createCheckoutTokenRoute(clients: {
|
|
|
138
138
|
workspaceId,
|
|
139
139
|
projectId,
|
|
140
140
|
triggerReference,
|
|
141
|
+
run,
|
|
141
142
|
integrations: clients.integrations,
|
|
142
143
|
projects: clients.projects,
|
|
143
144
|
});
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import {buildUserContext, setUserContext} from '@shipfox/api-auth-context';
|
|
2
2
|
import type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module';
|
|
3
|
+
import {eq} from 'drizzle-orm';
|
|
3
4
|
import type {FastifyInstance} from 'fastify';
|
|
4
5
|
import Fastify from 'fastify';
|
|
5
6
|
import {serializerCompiler, validatorCompiler} from 'fastify-type-provider-zod';
|
|
7
|
+
import {db} from '#db/db.js';
|
|
8
|
+
import {workflowRuns} from '#db/schema/workflow-runs.js';
|
|
6
9
|
import {createWorkflowRun, updateWorkflowRunStatus} from '#db/workflow-runs.js';
|
|
7
10
|
import {workflowModel} from '#test/index.js';
|
|
8
11
|
import {getRunAggregatesRoute} from './get-run-aggregates.js';
|
|
@@ -117,4 +120,57 @@ describe('GET /api/workflows/runs/aggregates', () => {
|
|
|
117
120
|
expect(body.trigger_source).toEqual([{value: 'manual', count: 1}]);
|
|
118
121
|
expect(body.workflow).toEqual([{value: deployDefinitionId, count: 1}]);
|
|
119
122
|
});
|
|
123
|
+
|
|
124
|
+
test('filters faceted counts by origin so the Runs tab facet follows', async () => {
|
|
125
|
+
await createWorkflowRun({
|
|
126
|
+
workspaceId,
|
|
127
|
+
projectId,
|
|
128
|
+
definitionId: crypto.randomUUID(),
|
|
129
|
+
name: 'Synced',
|
|
130
|
+
model: workflowModel({name: 'Synced'}),
|
|
131
|
+
triggerPayload: {
|
|
132
|
+
source: 'manual',
|
|
133
|
+
event: 'fire',
|
|
134
|
+
subscriptionId: crypto.randomUUID(),
|
|
135
|
+
userId: crypto.randomUUID(),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
const devRun = await createWorkflowRun({
|
|
139
|
+
workspaceId,
|
|
140
|
+
projectId,
|
|
141
|
+
definitionId: crypto.randomUUID(),
|
|
142
|
+
name: 'Dev',
|
|
143
|
+
model: workflowModel({name: 'Dev'}),
|
|
144
|
+
triggerPayload: {
|
|
145
|
+
source: 'manual',
|
|
146
|
+
event: 'fire',
|
|
147
|
+
subscriptionId: crypto.randomUUID(),
|
|
148
|
+
userId: crypto.randomUUID(),
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
await db()
|
|
152
|
+
.update(workflowRuns)
|
|
153
|
+
.set({
|
|
154
|
+
origin: 'dev',
|
|
155
|
+
devSource: {
|
|
156
|
+
ref: 'fix-triage-prompt',
|
|
157
|
+
commit: 'abc123',
|
|
158
|
+
config_path: '.shipfox/workflows/triage-sentry.yml',
|
|
159
|
+
initiated_by_user_id: crypto.randomUUID(),
|
|
160
|
+
replay_of_event_id: null,
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
.where(eq(workflowRuns.id, devRun.id));
|
|
164
|
+
|
|
165
|
+
const res = await app.inject({
|
|
166
|
+
method: 'GET',
|
|
167
|
+
url: `/api/workflows/runs/aggregates?project_id=${projectId}&origin=dev`,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(res.statusCode).toBe(200);
|
|
171
|
+
const body = res.json();
|
|
172
|
+
expect(body.status).toEqual([{value: 'pending', count: 1}]);
|
|
173
|
+
expect(body.trigger_source).toEqual([{value: 'manual', count: 1}]);
|
|
174
|
+
expect(body.workflow).toEqual([{value: devRun.definitionId, count: 1}]);
|
|
175
|
+
});
|
|
120
176
|
});
|
|
@@ -26,6 +26,7 @@ export function getRunAggregatesRoute(projects: ProjectsModuleClient) {
|
|
|
26
26
|
status,
|
|
27
27
|
definition_id: definitionId,
|
|
28
28
|
trigger_source: triggerSource,
|
|
29
|
+
origin,
|
|
29
30
|
created_from: createdFrom,
|
|
30
31
|
created_to: createdTo,
|
|
31
32
|
} = request.query;
|
|
@@ -35,6 +36,7 @@ export function getRunAggregatesRoute(projects: ProjectsModuleClient) {
|
|
|
35
36
|
status,
|
|
36
37
|
definitionId,
|
|
37
38
|
triggerSource,
|
|
39
|
+
origin,
|
|
38
40
|
createdFrom: createdFrom ? new Date(createdFrom) : undefined,
|
|
39
41
|
createdTo: createdTo ? new Date(createdTo) : undefined,
|
|
40
42
|
};
|
|
@@ -2,7 +2,10 @@ import {type LeasedJobContext, requireLeasedJobContext} from '@shipfox/api-auth-
|
|
|
2
2
|
import type {RunnersInterModuleClient} from '@shipfox/api-runners-dto/inter-module';
|
|
3
3
|
import {ClientError} from '@shipfox/node-fastify';
|
|
4
4
|
import type {Step} from '#core/entities/step.js';
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
WorkflowRunOriginState,
|
|
7
|
+
WorkflowRunTriggerReference,
|
|
8
|
+
} from '#core/entities/workflow-run.js';
|
|
6
9
|
import {getJobScope, getStepByIdForJobExecution} from '#db/index.js';
|
|
7
10
|
|
|
8
11
|
export interface LoadedRunningLeasedStep {
|
|
@@ -11,6 +14,8 @@ export interface LoadedRunningLeasedStep {
|
|
|
11
14
|
workspaceId: string;
|
|
12
15
|
projectId: string;
|
|
13
16
|
triggerReference: WorkflowRunTriggerReference | null;
|
|
17
|
+
/** The run's origin state, forwarded for checkout fallbacks. */
|
|
18
|
+
run: WorkflowRunOriginState;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
export async function loadRunningLeasedStep(params: {
|
|
@@ -59,5 +64,6 @@ export async function loadRunningLeasedStep(params: {
|
|
|
59
64
|
workspaceId: scope.workspaceId,
|
|
60
65
|
projectId: scope.projectId,
|
|
61
66
|
triggerReference: scope.triggerReference,
|
|
67
|
+
run: scope.run,
|
|
62
68
|
};
|
|
63
69
|
}
|
|
@@ -164,6 +164,81 @@ describe('GET /api/workflows/runs', () => {
|
|
|
164
164
|
expect(res.json().runs[0].trigger_reference).toBeNull();
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
+
test('labels runs with the synced origin and no dev source by default', async () => {
|
|
168
|
+
await createWorkflowRun({
|
|
169
|
+
workspaceId,
|
|
170
|
+
projectId,
|
|
171
|
+
definitionId: crypto.randomUUID(),
|
|
172
|
+
name: 'Manual',
|
|
173
|
+
model: workflowModel({name: 'Manual'}),
|
|
174
|
+
triggerPayload: {
|
|
175
|
+
source: 'manual',
|
|
176
|
+
event: 'fire',
|
|
177
|
+
subscriptionId: crypto.randomUUID(),
|
|
178
|
+
userId: crypto.randomUUID(),
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const res = await app.inject({
|
|
183
|
+
method: 'GET',
|
|
184
|
+
url: `/api/workflows/runs?project_id=${projectId}`,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(res.statusCode).toBe(200);
|
|
188
|
+
expect(res.json().runs[0]).toMatchObject({origin: 'synced', dev_source: null});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('filters runs by origin', async () => {
|
|
192
|
+
await createWorkflowRun({
|
|
193
|
+
workspaceId,
|
|
194
|
+
projectId,
|
|
195
|
+
definitionId: crypto.randomUUID(),
|
|
196
|
+
name: 'Synced',
|
|
197
|
+
model: workflowModel({name: 'Synced'}),
|
|
198
|
+
triggerPayload: {
|
|
199
|
+
source: 'manual',
|
|
200
|
+
event: 'fire',
|
|
201
|
+
subscriptionId: crypto.randomUUID(),
|
|
202
|
+
userId: crypto.randomUUID(),
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
const devRun = await createWorkflowRun({
|
|
206
|
+
workspaceId,
|
|
207
|
+
projectId,
|
|
208
|
+
definitionId: crypto.randomUUID(),
|
|
209
|
+
name: 'Dev',
|
|
210
|
+
model: workflowModel({name: 'Dev'}),
|
|
211
|
+
triggerPayload: {
|
|
212
|
+
source: 'manual',
|
|
213
|
+
event: 'fire',
|
|
214
|
+
subscriptionId: crypto.randomUUID(),
|
|
215
|
+
userId: crypto.randomUUID(),
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
const initiatedByUserId = crypto.randomUUID();
|
|
219
|
+
await markDevRun(devRun.id, initiatedByUserId);
|
|
220
|
+
|
|
221
|
+
const res = await app.inject({
|
|
222
|
+
method: 'GET',
|
|
223
|
+
url: `/api/workflows/runs?project_id=${projectId}&origin=dev`,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(res.statusCode).toBe(200);
|
|
227
|
+
const body = res.json();
|
|
228
|
+
expect(body.runs.map((run: {id: string}) => run.id)).toEqual([devRun.id]);
|
|
229
|
+
expect(body.filtered_total_count).toBe(1);
|
|
230
|
+
expect(body.runs[0]).toMatchObject({
|
|
231
|
+
origin: 'dev',
|
|
232
|
+
dev_source: {
|
|
233
|
+
ref: 'fix-triage-prompt',
|
|
234
|
+
commit: 'abc123',
|
|
235
|
+
config_path: '.shipfox/workflows/triage-sentry.yml',
|
|
236
|
+
initiated_by_user_id: initiatedByUserId,
|
|
237
|
+
replay_of_event_id: null,
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
167
242
|
test('returns empty array for project with no runs', async () => {
|
|
168
243
|
const res = await app.inject({
|
|
169
244
|
method: 'GET',
|
|
@@ -257,6 +332,67 @@ describe('GET /api/workflows/runs', () => {
|
|
|
257
332
|
expect(next.json().runs.map((run: {id: string}) => run.id)).toEqual([first.id]);
|
|
258
333
|
});
|
|
259
334
|
|
|
335
|
+
test('keeps the origin filter across cursor pages', async () => {
|
|
336
|
+
const devFirst = await createRunAt({
|
|
337
|
+
workspaceId,
|
|
338
|
+
projectId,
|
|
339
|
+
name: 'Dev first',
|
|
340
|
+
createdAt: new Date('2026-05-07T00:00:00.000Z'),
|
|
341
|
+
});
|
|
342
|
+
await markDevRun(devFirst.id);
|
|
343
|
+
await createRunAt({
|
|
344
|
+
workspaceId,
|
|
345
|
+
projectId,
|
|
346
|
+
name: 'Synced middle one',
|
|
347
|
+
createdAt: new Date('2026-05-07T01:00:00.000Z'),
|
|
348
|
+
});
|
|
349
|
+
const devSecond = await createRunAt({
|
|
350
|
+
workspaceId,
|
|
351
|
+
projectId,
|
|
352
|
+
name: 'Dev second',
|
|
353
|
+
createdAt: new Date('2026-05-07T02:00:00.000Z'),
|
|
354
|
+
});
|
|
355
|
+
await markDevRun(devSecond.id);
|
|
356
|
+
await createRunAt({
|
|
357
|
+
workspaceId,
|
|
358
|
+
projectId,
|
|
359
|
+
name: 'Synced middle two',
|
|
360
|
+
createdAt: new Date('2026-05-07T03:00:00.000Z'),
|
|
361
|
+
});
|
|
362
|
+
const devThird = await createRunAt({
|
|
363
|
+
workspaceId,
|
|
364
|
+
projectId,
|
|
365
|
+
name: 'Dev third',
|
|
366
|
+
createdAt: new Date('2026-05-07T04:00:00.000Z'),
|
|
367
|
+
});
|
|
368
|
+
await markDevRun(devThird.id);
|
|
369
|
+
|
|
370
|
+
const firstPage = await app.inject({
|
|
371
|
+
method: 'GET',
|
|
372
|
+
url: `/api/workflows/runs?project_id=${projectId}&origin=dev&limit=2`,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
expect(firstPage.statusCode).toBe(200);
|
|
376
|
+
const firstBody = firstPage.json();
|
|
377
|
+
expect(firstBody.runs.map((run: {name: string}) => run.name)).toEqual([
|
|
378
|
+
'Dev third',
|
|
379
|
+
'Dev second',
|
|
380
|
+
]);
|
|
381
|
+
expect(firstBody.runs.every((run: {origin: string}) => run.origin === 'dev')).toBe(true);
|
|
382
|
+
expect(firstBody.filtered_total_count).toBe(3);
|
|
383
|
+
|
|
384
|
+
const secondPage = await app.inject({
|
|
385
|
+
method: 'GET',
|
|
386
|
+
url: `/api/workflows/runs?project_id=${projectId}&origin=dev&limit=2&cursor=${firstBody.next_cursor}`,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
expect(secondPage.statusCode).toBe(200);
|
|
390
|
+
const secondBody = secondPage.json();
|
|
391
|
+
expect(secondBody.runs.map((run: {name: string}) => run.name)).toEqual(['Dev first']);
|
|
392
|
+
expect(secondBody.runs.every((run: {origin: string}) => run.origin === 'dev')).toBe(true);
|
|
393
|
+
expect(secondBody.next_cursor).toBeNull();
|
|
394
|
+
});
|
|
395
|
+
|
|
260
396
|
test('invalid cursor returns stable client error', async () => {
|
|
261
397
|
const res = await app.inject({
|
|
262
398
|
method: 'GET',
|
|
@@ -309,3 +445,19 @@ async function createRunAt({
|
|
|
309
445
|
|
|
310
446
|
return {...run, createdAt, updatedAt: createdAt};
|
|
311
447
|
}
|
|
448
|
+
|
|
449
|
+
async function markDevRun(runId: string, initiatedByUserId = crypto.randomUUID()) {
|
|
450
|
+
await db()
|
|
451
|
+
.update(workflowRuns)
|
|
452
|
+
.set({
|
|
453
|
+
origin: 'dev',
|
|
454
|
+
devSource: {
|
|
455
|
+
ref: 'fix-triage-prompt',
|
|
456
|
+
commit: 'abc123',
|
|
457
|
+
config_path: '.shipfox/workflows/triage-sentry.yml',
|
|
458
|
+
initiated_by_user_id: initiatedByUserId,
|
|
459
|
+
replay_of_event_id: null,
|
|
460
|
+
},
|
|
461
|
+
})
|
|
462
|
+
.where(eq(workflowRuns.id, runId));
|
|
463
|
+
}
|
|
@@ -30,6 +30,7 @@ export function listRunsRoute(projects: ProjectsModuleClient) {
|
|
|
30
30
|
status,
|
|
31
31
|
definition_id: definitionId,
|
|
32
32
|
trigger_source: triggerSource,
|
|
33
|
+
origin,
|
|
33
34
|
created_from: createdFrom,
|
|
34
35
|
created_to: createdTo,
|
|
35
36
|
} = request.query;
|
|
@@ -44,6 +45,7 @@ export function listRunsRoute(projects: ProjectsModuleClient) {
|
|
|
44
45
|
status,
|
|
45
46
|
definitionId,
|
|
46
47
|
triggerSource,
|
|
48
|
+
origin,
|
|
47
49
|
createdFrom: createdFrom ? new Date(createdFrom) : undefined,
|
|
48
50
|
createdTo: createdTo ? new Date(createdTo) : undefined,
|
|
49
51
|
};
|
|
@@ -37,6 +37,8 @@ export const workflowRunFactory = Factory.define<WorkflowRun, WorkflowRunTransie
|
|
|
37
37
|
workflowName: 'Test Workflow',
|
|
38
38
|
nameOverride: null,
|
|
39
39
|
status: 'pending',
|
|
40
|
+
origin: 'synced',
|
|
41
|
+
devSource: null,
|
|
40
42
|
currentAttempt: 1,
|
|
41
43
|
triggerProvider: null,
|
|
42
44
|
triggerSource: 'manual',
|