@shipfox/api-workflows 12.5.0 → 12.6.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 +11 -0
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/schema/jobs.d.ts.map +1 -1
- package/dist/db/schema/jobs.js +3 -0
- package/dist/db/schema/jobs.js.map +1 -1
- package/dist/db/workflow-runs/queries.d.ts +14 -2
- package/dist/db/workflow-runs/queries.d.ts.map +1 -1
- package/dist/db/workflow-runs/queries.js +52 -13
- package/dist/db/workflow-runs/queries.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.map +1 -1
- package/dist/presentation/dto/workflow-run.d.ts.map +1 -1
- package/dist/presentation/dto/workflow-run.js +10 -2
- package/dist/presentation/dto/workflow-run.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/db/index.ts +1 -0
- package/src/db/schema/jobs.ts +3 -0
- package/src/db/workflow-runs/queries.test.ts +169 -2
- package/src/db/workflow-runs/queries.ts +86 -11
- package/src/db/workflow-runs.ts +1 -0
- package/src/presentation/dto/workflow-run.ts +6 -2
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-workflows",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "12.
|
|
4
|
+
"version": "12.6.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@shipfox/api-integration-core-dto": "12.2.0",
|
|
33
33
|
"@shipfox/api-runners-dto": "12.4.0",
|
|
34
34
|
"@shipfox/api-secrets-dto": "12.0.0",
|
|
35
|
-
"@shipfox/api-workflows-dto": "12.
|
|
35
|
+
"@shipfox/api-workflows-dto": "12.6.0",
|
|
36
36
|
"@shipfox/api-workspaces-dto": "12.0.0",
|
|
37
37
|
"@shipfox/config": "1.2.4",
|
|
38
38
|
"@shipfox/inter-module": "0.2.3",
|
package/src/db/index.ts
CHANGED
package/src/db/schema/jobs.ts
CHANGED
|
@@ -61,6 +61,9 @@ export const jobs = pgTable(
|
|
|
61
61
|
key: text('key').notNull(),
|
|
62
62
|
mode: jobModeEnum('mode').notNull().default('one_shot'),
|
|
63
63
|
name: text('name'),
|
|
64
|
+
// This is the job's execution verdict, not its runtime lifecycle. Active execution state
|
|
65
|
+
// lives on job_executions and must be read there when a surface needs to distinguish
|
|
66
|
+
// waiting from executing.
|
|
64
67
|
status: jobStatusEnum('status').notNull().default('pending'),
|
|
65
68
|
statusReason: jobStatusReasonEnum('status_reason'),
|
|
66
69
|
carriedOver: boolean('carried_over').notNull().default(false),
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import {WORKFLOW_RUN_JOB_PREVIEW_LIMIT} from '@shipfox/api-workflows-dto';
|
|
2
|
+
import {eq} from 'drizzle-orm';
|
|
2
3
|
import {buildModel, createTestRun} from '#test/helpers/workflow-runs.js';
|
|
4
|
+
import {db} from '../db.js';
|
|
5
|
+
import {jobs} from '../schema/jobs.js';
|
|
3
6
|
import {
|
|
4
7
|
createRerunWorkflowRun,
|
|
5
8
|
createWorkflowRun,
|
|
@@ -272,6 +275,147 @@ describe('workflow run queries', () => {
|
|
|
272
275
|
expect(summary.get(run.id)?.statusCounts).toEqual([{status: 'pending', count: 2}]);
|
|
273
276
|
});
|
|
274
277
|
|
|
278
|
+
test('returns execution evidence and counts its display status', async () => {
|
|
279
|
+
const run = await createWorkflowRun({
|
|
280
|
+
workspaceId,
|
|
281
|
+
projectId,
|
|
282
|
+
definitionId,
|
|
283
|
+
model: buildModel({jobs: {build: {steps: [{run: 'echo build'}]}}}),
|
|
284
|
+
triggerPayload: manualTrigger(),
|
|
285
|
+
});
|
|
286
|
+
const [job] = await getJobsByWorkflowRunId(run.id);
|
|
287
|
+
if (!job) throw new Error('expected the run to have a job');
|
|
288
|
+
const execution = await getFirstJobExecutionByJobId(job.id);
|
|
289
|
+
if (!execution) throw new Error('expected the job to have an execution');
|
|
290
|
+
|
|
291
|
+
await updateJobExecutionStatus({
|
|
292
|
+
jobExecutionId: execution.id,
|
|
293
|
+
status: 'running',
|
|
294
|
+
expectedVersion: execution.version,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const summary = await listWorkflowRunJobSummaries([
|
|
298
|
+
{id: run.id, currentAttempt: run.currentAttempt},
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
expect(summary.get(run.id)?.preview).toMatchObject([
|
|
302
|
+
{
|
|
303
|
+
status: 'pending',
|
|
304
|
+
mode: 'one_shot',
|
|
305
|
+
listenerStatus: 'inactive',
|
|
306
|
+
executionStatus: 'running',
|
|
307
|
+
},
|
|
308
|
+
]);
|
|
309
|
+
expect(summary.get(run.id)?.statusCounts).toEqual([{status: 'running', count: 1}]);
|
|
310
|
+
expect(summary.get(run.id)?.rawStatusCounts).toEqual([{status: 'pending', count: 1}]);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('counts an active listener without an execution', async () => {
|
|
314
|
+
const run = await createWorkflowRun({
|
|
315
|
+
workspaceId,
|
|
316
|
+
projectId,
|
|
317
|
+
definitionId,
|
|
318
|
+
model: buildModel({
|
|
319
|
+
jobs: {
|
|
320
|
+
listen: {
|
|
321
|
+
listening: {
|
|
322
|
+
on: [{source: 'github', event: 'push'}],
|
|
323
|
+
onResolve: 'finish',
|
|
324
|
+
},
|
|
325
|
+
steps: [{run: 'echo listen'}],
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
}),
|
|
329
|
+
triggerPayload: manualTrigger(),
|
|
330
|
+
});
|
|
331
|
+
const [job] = await getJobsByWorkflowRunId(run.id);
|
|
332
|
+
if (!job) throw new Error('expected the run to have a listener job');
|
|
333
|
+
|
|
334
|
+
await db().update(jobs).set({listenerStatus: 'listening'}).where(eq(jobs.id, job.id));
|
|
335
|
+
|
|
336
|
+
const summary = await listWorkflowRunJobSummaries([
|
|
337
|
+
{id: run.id, currentAttempt: run.currentAttempt},
|
|
338
|
+
]);
|
|
339
|
+
|
|
340
|
+
expect(summary.get(run.id)?.preview).toMatchObject([
|
|
341
|
+
{
|
|
342
|
+
mode: 'listening',
|
|
343
|
+
listenerStatus: 'listening',
|
|
344
|
+
executionStatus: null,
|
|
345
|
+
},
|
|
346
|
+
]);
|
|
347
|
+
expect(summary.get(run.id)?.statusCounts).toEqual([{status: 'listening', count: 1}]);
|
|
348
|
+
expect(summary.get(run.id)?.rawStatusCounts).toEqual([{status: 'pending', count: 1}]);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test('keeps a terminal verdict ahead of a running execution', async () => {
|
|
352
|
+
const run = await createWorkflowRun({
|
|
353
|
+
workspaceId,
|
|
354
|
+
projectId,
|
|
355
|
+
definitionId,
|
|
356
|
+
model: buildModel({jobs: {build: {steps: [{run: 'echo build'}]}}}),
|
|
357
|
+
triggerPayload: manualTrigger(),
|
|
358
|
+
});
|
|
359
|
+
const [job] = await getJobsByWorkflowRunId(run.id);
|
|
360
|
+
if (!job) throw new Error('expected the run to have a job');
|
|
361
|
+
const execution = await getFirstJobExecutionByJobId(job.id);
|
|
362
|
+
if (!execution) throw new Error('expected the job to have an execution');
|
|
363
|
+
|
|
364
|
+
await updateJobExecutionStatus({
|
|
365
|
+
jobExecutionId: execution.id,
|
|
366
|
+
status: 'running',
|
|
367
|
+
expectedVersion: execution.version,
|
|
368
|
+
});
|
|
369
|
+
await updateJobStatus({jobId: job.id, status: 'failed', expectedVersion: job.version});
|
|
370
|
+
|
|
371
|
+
const summary = await listWorkflowRunJobSummaries([
|
|
372
|
+
{id: run.id, currentAttempt: run.currentAttempt},
|
|
373
|
+
]);
|
|
374
|
+
|
|
375
|
+
expect(summary.get(run.id)?.preview).toMatchObject([
|
|
376
|
+
{status: 'failed', executionStatus: 'running'},
|
|
377
|
+
]);
|
|
378
|
+
expect(summary.get(run.id)?.statusCounts).toEqual([{status: 'failed', count: 1}]);
|
|
379
|
+
expect(summary.get(run.id)?.rawStatusCounts).toEqual([{status: 'failed', count: 1}]);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test('counts a skipped zero-execution job from its terminal verdict', async () => {
|
|
383
|
+
const run = await createWorkflowRun({
|
|
384
|
+
workspaceId,
|
|
385
|
+
projectId,
|
|
386
|
+
definitionId,
|
|
387
|
+
model: buildModel({
|
|
388
|
+
jobs: {
|
|
389
|
+
listen: {
|
|
390
|
+
listening: {
|
|
391
|
+
on: [{source: 'github', event: 'push'}],
|
|
392
|
+
onResolve: 'finish',
|
|
393
|
+
},
|
|
394
|
+
steps: [{run: 'echo listen'}],
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
}),
|
|
398
|
+
triggerPayload: manualTrigger(),
|
|
399
|
+
});
|
|
400
|
+
const [job] = await getJobsByWorkflowRunId(run.id);
|
|
401
|
+
if (!job) throw new Error('expected the run to have a listener job');
|
|
402
|
+
|
|
403
|
+
await db()
|
|
404
|
+
.update(jobs)
|
|
405
|
+
.set({status: 'skipped', listenerStatus: 'resolved'})
|
|
406
|
+
.where(eq(jobs.id, job.id));
|
|
407
|
+
|
|
408
|
+
const summary = await listWorkflowRunJobSummaries([
|
|
409
|
+
{id: run.id, currentAttempt: run.currentAttempt},
|
|
410
|
+
]);
|
|
411
|
+
|
|
412
|
+
expect(summary.get(run.id)?.preview).toMatchObject([
|
|
413
|
+
{status: 'skipped', executionStatus: null},
|
|
414
|
+
]);
|
|
415
|
+
expect(summary.get(run.id)?.statusCounts).toEqual([{status: 'skipped', count: 1}]);
|
|
416
|
+
expect(summary.get(run.id)?.rawStatusCounts).toEqual([{status: 'skipped', count: 1}]);
|
|
417
|
+
});
|
|
418
|
+
|
|
275
419
|
// Checks the invariant the snapshot exists to protect: for a run inside the preview
|
|
276
420
|
// bound, the statuses drawn and the statuses counted describe the same jobs and must
|
|
277
421
|
// agree exactly. The read races a commit to give the anomaly a chance to appear, so this
|
|
@@ -445,13 +589,36 @@ function totalOf(summary: {statusCounts: Array<{count: number}>} | undefined): n
|
|
|
445
589
|
|
|
446
590
|
/** Statuses the preview actually drew, counted. */
|
|
447
591
|
function previewCounts(
|
|
448
|
-
summary:
|
|
592
|
+
summary:
|
|
593
|
+
| {
|
|
594
|
+
preview: Array<{
|
|
595
|
+
status: string;
|
|
596
|
+
mode: string;
|
|
597
|
+
listenerStatus: string;
|
|
598
|
+
executionStatus: string | null;
|
|
599
|
+
}>;
|
|
600
|
+
}
|
|
601
|
+
| undefined,
|
|
449
602
|
): Record<string, number> {
|
|
450
603
|
const counts: Record<string, number> = {};
|
|
451
|
-
for (const job of summary?.preview ?? [])
|
|
604
|
+
for (const job of summary?.preview ?? []) {
|
|
605
|
+
const status = previewDisplayStatus(job);
|
|
606
|
+
counts[status] = (counts[status] ?? 0) + 1;
|
|
607
|
+
}
|
|
452
608
|
return counts;
|
|
453
609
|
}
|
|
454
610
|
|
|
611
|
+
function previewDisplayStatus(job: {
|
|
612
|
+
status: string;
|
|
613
|
+
mode: string;
|
|
614
|
+
listenerStatus: string;
|
|
615
|
+
executionStatus: string | null;
|
|
616
|
+
}): string {
|
|
617
|
+
if (['succeeded', 'failed', 'cancelled', 'skipped'].includes(job.status)) return job.status;
|
|
618
|
+
if (job.mode === 'listening' && job.listenerStatus === 'listening') return 'listening';
|
|
619
|
+
return job.executionStatus ?? 'pending';
|
|
620
|
+
}
|
|
621
|
+
|
|
455
622
|
/** The same shape read off the totals, so the two halves can be compared directly. */
|
|
456
623
|
function statusCountMap(
|
|
457
624
|
summary: {statusCounts: Array<{status: string; count: number}>} | undefined,
|
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
timestampIdCursorWhere,
|
|
6
6
|
} from '@shipfox/node-drizzle';
|
|
7
7
|
import {and, asc, count, desc, eq, gte, lte, type SQL, sql} from 'drizzle-orm';
|
|
8
|
-
import type {JobStatus} from '#core/entities/job.js';
|
|
8
|
+
import type {JobMode, JobStatus, ListenerStatus} from '#core/entities/job.js';
|
|
9
|
+
import type {JobExecutionStatus} from '#core/entities/job-execution.js';
|
|
9
10
|
import type {
|
|
10
11
|
JobExecutionDetail,
|
|
11
12
|
StepDetail,
|
|
@@ -52,6 +53,9 @@ export interface WorkflowRunJobSummary {
|
|
|
52
53
|
key: string;
|
|
53
54
|
name: string | null;
|
|
54
55
|
status: JobStatus;
|
|
56
|
+
mode: JobMode;
|
|
57
|
+
listenerStatus: ListenerStatus;
|
|
58
|
+
executionStatus: JobExecutionStatus | null;
|
|
55
59
|
position: number;
|
|
56
60
|
}
|
|
57
61
|
|
|
@@ -212,14 +216,23 @@ export interface WorkflowRunJobSummaryTarget {
|
|
|
212
216
|
* A page row's jobs: a bounded slice to draw, and totals describing all of them.
|
|
213
217
|
*
|
|
214
218
|
* The preview is what a row can show; `statusCounts` is what it can say. Keeping the counts
|
|
215
|
-
* server-side is what lets a row report a failure that sits past the preview.
|
|
219
|
+
* server-side is what lets a row report a failure that sits past the preview. Counts use the
|
|
220
|
+
* display status derived from the job verdict, listener state, and selected execution state;
|
|
221
|
+
* the row keeps the verdict and evidence separate so the client can apply the same display
|
|
222
|
+
* rule as run detail.
|
|
216
223
|
*/
|
|
217
224
|
export interface WorkflowRunJobsSummary {
|
|
218
225
|
preview: WorkflowRunJobSummary[];
|
|
219
226
|
statusCounts: WorkflowRunJobStatusCount[];
|
|
227
|
+
rawStatusCounts: WorkflowRunJobRawStatusCount[];
|
|
220
228
|
}
|
|
221
229
|
|
|
222
230
|
export interface WorkflowRunJobStatusCount {
|
|
231
|
+
status: JobStatus | 'listening';
|
|
232
|
+
count: number;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface WorkflowRunJobRawStatusCount {
|
|
223
236
|
status: JobStatus;
|
|
224
237
|
count: number;
|
|
225
238
|
}
|
|
@@ -262,23 +275,64 @@ export async function listWorkflowRunJobSummaries(
|
|
|
262
275
|
|
|
263
276
|
const {previewRows, countRows} = await db().transaction(
|
|
264
277
|
async (tx) => {
|
|
278
|
+
// Pick the execution the detail view would display once per job before either list
|
|
279
|
+
// statement touches it. The existing (job_id, sequence) index supports the latest
|
|
280
|
+
// execution ordering, while the status-first expression preserves the rule that an
|
|
281
|
+
// active execution wins over a newer completed retry.
|
|
282
|
+
const selectedExecution = tx.$with('selected_execution').as(
|
|
283
|
+
tx
|
|
284
|
+
.selectDistinctOn([jobExecutions.jobId], {
|
|
285
|
+
jobId: jobExecutions.jobId,
|
|
286
|
+
executionStatus: sql<JobExecutionStatus>`${jobExecutions.status}`.as(
|
|
287
|
+
'execution_status',
|
|
288
|
+
),
|
|
289
|
+
})
|
|
290
|
+
.from(jobExecutions)
|
|
291
|
+
.innerJoin(jobs, eq(jobExecutions.jobId, jobs.id))
|
|
292
|
+
.innerJoin(workflowRunAttempts, eq(jobs.workflowRunAttemptId, workflowRunAttempts.id))
|
|
293
|
+
.where(attemptFilter)
|
|
294
|
+
.orderBy(
|
|
295
|
+
asc(jobExecutions.jobId),
|
|
296
|
+
sql`case when ${jobExecutions.status} = 'running' then 0 else 1 end`,
|
|
297
|
+
desc(jobExecutions.sequence),
|
|
298
|
+
desc(jobExecutions.id),
|
|
299
|
+
),
|
|
300
|
+
);
|
|
301
|
+
|
|
265
302
|
const ranked = tx
|
|
303
|
+
.with(selectedExecution)
|
|
266
304
|
.select({
|
|
267
305
|
workflowRunId: workflowRunAttempts.workflowRunId,
|
|
268
306
|
id: jobs.id,
|
|
269
307
|
key: jobs.key,
|
|
270
308
|
name: jobs.name,
|
|
271
309
|
status: jobs.status,
|
|
310
|
+
mode: jobs.mode,
|
|
311
|
+
listenerStatus: jobs.listenerStatus,
|
|
312
|
+
executionStatus: selectedExecution.executionStatus,
|
|
272
313
|
position: jobs.position,
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
314
|
+
jobRank:
|
|
315
|
+
sql<number>`row_number() over (partition by ${workflowRunAttempts.workflowRunId} order by ${jobs.position} asc, ${jobs.id} asc)`.as(
|
|
316
|
+
'job_rank',
|
|
317
|
+
),
|
|
276
318
|
})
|
|
277
319
|
.from(jobs)
|
|
278
320
|
.innerJoin(workflowRunAttempts, eq(jobs.workflowRunAttemptId, workflowRunAttempts.id))
|
|
321
|
+
.leftJoin(selectedExecution, eq(selectedExecution.jobId, jobs.id))
|
|
279
322
|
.where(attemptFilter)
|
|
280
323
|
.as('ranked');
|
|
281
324
|
|
|
325
|
+
const executionDisplayStatus = sql<JobExecutionStatus | null>`
|
|
326
|
+
${selectedExecution.executionStatus}
|
|
327
|
+
`;
|
|
328
|
+
const displayStatus = sql<WorkflowRunJobStatusCount['status']>`
|
|
329
|
+
case
|
|
330
|
+
when ${jobs.status} in ('succeeded', 'failed', 'cancelled', 'skipped') then ${jobs.status}::text
|
|
331
|
+
when ${jobs.mode} = 'listening' and ${jobs.listenerStatus} = 'listening' then 'listening'
|
|
332
|
+
when ${executionDisplayStatus} is not null then ${executionDisplayStatus}::text
|
|
333
|
+
else 'pending'
|
|
334
|
+
end
|
|
335
|
+
`;
|
|
282
336
|
return {
|
|
283
337
|
previewRows: await tx
|
|
284
338
|
.select({
|
|
@@ -287,21 +341,27 @@ export async function listWorkflowRunJobSummaries(
|
|
|
287
341
|
key: ranked.key,
|
|
288
342
|
name: ranked.name,
|
|
289
343
|
status: ranked.status,
|
|
344
|
+
mode: ranked.mode,
|
|
345
|
+
listenerStatus: ranked.listenerStatus,
|
|
346
|
+
executionStatus: ranked.executionStatus,
|
|
290
347
|
position: ranked.position,
|
|
291
348
|
})
|
|
292
349
|
.from(ranked)
|
|
293
|
-
.where(lte(ranked.
|
|
350
|
+
.where(lte(ranked.jobRank, WORKFLOW_RUN_JOB_PREVIEW_LIMIT))
|
|
294
351
|
.orderBy(asc(ranked.workflowRunId), asc(ranked.position), asc(ranked.id)),
|
|
295
352
|
countRows: await tx
|
|
353
|
+
.with(selectedExecution)
|
|
296
354
|
.select({
|
|
297
355
|
workflowRunId: workflowRunAttempts.workflowRunId,
|
|
298
|
-
|
|
356
|
+
rawStatus: jobs.status,
|
|
357
|
+
status: displayStatus,
|
|
299
358
|
count: count(),
|
|
300
359
|
})
|
|
301
360
|
.from(jobs)
|
|
302
361
|
.innerJoin(workflowRunAttempts, eq(jobs.workflowRunAttemptId, workflowRunAttempts.id))
|
|
362
|
+
.leftJoin(selectedExecution, eq(selectedExecution.jobId, jobs.id))
|
|
303
363
|
.where(attemptFilter)
|
|
304
|
-
.groupBy(workflowRunAttempts.workflowRunId, jobs.status),
|
|
364
|
+
.groupBy(workflowRunAttempts.workflowRunId, jobs.status, displayStatus),
|
|
305
365
|
};
|
|
306
366
|
},
|
|
307
367
|
{isolationLevel: 'repeatable read', accessMode: 'read only'},
|
|
@@ -310,8 +370,10 @@ export async function listWorkflowRunJobSummaries(
|
|
|
310
370
|
for (const {workflowRunId, ...summary} of previewRows) {
|
|
311
371
|
summaryFor(summaries, workflowRunId).preview.push(summary);
|
|
312
372
|
}
|
|
313
|
-
for (const {workflowRunId, status, count: statusCount} of countRows) {
|
|
314
|
-
summaryFor(summaries, workflowRunId)
|
|
373
|
+
for (const {workflowRunId, rawStatus, status, count: statusCount} of countRows) {
|
|
374
|
+
const summary = summaryFor(summaries, workflowRunId);
|
|
375
|
+
appendStatusCount(summary.rawStatusCounts, rawStatus, statusCount);
|
|
376
|
+
appendStatusCount(summary.statusCounts, status, statusCount);
|
|
315
377
|
}
|
|
316
378
|
|
|
317
379
|
return summaries;
|
|
@@ -323,11 +385,24 @@ function summaryFor(
|
|
|
323
385
|
): WorkflowRunJobsSummary {
|
|
324
386
|
const existing = summaries.get(workflowRunId);
|
|
325
387
|
if (existing) return existing;
|
|
326
|
-
const created: WorkflowRunJobsSummary = {preview: [], statusCounts: []};
|
|
388
|
+
const created: WorkflowRunJobsSummary = {preview: [], statusCounts: [], rawStatusCounts: []};
|
|
327
389
|
summaries.set(workflowRunId, created);
|
|
328
390
|
return created;
|
|
329
391
|
}
|
|
330
392
|
|
|
393
|
+
function appendStatusCount<T extends string>(
|
|
394
|
+
counts: Array<{status: T; count: number}>,
|
|
395
|
+
status: T,
|
|
396
|
+
count: number,
|
|
397
|
+
): void {
|
|
398
|
+
const existing = counts.find((entry) => entry.status === status);
|
|
399
|
+
if (existing) {
|
|
400
|
+
existing.count += count;
|
|
401
|
+
} else {
|
|
402
|
+
counts.push({status, count});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
331
406
|
export async function listWorkflowRunsByProject(projectId: string): Promise<WorkflowRun[]> {
|
|
332
407
|
const result = await listWorkflowRuns({projectId, limit: 100});
|
|
333
408
|
return result.runs;
|
package/src/db/workflow-runs.ts
CHANGED
|
@@ -33,7 +33,7 @@ export function toRunDto(run: WorkflowRun, latestAttempt = run.currentAttempt):
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
const EMPTY_JOBS: WorkflowRunJobsSummary = {preview: [], statusCounts: []};
|
|
36
|
+
const EMPTY_JOBS: WorkflowRunJobsSummary = {preview: [], statusCounts: [], rawStatusCounts: []};
|
|
37
37
|
|
|
38
38
|
export function toRunListItemDto(
|
|
39
39
|
run: WorkflowRun,
|
|
@@ -46,9 +46,13 @@ export function toRunListItemDto(
|
|
|
46
46
|
key: job.key,
|
|
47
47
|
name: job.name,
|
|
48
48
|
status: job.status,
|
|
49
|
+
mode: job.mode,
|
|
50
|
+
listener_status: job.listenerStatus,
|
|
51
|
+
execution_status: job.executionStatus,
|
|
49
52
|
position: job.position,
|
|
50
53
|
})),
|
|
51
|
-
job_status_counts: jobs.
|
|
54
|
+
job_status_counts: jobs.rawStatusCounts.map(({status, count}) => ({status, count})),
|
|
55
|
+
job_display_status_counts: jobs.statusCounts.map(({status, count}) => ({status, count})),
|
|
52
56
|
};
|
|
53
57
|
}
|
|
54
58
|
|