@shipfox/api-runners 6.0.0 → 7.0.2

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 (129) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +26 -0
  3. package/dist/config.js +1 -1
  4. package/dist/config.js.map +1 -1
  5. package/dist/core/entities/runner-instance.d.ts +1 -0
  6. package/dist/core/entities/runner-instance.d.ts.map +1 -1
  7. package/dist/core/entities/runner-instance.js.map +1 -1
  8. package/dist/core/errors.d.ts +6 -6
  9. package/dist/core/errors.d.ts.map +1 -1
  10. package/dist/core/errors.js +8 -8
  11. package/dist/core/errors.js.map +1 -1
  12. package/dist/core/index.d.ts +2 -4
  13. package/dist/core/index.d.ts.map +1 -1
  14. package/dist/core/index.js +2 -3
  15. package/dist/core/index.js.map +1 -1
  16. package/dist/core/runner-assignments.d.ts +6 -0
  17. package/dist/core/runner-assignments.d.ts.map +1 -0
  18. package/dist/core/runner-assignments.js +6 -0
  19. package/dist/core/runner-assignments.js.map +1 -0
  20. package/dist/core/runner-instances.d.ts +2 -9
  21. package/dist/core/runner-instances.d.ts.map +1 -1
  22. package/dist/core/runner-instances.js +3 -6
  23. package/dist/core/runner-instances.js.map +1 -1
  24. package/dist/db/db.d.ts +34 -218
  25. package/dist/db/db.d.ts.map +1 -1
  26. package/dist/db/db.js +0 -2
  27. package/dist/db/db.js.map +1 -1
  28. package/dist/db/index.d.ts +2 -2
  29. package/dist/db/index.d.ts.map +1 -1
  30. package/dist/db/index.js +2 -2
  31. package/dist/db/index.js.map +1 -1
  32. package/dist/db/runner-assignments.d.ts +7 -0
  33. package/dist/db/runner-assignments.d.ts.map +1 -0
  34. package/dist/db/runner-assignments.js +55 -0
  35. package/dist/db/runner-assignments.js.map +1 -0
  36. package/dist/db/runner-instances.d.ts +2 -9
  37. package/dist/db/runner-instances.d.ts.map +1 -1
  38. package/dist/db/runner-instances.js +2 -16
  39. package/dist/db/runner-instances.js.map +1 -1
  40. package/dist/db/schema/runner-instances.d.ts +17 -0
  41. package/dist/db/schema/runner-instances.d.ts.map +1 -1
  42. package/dist/db/schema/runner-instances.js +5 -1
  43. package/dist/db/schema/runner-instances.js.map +1 -1
  44. package/dist/index.d.ts +0 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +0 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/presentation/dto/index.d.ts +0 -1
  49. package/dist/presentation/dto/index.d.ts.map +1 -1
  50. package/dist/presentation/dto/index.js +0 -1
  51. package/dist/presentation/dto/index.js.map +1 -1
  52. package/dist/presentation/routes/assign-runner-instances.d.ts +2 -0
  53. package/dist/presentation/routes/assign-runner-instances.d.ts.map +1 -0
  54. package/dist/presentation/routes/assign-runner-instances.js +40 -0
  55. package/dist/presentation/routes/assign-runner-instances.js.map +1 -0
  56. package/dist/presentation/routes/index.d.ts.map +1 -1
  57. package/dist/presentation/routes/index.js +4 -8
  58. package/dist/presentation/routes/index.js.map +1 -1
  59. package/dist/presentation/routes/runner-enrollment.d.ts +1 -0
  60. package/dist/presentation/routes/runner-enrollment.d.ts.map +1 -1
  61. package/dist/presentation/routes/runner-enrollment.js +27 -0
  62. package/dist/presentation/routes/runner-enrollment.js.map +1 -1
  63. package/dist/tsconfig.test.tsbuildinfo +1 -1
  64. package/drizzle/0000_initial.sql +1 -12
  65. package/drizzle/meta/0000_snapshot.json +6 -97
  66. package/package.json +8 -29
  67. package/src/config.ts +1 -1
  68. package/src/core/entities/runner-instance.ts +1 -0
  69. package/src/core/errors.ts +8 -8
  70. package/src/core/index.ts +2 -9
  71. package/src/core/runner-assignments.ts +9 -0
  72. package/src/core/runner-instances.ts +4 -13
  73. package/src/db/db.ts +0 -2
  74. package/src/db/index.ts +2 -3
  75. package/src/db/runner-assignments.test.ts +163 -0
  76. package/src/db/runner-assignments.ts +115 -0
  77. package/src/db/runner-instances.test.ts +36 -23
  78. package/src/db/runner-instances.ts +3 -23
  79. package/src/db/schema/runner-instances.ts +3 -1
  80. package/src/index.ts +0 -9
  81. package/src/presentation/dto/index.ts +0 -4
  82. package/src/presentation/routes/assign-runner-instances.ts +49 -0
  83. package/src/presentation/routes/index.ts +4 -7
  84. package/src/presentation/routes/runner-enrollment.test.ts +60 -0
  85. package/src/presentation/routes/runner-enrollment.ts +22 -0
  86. package/test/globalSetup.ts +0 -1
  87. package/tsconfig.build.tsbuildinfo +1 -1
  88. package/dist/core/capacity-assignments.d.ts +0 -7
  89. package/dist/core/capacity-assignments.d.ts.map +0 -1
  90. package/dist/core/capacity-assignments.js +0 -6
  91. package/dist/core/capacity-assignments.js.map +0 -1
  92. package/dist/core/entities/capacity-assignment.d.ts +0 -9
  93. package/dist/core/entities/capacity-assignment.d.ts.map +0 -1
  94. package/dist/core/entities/capacity-assignment.js +0 -3
  95. package/dist/core/entities/capacity-assignment.js.map +0 -1
  96. package/dist/db/capacity-assignments.d.ts +0 -7
  97. package/dist/db/capacity-assignments.d.ts.map +0 -1
  98. package/dist/db/capacity-assignments.js +0 -72
  99. package/dist/db/capacity-assignments.js.map +0 -1
  100. package/dist/db/schema/capacity-assignments.d.ts +0 -113
  101. package/dist/db/schema/capacity-assignments.d.ts.map +0 -1
  102. package/dist/db/schema/capacity-assignments.js +0 -29
  103. package/dist/db/schema/capacity-assignments.js.map +0 -1
  104. package/dist/presentation/dto/mint-registration-tokens.d.ts +0 -7
  105. package/dist/presentation/dto/mint-registration-tokens.d.ts.map +0 -1
  106. package/dist/presentation/dto/mint-registration-tokens.js +0 -19
  107. package/dist/presentation/dto/mint-registration-tokens.js.map +0 -1
  108. package/dist/presentation/routes/assign-capacity.d.ts +0 -2
  109. package/dist/presentation/routes/assign-capacity.d.ts.map +0 -1
  110. package/dist/presentation/routes/assign-capacity.js +0 -44
  111. package/dist/presentation/routes/assign-capacity.js.map +0 -1
  112. package/dist/presentation/routes/mint-registration-tokens.d.ts +0 -2
  113. package/dist/presentation/routes/mint-registration-tokens.d.ts.map +0 -1
  114. package/dist/presentation/routes/mint-registration-tokens.js +0 -74
  115. package/dist/presentation/routes/mint-registration-tokens.js.map +0 -1
  116. package/dist/presentation/routes/planned-capacity.d.ts +0 -3
  117. package/dist/presentation/routes/planned-capacity.d.ts.map +0 -1
  118. package/dist/presentation/routes/planned-capacity.js +0 -54
  119. package/dist/presentation/routes/planned-capacity.js.map +0 -1
  120. package/src/core/capacity-assignments.ts +0 -10
  121. package/src/core/entities/capacity-assignment.ts +0 -8
  122. package/src/db/capacity-assignments.test.ts +0 -159
  123. package/src/db/capacity-assignments.ts +0 -116
  124. package/src/db/schema/capacity-assignments.ts +0 -34
  125. package/src/presentation/dto/mint-registration-tokens.ts +0 -28
  126. package/src/presentation/routes/assign-capacity.ts +0 -48
  127. package/src/presentation/routes/mint-registration-tokens.test.ts +0 -453
  128. package/src/presentation/routes/mint-registration-tokens.ts +0 -80
  129. package/src/presentation/routes/planned-capacity.ts +0 -49
