@shipfox/api-runners 12.7.0 → 13.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.
Files changed (55) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +20 -0
  3. package/dist/db/index.d.ts +1 -1
  4. package/dist/db/index.d.ts.map +1 -1
  5. package/dist/db/index.js +1 -1
  6. package/dist/db/index.js.map +1 -1
  7. package/dist/db/job-executions.d.ts +8 -24
  8. package/dist/db/job-executions.d.ts.map +1 -1
  9. package/dist/db/job-executions.js +106 -83
  10. package/dist/db/job-executions.js.map +1 -1
  11. package/dist/db/reservations.d.ts.map +1 -1
  12. package/dist/db/reservations.js +6 -4
  13. package/dist/db/reservations.js.map +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +5 -4
  17. package/dist/index.js.map +1 -1
  18. package/dist/presentation/index.d.ts +2 -1
  19. package/dist/presentation/index.d.ts.map +1 -1
  20. package/dist/presentation/index.js +2 -1
  21. package/dist/presentation/index.js.map +1 -1
  22. package/dist/presentation/inter-module.d.ts +0 -1
  23. package/dist/presentation/inter-module.d.ts.map +1 -1
  24. package/dist/presentation/inter-module.js +2 -25
  25. package/dist/presentation/inter-module.js.map +1 -1
  26. package/dist/presentation/subscribers/on-workflows-job-execution-queued.d.ts +3 -0
  27. package/dist/presentation/subscribers/on-workflows-job-execution-queued.d.ts.map +1 -0
  28. package/dist/presentation/subscribers/on-workflows-job-execution-queued.js +21 -0
  29. package/dist/presentation/subscribers/on-workflows-job-execution-queued.js.map +1 -0
  30. package/dist/presentation/subscribers/on-workflows-job-execution-terminated.d.ts +3 -0
  31. package/dist/presentation/subscribers/on-workflows-job-execution-terminated.d.ts.map +1 -0
  32. package/dist/presentation/subscribers/{on-workflows-job-execution-timed-out.js → on-workflows-job-execution-terminated.js} +5 -4
  33. package/dist/presentation/subscribers/on-workflows-job-execution-terminated.js.map +1 -0
  34. package/dist/tsconfig.test.tsbuildinfo +1 -1
  35. package/package.json +3 -3
  36. package/src/db/index.ts +0 -2
  37. package/src/db/job-executions.test.ts +197 -335
  38. package/src/db/job-executions.ts +145 -100
  39. package/src/db/reservations.ts +6 -3
  40. package/src/db/runner-instances.test.ts +100 -0
  41. package/src/index.test.ts +1 -0
  42. package/src/index.ts +8 -7
  43. package/src/presentation/index.ts +2 -1
  44. package/src/presentation/inter-module.ts +2 -40
  45. package/src/presentation/subscribers/on-workflows-job-execution-queued.test.ts +57 -0
  46. package/src/presentation/subscribers/on-workflows-job-execution-queued.ts +26 -0
  47. package/src/presentation/subscribers/on-workflows-job-execution-terminated.test.ts +142 -0
  48. package/src/presentation/subscribers/{on-workflows-job-execution-timed-out.ts → on-workflows-job-execution-terminated.ts} +5 -4
  49. package/test/factories/pending-job.ts +3 -0
  50. package/tsconfig.build.tsbuildinfo +1 -1
  51. package/dist/presentation/subscribers/on-workflows-job-execution-timed-out.d.ts +0 -3
  52. package/dist/presentation/subscribers/on-workflows-job-execution-timed-out.d.ts.map +0 -1
  53. package/dist/presentation/subscribers/on-workflows-job-execution-timed-out.js.map +0 -1
  54. package/src/presentation/inter-module.test.ts +0 -15
  55. package/src/presentation/subscribers/on-workflows-job-execution-timed-out.test.ts +0 -163