@@ -2,18 +2,6 @@ CREATE TYPE "public"."runners_provider_runner_state" AS ENUM('starting', 'runnin
2
2
  CREATE TYPE "public"."runners_provisioner_scope" AS ENUM('workspace', 'installation');--> statement-breakpoint
3
3
  CREATE TYPE "public"."runners_runner_session_registration_token_kind" AS ENUM('manual', 'ephemeral', 'activation');--> statement-breakpoint
4
4
  CREATE TYPE "public"."runners_runner_session_scope" AS ENUM('workspace');--> statement-breakpoint
5
- CREATE TABLE "runners_capacity_assignments" (
6
- "id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
7
- "capacity_id" uuid NOT NULL,
8
- "reservation_id" uuid NOT NULL,
9
- "workspace_id" uuid NOT NULL,
10
- "provisioner_id" uuid NOT NULL,
11
- "created_at" timestamp with time zone DEFAULT now() NOT NULL
12
- );
13
- --> statement-breakpoint
14
- CREATE UNIQUE INDEX "runners_capacity_assignments_capacity_unique" ON "runners_capacity_assignments" USING btree ("capacity_id");--> statement-breakpoint
15
- CREATE INDEX "runners_capacity_assignments_reservation_idx" ON "runners_capacity_assignments" USING btree ("reservation_id");--> statement-breakpoint
16
- CREATE INDEX "runners_capacity_assignments_workspace_idx" ON "runners_capacity_assignments" USING btree ("workspace_id");--> statement-breakpoint
17
5
  CREATE TABLE "runners_ephemeral_registration_tokens" (
18
6
  "id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
19
7
  "workspace_id" uuid,
@@ -73,6 +61,7 @@ CREATE TABLE "runners_runner_instances" (
73
61
  "provisioner_id" uuid NOT NULL,
74
62
  "provider_runner_id" text,
75
63
  "reservation_id" uuid,
64
+ "assigned_at" timestamp with time zone,
76
65
  "template_key" text,
77
66
  "labels" text[] DEFAULT '{}' NOT NULL,
78
67
  "state" "runners_provider_runner_state" NOT NULL,
@@ -4,103 +4,6 @@
4
4
  "version": "7",
5
5
  "dialect": "postgresql",
6
6
  "tables": {
7
- "public.runners_capacity_assignments": {
8
- "name": "runners_capacity_assignments",
9
- "schema": "",
10
- "columns": {
11
- "id": {
12
- "name": "id",
13
- "type": "uuid",
14
- "primaryKey": true,
15
- "notNull": true,
16
- "default": "uuidv7()"
17
- },
18
- "capacity_id": {
19
- "name": "capacity_id",
20
- "type": "uuid",
21
- "primaryKey": false,
22
- "notNull": true
23
- },
24
- "reservation_id": {
25
- "name": "reservation_id",
26
- "type": "uuid",
27
- "primaryKey": false,
28
- "notNull": true
29
- },
30
- "workspace_id": {
31
- "name": "workspace_id",
32
- "type": "uuid",
33
- "primaryKey": false,
34
- "notNull": true
35
- },
36
- "provisioner_id": {
37
- "name": "provisioner_id",
38
- "type": "uuid",
39
- "primaryKey": false,
40
- "notNull": true
41
- },
42
- "created_at": {
43
- "name": "created_at",
44
- "type": "timestamp with time zone",
45
- "primaryKey": false,
46
- "notNull": true,
47
- "default": "now()"
48
- }
49
- },
50
- "indexes": {
51
- "runners_capacity_assignments_capacity_unique": {
52
- "name": "runners_capacity_assignments_capacity_unique",
53
- "columns": [
54
- {
55
- "expression": "capacity_id",
56
- "isExpression": false,
57
- "asc": true,
58
- "nulls": "last"
59
- }
60
- ],
61
- "isUnique": true,
62
- "concurrently": false,
63
- "method": "btree",
64
- "with": {}
65
- },
66
- "runners_capacity_assignments_reservation_idx": {
67
- "name": "runners_capacity_assignments_reservation_idx",
68
- "columns": [
69
- {
70
- "expression": "reservation_id",
71
- "isExpression": false,
72
- "asc": true,
73
- "nulls": "last"
74
- }
75
- ],
76
- "isUnique": false,
77
- "concurrently": false,
78
- "method": "btree",
79
- "with": {}
80
- },
81
- "runners_capacity_assignments_workspace_idx": {
82
- "name": "runners_capacity_assignments_workspace_idx",
83
- "columns": [
84
- {
85
- "expression": "workspace_id",
86
- "isExpression": false,
87
- "asc": true,
88
- "nulls": "last"
89
- }
90
- ],
91
- "isUnique": false,
92
- "concurrently": false,
93
- "method": "btree",
94
- "with": {}
95
- }
96
- },
97
- "foreignKeys": {},
98
- "compositePrimaryKeys": {},
99
- "uniqueConstraints": {},
100
- "policies": {},
101
- "checkConstraints": {},
102
- "isRLSEnabled": false
103
- },
104
7
  "public.runners_ephemeral_registration_tokens": {
105
8
  "name": "runners_ephemeral_registration_tokens",
106
9
  "schema": "",
@@ -852,6 +755,12 @@
852
755
  "primaryKey": false,
853
756
  "notNull": false
854
757
  },
758
+ "assigned_at": {
759
+ "name": "assigned_at",
760
+ "type": "timestamp with time zone",
761
+ "primaryKey": false,
762
+ "notNull": false
763
+ },
855
764
  "template_key": {
856
765
  "name": "template_key",
857
766
  "type": "text",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/api-runners",
3
3
  "license": "MIT",
4
- "version": "6.0.0",
4
+ "version": "7.0.2",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -27,42 +27,21 @@
27
27
  "zod": "^4.4.3",
28
28
  "@shipfox/api-auth-context": "6.0.0",
29
29
  "@shipfox/api-auth-dto": "6.0.0",
30
- "@shipfox/api-runners-dto": "6.0.0",
30
+ "@shipfox/api-runners-dto": "7.0.1",
31
31
  "@shipfox/api-workflows-dto": "6.0.0",
32
32
  "@shipfox/config": "1.2.2",
33
+ "@shipfox/inter-module": "0.2.0",
33
34
  "@shipfox/node-drizzle": "0.3.2",
34
- "@shipfox/node-auth-root-key": "0.2.0",
35
+ "@shipfox/node-auth-root-key": "0.2.1",
35
36
  "@shipfox/node-fastify": "0.2.4",
37
+ "@shipfox/node-module": "0.4.0",
36
38
  "@shipfox/node-opentelemetry": "0.5.2",
37
39
  "@shipfox/node-outbox": "0.2.4",
38
- "@shipfox/inter-module": "0.2.0",
39
- "@shipfox/node-module": "0.4.0",
40
40
  "@shipfox/node-postgres": "0.4.2",
41
- "@shipfox/node-tokens": "0.3.0",
41
+ "@shipfox/node-rate-limit": "0.3.0",
42
42
  "@shipfox/node-temporal": "0.3.2",
43
- "@shipfox/runner-labels": "0.1.1",
44
- "@shipfox/node-rate-limit": "0.3.0"
45
- },
46
- "devDependencies": {
47
- "@temporalio/activity": "1.18.1",
48
- "@temporalio/client": "1.18.1",
49
- "@temporalio/common": "1.18.1",
50
- "@temporalio/testing": "1.18.1",
51
- "@temporalio/worker": "1.18.1",
52
- "@types/pg": "^8.15.5",
53
- "drizzle-kit": "^0.31.10",
54
- "fastify": "^5.3.3",
55
- "fastify-type-provider-zod": "^6.0.0",
56
- "fishery": "^2.4.0",
57
- "@shipfox/api-auth": "6.0.0",
58
- "@shipfox/api-auth-dto": "6.0.0",
59
- "@shipfox/biome": "1.8.2",
60
- "@shipfox/depcruise": "1.0.2",
61
- "@shipfox/node-jwt": "0.3.0",
62
- "@shipfox/swc": "1.2.6",
63
- "@shipfox/typescript": "1.1.7",
64
- "@shipfox/vitest": "1.2.3",
65
- "@shipfox/ts-config": "1.3.8"
43
+ "@shipfox/node-tokens": "0.3.0",
44
+ "@shipfox/runner-labels": "0.1.1"
66
45
  },
67
46
  "scripts": {
68
47
  "build": "shipfox-swc && shipfox-temporal-bundle dist/temporal/workflows/index.js",
package/src/config.ts CHANGED
@@ -1,8 +1,8 @@
1
- import {REGISTRATION_TOKEN_BATCH_HARD_MAX} from '@shipfox/api-runners-dto';
2
1
  import {bool, createConfig, num} from '@shipfox/config';
3
2
  import {STUCK_JOB_THRESHOLD_SECONDS} from '#core/maintenance-policy.js';
4
3
 
5
4
  const EPHEMERAL_REGISTRATION_TOKEN_TTL_HARD_MAX_SECONDS = 3600;
5
+ const REGISTRATION_TOKEN_BATCH_HARD_MAX = 1000;
6
6
  const RUNNER_CONTROL_PLANE_TOKEN_TTL_HARD_MAX_SECONDS = 3600;
7
7
 
8
8
  export const config = createConfig({
@@ -12,6 +12,7 @@ export interface RunnerInstance {
12
12
  provisionerId: string;
13
13
  providerRunnerId: string;
14
14
  reservationId: string | null;
15
+ assignedAt?: Date | null;
15
16
  templateKey: string | null;
16
17
  labels: string[];
17
18
  state: RunnerInstanceState;
@@ -86,16 +86,16 @@ export class ReservationExpiredError extends Error {
86
86
  }
87
87
  }
88
88
 
89
- export class CapacityNotAssignableError extends Error {
90
- constructor(public readonly capacityId: string) {
91
- super(`Capacity cannot be assigned: ${capacityId}`);
92
- this.name = 'CapacityNotAssignableError';
89
+ export class RunnerInstanceNotAssignableError extends Error {
90
+ constructor(public readonly runnerInstanceId: string) {
91
+ super(`Runner instance cannot be assigned: ${runnerInstanceId}`);
92
+ this.name = 'RunnerInstanceNotAssignableError';
93
93
  }
94
94
  }
95
- export class CapacityAlreadyAssignedError extends Error {
96
- constructor(public readonly capacityId: string) {
97
- super(`Capacity is already assigned: ${capacityId}`);
98
- this.name = 'CapacityAlreadyAssignedError';
95
+ export class RunnerInstanceAlreadyAssignedError extends Error {
96
+ constructor(public readonly runnerInstanceId: string) {
97
+ super(`Runner instance is already assigned: ${runnerInstanceId}`);
98
+ this.name = 'RunnerInstanceAlreadyAssignedError';
99
99
  }
100
100
  }
101
101
  export class ReservationAlreadyAssignedError extends Error {
package/src/core/index.ts CHANGED
@@ -1,5 +1,3 @@
1
- export {assignCapacityBatch} from './capacity-assignments.js';
2
- export type {CapacityAssignment} from './entities/capacity-assignment.js';
3
1
  export type {EphemeralRegistrationToken} from './entities/ephemeral-registration-token.js';
4
2
  export type {ManualRegistrationToken} from './entities/manual-registration-token.js';
5
3
  export type {ProvisionerCapabilitySnapshot} from './entities/provisioner-capability-snapshot.js';
@@ -13,11 +11,6 @@ export type {
13
11
  RunnerInstanceState,
14
12
  } from './entities/runner-instance.js';
15
13
  export type {RunnerSession} from './entities/runner-session.js';
16
- export {
17
- type MintEphemeralRegistrationTokenParams,
18
- type MintEphemeralRegistrationTokenResult,
19
- mintEphemeralRegistrationToken,
20
- } from './ephemeral-registration-tokens.js';
21
14
  export {
22
15
  EmptyRunnerLabelsError,
23
16
  ManualRegistrationTokenNotFoundError,
@@ -43,6 +36,7 @@ export {
43
36
  revokeWorkspaceProvisionerToken,
44
37
  } from './provisioner-tokens.js';
45
38
  export {getRunnerAssignment, issueRunnerActivationToken} from './runner-activation.js';
39
+ export {assignRunnerInstances} from './runner-assignments.js';
46
40
  export {
47
41
  attachRunnerControlProviderId,
48
42
  createRunnerInstancesWithBootstrapTokens,
@@ -54,8 +48,7 @@ export {
54
48
  } from './runner-control-sessions.js';
55
49
  export {
56
50
  type ActiveRunner,
57
- attachProviderRunnerId,
58
- createPlannedProvisionedCapacity,
51
+ attachRunnerInstanceProviderId,
59
52
  listActiveRunners,
60
53
  type ReconcileDesiredIntent,
61
54
  type ReconciledBoundJobExecution,
@@ -0,0 +1,9 @@
1
+ import {assignRunnerInstances as assignRunnerInstancesDb} from '#db/runner-assignments.js';
2
+
3
+ export function assignRunnerInstances(params: {
4
+ provisionerId: string;
5
+ reservationId: string;
6
+ runnerInstanceIds: string[];
7
+ }): Promise<string[]> {
8
+ return assignRunnerInstancesDb(params);
9
+ }
@@ -1,7 +1,6 @@
1
1
  import type {RunnerInstance, RunnerInstanceState} from '#core/entities/runner-instance.js';
2
2
  import {
3
- attachProviderRunnerId as attachProviderRunnerIdDb,
4
- createPlannedProvisionedCapacity as createPlannedProvisionedCapacityDb,
3
+ attachRunnerInstanceProviderId as attachRunnerInstanceProviderIdDb,
5
4
  isTerminalState,
6
5
  listActiveRunnerInstances,
7
6
  listActiveRunningJobExecutions,
@@ -29,20 +28,12 @@ export interface ReportRunnerInstancesParams {
29
28
  events: RunnerInstanceReportEvent[];
30
29
  }
31
30
 
32
- export function createPlannedProvisionedCapacity(params: {
33
- provisionerId: string;
34
- providerKind: string | null;
35
- templateKey: string | null;
36
- }): Promise<{capacityId: string}> {
37
- return createPlannedProvisionedCapacityDb(params);
38
- }
39
-
40
- export function attachProviderRunnerId(params: {
41
- capacityId: string;
31
+ export function attachRunnerInstanceProviderId(params: {
32
+ runnerInstanceId: string;
42
33
  provisionerId: string;
43
34
  providerRunnerId: string;
44
35
  }): Promise<boolean> {
45
- return attachProviderRunnerIdDb(params);
36
+ return attachRunnerInstanceProviderIdDb(params);
46
37
  }
47
38
 
48
39
  export interface ReportRunnerInstancesResult {
package/src/db/db.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import {drizzle, type NodePgDatabase} from '@shipfox/node-drizzle';
2
2
  import {pgClient} from '@shipfox/node-postgres';
3
- import {capacityAssignments} from './schema/capacity-assignments.js';
4
3
  import {ephemeralRegistrationTokens} from './schema/ephemeral-registration-tokens.js';
5
4
  import {manualRegistrationTokens} from './schema/manual-registration-tokens.js';
6
5
  import {runnersOutbox} from './schema/outbox.js';
@@ -16,7 +15,6 @@ import {runnerSessions} from './schema/runner-sessions.js';
16
15
  import {runningJobExecutions} from './schema/running-job-executions.js';
17
16
 
18
17
  export const schema = {
19
- capacityAssignments,
20
18
  ephemeralRegistrationTokens,
21
19
  pendingJobExecutions,
22
20
  providerRunners,
package/src/db/index.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import {dirname, resolve} from 'node:path';
2
2
  import {fileURLToPath} from 'node:url';
3
3
 
4
- export {assignCapacityBatch} from './capacity-assignments.js';
5
4
  export {closeDb, db, schema} from './db.js';
6
5
  export type {
7
6
  CreateEphemeralRegistrationTokenParams,
@@ -67,6 +66,7 @@ export {
67
66
  pollDemandAndReserve,
68
67
  releaseReservationUnits,
69
68
  } from './reservations.js';
69
+ export {assignRunnerInstances} from './runner-assignments.js';
70
70
  export type {
71
71
  ActiveRunnerInstanceTemplateCount,
72
72
  ReapStaleRunnerInstancesResult,
@@ -78,8 +78,7 @@ export type {
78
78
  RunnerInstanceTerminateIntentReason,
79
79
  } from './runner-instances.js';
80
80
  export {
81
- attachProviderRunnerId,
82
- createPlannedProvisionedCapacity,
81
+ attachRunnerInstanceProviderId,
83
82
  isTerminalState,
84
83
  listActiveRunnerInstanceCountsByTemplateTx,
85
84
  listActiveRunnerInstances,
@@ -0,0 +1,163 @@
1
+ import {eq} from 'drizzle-orm';
2
+ import {
3
+ ReservationExpiredError,
4
+ RunnerInstanceAlreadyAssignedError,
5
+ RunnerInstanceNotAssignableError,
6
+ } from '#core/errors.js';
7
+ import {db} from '#db/db.js';
8
+ import {assignRunnerInstances} from '#db/runner-assignments.js';
9
+ import {reservations} from '#db/schema/reservations.js';
10
+ import {runnerControlSessions} from '#db/schema/runner-control-sessions.js';
11
+ import {providerRunners} from '#db/schema/runner-instances.js';
12
+
13
+ describe('assignRunnerInstances', () => {
14
+ let workspaceId: string;
15
+ let provisionerId: string;
16
+
17
+ beforeEach(() => {
18
+ workspaceId = crypto.randomUUID();
19
+ provisionerId = crypto.randomUUID();
20
+ });
21
+
22
+ it('writes the reservation and workspace after enrollment', async () => {
23
+ const reservation = await createReservation();
24
+ const runner = await createEnrolledRunner();
25
+
26
+ const assigned = await assignRunnerInstances({
27
+ provisionerId,
28
+ reservationId: reservation.id,
29
+ runnerInstanceIds: [runner.id],
30
+ });
31
+
32
+ expect(assigned).toEqual([runner.id]);
33
+ const [stored] = await db()
34
+ .select()
35
+ .from(providerRunners)
36
+ .where(eq(providerRunners.id, runner.id));
37
+ expect(stored).toMatchObject({
38
+ workspaceId,
39
+ reservationId: reservation.id,
40
+ assignedAt: expect.any(Date),
41
+ });
42
+ });
43
+
44
+ it('is idempotent for concurrent retries of the same assignment', async () => {
45
+ const reservation = await createReservation();
46
+ const runner = await createEnrolledRunner();
47
+
48
+ const results = await Promise.all([
49
+ assignRunnerInstances({
50
+ provisionerId,
51
+ reservationId: reservation.id,
52
+ runnerInstanceIds: [runner.id],
53
+ }),
54
+ assignRunnerInstances({
55
+ provisionerId,
56
+ reservationId: reservation.id,
57
+ runnerInstanceIds: [runner.id],
58
+ }),
59
+ ]);
60
+
61
+ expect(results).toEqual([[runner.id], [runner.id]]);
62
+ });
63
+
64
+ it('rejects expired reservations', async () => {
65
+ const reservation = await createReservation({expiresAt: new Date(Date.now() - 1_000)});
66
+ const runner = await createEnrolledRunner();
67
+
68
+ const assignment = assignRunnerInstances({
69
+ provisionerId,
70
+ reservationId: reservation.id,
71
+ runnerInstanceIds: [runner.id],
72
+ });
73
+
74
+ await expect(assignment).rejects.toThrow(ReservationExpiredError);
75
+ });
76
+
77
+ it('rejects unenrolled or incompatible runners', async () => {
78
+ const reservation = await createReservation({requiredLabels: ['linux', 'gpu']});
79
+ const runner = await createEnrolledRunner({labels: ['linux']});
80
+
81
+ const assignment = assignRunnerInstances({
82
+ provisionerId,
83
+ reservationId: reservation.id,
84
+ runnerInstanceIds: [runner.id],
85
+ });
86
+
87
+ await expect(assignment).rejects.toThrow(RunnerInstanceNotAssignableError);
88
+ });
89
+
90
+ it('rejects a runner assigned to a different reservation', async () => {
91
+ const firstReservation = await createReservation();
92
+ const secondReservation = await createReservation();
93
+ const runner = await createEnrolledRunner();
94
+ await assignRunnerInstances({
95
+ provisionerId,
96
+ reservationId: firstReservation.id,
97
+ runnerInstanceIds: [runner.id],
98
+ });
99
+
100
+ const assignment = assignRunnerInstances({
101
+ provisionerId,
102
+ reservationId: secondReservation.id,
103
+ runnerInstanceIds: [runner.id],
104
+ });
105
+
106
+ await expect(assignment).rejects.toThrow(RunnerInstanceAlreadyAssignedError);
107
+ });
108
+
109
+ it('rejects assignments that exceed reservation capacity', async () => {
110
+ const reservation = await createReservation();
111
+ const firstRunner = await createEnrolledRunner();
112
+ const secondRunner = await createEnrolledRunner();
113
+
114
+ const assignment = assignRunnerInstances({
115
+ provisionerId,
116
+ reservationId: reservation.id,
117
+ runnerInstanceIds: [firstRunner.id, secondRunner.id],
118
+ });
119
+
120
+ await expect(assignment).rejects.toThrow(RunnerInstanceNotAssignableError);
121
+ });
122
+
123
+ async function createReservation(
124
+ overrides: Partial<{expiresAt: Date; requiredLabels: string[]}> = {},
125
+ ) {
126
+ const [reservation] = await db()
127
+ .insert(reservations)
128
+ .values({
129
+ workspaceId,
130
+ provisionerId,
131
+ requiredLabels: overrides.requiredLabels ?? ['linux'],
132
+ count: 1,
133
+ expiresAt: overrides.expiresAt ?? new Date(Date.now() + 60_000),
134
+ })
135
+ .returning();
136
+ if (!reservation) throw new Error('Reservation insert returned no row');
137
+ return reservation;
138
+ }
139
+
140
+ async function createEnrolledRunner(overrides: Partial<{labels: string[]}> = {}) {
141
+ const [runner] = await db()
142
+ .insert(providerRunners)
143
+ .values({
144
+ provisionerId,
145
+ providerRunnerId: crypto.randomUUID(),
146
+ labels: overrides.labels ?? ['linux'],
147
+ state: 'running',
148
+ reportedAt: new Date(),
149
+ })
150
+ .returning();
151
+ if (!runner) throw new Error('Runner instance insert returned no row');
152
+ await db()
153
+ .insert(runnerControlSessions)
154
+ .values({
155
+ runnerInstanceId: runner.id,
156
+ provisionerId,
157
+ hashedToken: crypto.randomUUID(),
158
+ prefix: 'test',
159
+ expiresAt: new Date(Date.now() + 60_000),
160
+ });
161
+ return runner;
162
+ }
163
+ });
@@ -0,0 +1,115 @@
1
+ import {and, eq, inArray, isNull, sql} from 'drizzle-orm';
2
+ import {
3
+ ReservationExpiredError,
4
+ ReservationNotFoundError,
5
+ RunnerInstanceAlreadyAssignedError,
6
+ RunnerInstanceNotAssignableError,
7
+ } from '#core/errors.js';
8
+ import {db} from './db.js';
9
+ import {reservations} from './schema/reservations.js';
10
+ import {runnerControlSessions} from './schema/runner-control-sessions.js';
11
+ import {providerRunners} from './schema/runner-instances.js';
12
+
13
+ /** Atomically consumes reservation units by writing the immutable assignment on each runner. */
14
+ export async function assignRunnerInstances(params: {
15
+ provisionerId: string;
16
+ reservationId: string;
17
+ runnerInstanceIds: string[];
18
+ }): Promise<string[]> {
19
+ const runnerInstanceIds = [...params.runnerInstanceIds].sort();
20
+ return await db().transaction(async (tx) => {
21
+ await tx.execute(
22
+ sql`select pg_advisory_xact_lock(hashtext(${`runners_assignment:${params.provisionerId}:${params.reservationId}`}))`,
23
+ );
24
+ const [reservation] = await tx
25
+ .select()
26
+ .from(reservations)
27
+ .where(
28
+ and(
29
+ eq(reservations.id, params.reservationId),
30
+ eq(reservations.provisionerId, params.provisionerId),
31
+ ),
32
+ )
33
+ .limit(1)
34
+ .for('update');
35
+ if (!reservation) throw new ReservationNotFoundError(params.reservationId);
36
+ if (reservation.expiresAt <= new Date())
37
+ throw new ReservationExpiredError(params.reservationId);
38
+
39
+ const runnerRows = await tx
40
+ .select({
41
+ id: providerRunners.id,
42
+ reservationId: providerRunners.reservationId,
43
+ workspaceId: providerRunners.workspaceId,
44
+ providerRunnerId: providerRunners.providerRunnerId,
45
+ labels: providerRunners.labels,
46
+ state: providerRunners.state,
47
+ })
48
+ .from(providerRunners)
49
+ .where(
50
+ and(
51
+ eq(providerRunners.provisionerId, params.provisionerId),
52
+ inArray(providerRunners.id, runnerInstanceIds),
53
+ ),
54
+ )
55
+ .for('update');
56
+ const activeControlSessions = await tx
57
+ .select({runnerInstanceId: runnerControlSessions.runnerInstanceId})
58
+ .from(runnerControlSessions)
59
+ .where(
60
+ and(
61
+ inArray(
62
+ runnerControlSessions.runnerInstanceId,
63
+ runnerRows.map((runner) => runner.id),
64
+ ),
65
+ isNull(runnerControlSessions.closedAt),
66
+ ),
67
+ );
68
+ const runnerInstanceIdsWithControlSession = new Set(
69
+ activeControlSessions.map((session) => session.runnerInstanceId),
70
+ );
71
+ const runners = runnerRows.map((runner) => ({
72
+ ...runner,
73
+ controlSessionId: runnerInstanceIdsWithControlSession.has(runner.id) ? runner.id : null,
74
+ }));
75
+ if (runners.length !== runnerInstanceIds.length)
76
+ throw new RunnerInstanceNotAssignableError(runnerInstanceIds[0] ?? '');
77
+
78
+ const alreadyAssigned = runners.filter((runner) => runner.reservationId !== null);
79
+ if (alreadyAssigned.some((runner) => runner.reservationId !== reservation.id))
80
+ throw new RunnerInstanceAlreadyAssignedError(alreadyAssigned[0]?.id ?? '');
81
+ const newRunners = runners.filter((runner) => runner.reservationId === null);
82
+ const assignedCount = await tx
83
+ .select({count: sql<number>`count(*)::int`})
84
+ .from(providerRunners)
85
+ .where(eq(providerRunners.reservationId, reservation.id));
86
+ if ((assignedCount[0]?.count ?? 0) + newRunners.length > reservation.count)
87
+ throw new RunnerInstanceNotAssignableError(newRunners[0]?.id ?? '');
88
+ for (const runner of newRunners) {
89
+ if (
90
+ runner.state !== 'running' ||
91
+ !runner.providerRunnerId ||
92
+ !runner.controlSessionId ||
93
+ !reservation.requiredLabels.every((label) => runner.labels.includes(label))
94
+ )
95
+ throw new RunnerInstanceNotAssignableError(runner.id);
96
+ }
97
+ if (newRunners.length > 0) {
98
+ await tx
99
+ .update(providerRunners)
100
+ .set({
101
+ workspaceId: reservation.workspaceId,
102
+ reservationId: reservation.id,
103
+ assignedAt: sql`now()`,
104
+ updatedAt: sql`now()`,
105
+ })
106
+ .where(
107
+ inArray(
108
+ providerRunners.id,
109
+ newRunners.map((runner) => runner.id),
110
+ ),
111
+ );
112
+ }
113
+ return params.runnerInstanceIds;
114
+ });
115
+ }