@@ -0,0 +1,57 @@
1
+ import type {WorkflowsJobExecutionQueuedEventDto} from '@shipfox/api-workflows-dto';
2
+ import {eq} from 'drizzle-orm';
3
+ import {db} from '#db/db.js';
4
+ import {claimPendingJobExecution} from '#db/job-executions.js';
5
+ import {pendingJobExecutions} from '#db/schema/pending-job-executions.js';
6
+ import {runningJobExecutions} from '#db/schema/running-job-executions.js';
7
+ import {runnerSessionFactory} from '#test/index.js';
8
+ import {onWorkflowsJobExecutionQueued} from './on-workflows-job-execution-queued.js';
9
+
10
+ describe('onWorkflowsJobExecutionQueued', () => {
11
+ it('queues from the workflow timestamp and is idempotent across claim', async () => {
12
+ const workspaceId = crypto.randomUUID();
13
+ const payload: WorkflowsJobExecutionQueuedEventDto = {
14
+ jobId: crypto.randomUUID(),
15
+ jobExecutionId: crypto.randomUUID(),
16
+ workflowRunId: crypto.randomUUID(),
17
+ workflowRunAttemptId: crypto.randomUUID(),
18
+ workspaceId,
19
+ projectId: crypto.randomUUID(),
20
+ requiredLabels: ['linux'],
21
+ queuedAt: '2026-08-11T08:00:00.000Z',
22
+ };
23
+
24
+ await onWorkflowsJobExecutionQueued(payload);
25
+ await onWorkflowsJobExecutionQueued(payload);
26
+
27
+ const pending = await db()
28
+ .select()
29
+ .from(pendingJobExecutions)
30
+ .where(eq(pendingJobExecutions.jobExecutionId, payload.jobExecutionId));
31
+ expect(pending).toHaveLength(1);
32
+ expect(pending[0]?.createdAt.toISOString()).toBe(payload.queuedAt);
33
+
34
+ const session = await runnerSessionFactory.create({workspaceId});
35
+ await claimPendingJobExecution({
36
+ workspaceId,
37
+ runnerSessionId: session.id,
38
+ sessionLabels: ['linux'],
39
+ maxClaims: null,
40
+ runnerSessionLivenessThrottleSeconds: 10,
41
+ });
42
+ await onWorkflowsJobExecutionQueued(payload);
43
+
44
+ expect(
45
+ await db()
46
+ .select()
47
+ .from(pendingJobExecutions)
48
+ .where(eq(pendingJobExecutions.jobExecutionId, payload.jobExecutionId)),
49
+ ).toHaveLength(0);
50
+ expect(
51
+ await db()
52
+ .select()
53
+ .from(runningJobExecutions)
54
+ .where(eq(runningJobExecutions.jobExecutionId, payload.jobExecutionId)),
55
+ ).toHaveLength(1);
56
+ });
57
+ });
@@ -0,0 +1,26 @@
1
+ import type {WorkflowsJobExecutionQueuedEventDto} from '@shipfox/api-workflows-dto';
2
+ import {logger} from '@shipfox/node-opentelemetry';
3
+ import {enqueueJobExecution} from '#db/job-executions.js';
4
+
5
+ export async function onWorkflowsJobExecutionQueued(
6
+ payload: WorkflowsJobExecutionQueuedEventDto,
7
+ ): Promise<void> {
8
+ logger().info(
9
+ {
10
+ jobId: payload.jobId,
11
+ jobExecutionId: payload.jobExecutionId,
12
+ workflowRunAttemptId: payload.workflowRunAttemptId,
13
+ },
14
+ 'Queueing runner job execution from workflow fact',
15
+ );
16
+ await enqueueJobExecution({
17
+ workspaceId: payload.workspaceId,
18
+ workflowRunId: payload.workflowRunId,
19
+ workflowRunAttemptId: payload.workflowRunAttemptId,
20
+ jobId: payload.jobId,
21
+ jobExecutionId: payload.jobExecutionId,
22
+ projectId: payload.projectId,
23
+ requiredLabels: payload.requiredLabels,
24
+ queuedAt: new Date(payload.queuedAt),
25
+ });
26
+ }
@@ -0,0 +1,142 @@
1
+ import type {WorkflowsJobExecutionTerminatedEventDto} from '@shipfox/api-workflows-dto';
2
+ import {eq} from 'drizzle-orm';
3
+ import {db} from '#db/db.js';
4
+ import {reservations} from '#db/schema/reservations.js';
5
+ import {providerRunners} from '#db/schema/runner-instances.js';
6
+ import {runningJobExecutions} from '#db/schema/running-job-executions.js';
7
+ import {providerRunnerFactory, runnerSessionFactory} from '#test/index.js';
8
+ import {onWorkflowsJobExecutionTerminated} from './on-workflows-job-execution-terminated.js';
9
+
10
+ describe('onWorkflowsJobExecutionTerminated', () => {
11
+ it('releases a terminal runner reservation once no uncancelled lease remains', async () => {
12
+ const workspaceId = crypto.randomUUID();
13
+ const provisionerId = crypto.randomUUID();
14
+ const providerRunnerId = crypto.randomUUID();
15
+ const workflowRunId = crypto.randomUUID();
16
+ const workflowRunAttemptId = crypto.randomUUID();
17
+ const jobId = crypto.randomUUID();
18
+ const jobExecutionId = crypto.randomUUID();
19
+ const projectId = crypto.randomUUID();
20
+ const runnerSession = await runnerSessionFactory.create({workspaceId});
21
+ const [reservation] = await db()
22
+ .insert(reservations)
23
+ .values({
24
+ workspaceId,
25
+ provisionerId,
26
+ requiredLabels: ['linux'],
27
+ count: 1,
28
+ expiresAt: new Date(Date.now() + 60_000),
29
+ })
30
+ .returning();
31
+ if (!reservation) throw new Error('Expected reservation');
32
+ await providerRunnerFactory.create({
33
+ workspaceId,
34
+ provisionerId,
35
+ providerRunnerId,
36
+ reservationId: reservation.id,
37
+ runnerSessionId: runnerSession.id,
38
+ state: 'terminated',
39
+ terminatedAt: new Date(),
40
+ });
41
+ await db()
42
+ .insert(runningJobExecutions)
43
+ .values({
44
+ workspaceId,
45
+ workflowRunId,
46
+ workflowRunAttemptId,
47
+ jobId,
48
+ jobExecutionId,
49
+ projectId,
50
+ runnerSessionId: runnerSession.id,
51
+ provisionerId,
52
+ providerRunnerId,
53
+ requiredLabels: ['linux'],
54
+ runnerLabels: ['linux'],
55
+ });
56
+ const payload: WorkflowsJobExecutionTerminatedEventDto = {
57
+ jobId,
58
+ jobExecutionId,
59
+ workflowRunId,
60
+ workflowRunAttemptId,
61
+ status: 'cancelled',
62
+ statusReason: 'run_cancelled',
63
+ statusReasonMessage: null,
64
+ };
65
+
66
+ await onWorkflowsJobExecutionTerminated(payload);
67
+ await onWorkflowsJobExecutionTerminated(payload);
68
+
69
+ expect(
70
+ await db().select().from(reservations).where(eq(reservations.id, reservation.id)),
71
+ ).toHaveLength(0);
72
+ const [runner] = await db()
73
+ .select()
74
+ .from(providerRunners)
75
+ .where(eq(providerRunners.providerRunnerId, providerRunnerId));
76
+ expect(runner?.reservationReleasedAt).toBeInstanceOf(Date);
77
+ const [lease] = await db()
78
+ .select()
79
+ .from(runningJobExecutions)
80
+ .where(eq(runningJobExecutions.jobExecutionId, jobExecutionId));
81
+ expect(lease?.cancellationRequestedAt).toBeInstanceOf(Date);
82
+ });
83
+
84
+ it('keeps the reservation while the provider runner is non-terminal', async () => {
85
+ const workspaceId = crypto.randomUUID();
86
+ const provisionerId = crypto.randomUUID();
87
+ const providerRunnerId = crypto.randomUUID();
88
+ const runnerSession = await runnerSessionFactory.create({workspaceId});
89
+ const [reservation] = await db()
90
+ .insert(reservations)
91
+ .values({
92
+ workspaceId,
93
+ provisionerId,
94
+ requiredLabels: ['linux'],
95
+ count: 1,
96
+ expiresAt: new Date(Date.now() + 60_000),
97
+ })
98
+ .returning();
99
+ if (!reservation) throw new Error('Expected reservation');
100
+ await providerRunnerFactory.create({
101
+ workspaceId,
102
+ provisionerId,
103
+ providerRunnerId,
104
+ reservationId: reservation.id,
105
+ runnerSessionId: runnerSession.id,
106
+ state: 'running',
107
+ });
108
+ const workflowRunId = crypto.randomUUID();
109
+ const workflowRunAttemptId = crypto.randomUUID();
110
+ const jobId = crypto.randomUUID();
111
+ const jobExecutionId = crypto.randomUUID();
112
+ await db()
113
+ .insert(runningJobExecutions)
114
+ .values({
115
+ workspaceId,
116
+ workflowRunId,
117
+ workflowRunAttemptId,
118
+ jobId,
119
+ jobExecutionId,
120
+ projectId: crypto.randomUUID(),
121
+ runnerSessionId: runnerSession.id,
122
+ provisionerId,
123
+ providerRunnerId,
124
+ requiredLabels: ['linux'],
125
+ runnerLabels: ['linux'],
126
+ });
127
+
128
+ await onWorkflowsJobExecutionTerminated({
129
+ jobId,
130
+ jobExecutionId,
131
+ workflowRunId,
132
+ workflowRunAttemptId,
133
+ status: 'failed',
134
+ statusReason: 'step_failed',
135
+ statusReasonMessage: null,
136
+ });
137
+
138
+ expect(
139
+ await db().select().from(reservations).where(eq(reservations.id, reservation.id)),
140
+ ).toHaveLength(1);
141
+ });
142
+ });
@@ -1,17 +1,18 @@
1
- import type {WorkflowsJobExecutionTimedOutEventDto} from '@shipfox/api-workflows-dto';
1
+ import type {WorkflowsJobExecutionTerminatedEventDto} from '@shipfox/api-workflows-dto';
2
2
  import {logger} from '@shipfox/node-opentelemetry';
3
3
  import {reconcileTerminalJobExecution} from '#db/job-executions.js';
4
4
 
5
- export async function onWorkflowsJobExecutionTimedOut(
6
- payload: WorkflowsJobExecutionTimedOutEventDto,
5
+ export async function onWorkflowsJobExecutionTerminated(
6
+ payload: WorkflowsJobExecutionTerminatedEventDto,
7
7
  ): Promise<void> {
8
8
  logger().info(
9
9
  {
10
10
  jobId: payload.jobId,
11
11
  jobExecutionId: payload.jobExecutionId,
12
12
  workflowRunAttemptId: payload.workflowRunAttemptId,
13
+ status: payload.status,
13
14
  },
14
- 'Reconciling runner state for timed-out job execution',
15
+ 'Reconciling runner state for terminal job execution',
15
16
  );
16
17
  await reconcileTerminalJobExecution({jobExecutionId: payload.jobExecutionId});
17
18
  }
@@ -9,6 +9,7 @@ interface PendingJobAttrs {
9
9
  jobExecutionId: string;
10
10
  projectId: string;
11
11
  requiredLabels: string[];
12
+ queuedAt: Date;
12
13
  }
13
14
 
14
15
  export const pendingJobFactory = Factory.define<PendingJobAttrs>(({onCreate}) => {
@@ -28,6 +29,7 @@ export const pendingJobFactory = Factory.define<PendingJobAttrs>(({onCreate}) =>
28
29
  jobExecutionId: attrs.jobExecutionId,
29
30
  projectId: attrs.projectId,
30
31
  requiredLabels: attrs.requiredLabels,
32
+ queuedAt: attrs.queuedAt,
31
33
  });
32
34
  return attrs;
33
35
  });
@@ -40,5 +42,6 @@ export const pendingJobFactory = Factory.define<PendingJobAttrs>(({onCreate}) =>
40
42
  jobExecutionId,
41
43
  projectId,
42
44
  requiredLabels: ['linux'],
45
+ queuedAt: new Date(),
43
46
  };
44
47
  });