@shipfox/api-runners-dto 2.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 +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +59 -0
- package/LICENSE +21 -0
- package/dist/events.d.ts +55 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +33 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/claim-job.d.ts +11 -0
- package/dist/schemas/claim-job.d.ts.map +1 -0
- package/dist/schemas/claim-job.js +11 -0
- package/dist/schemas/claim-job.js.map +1 -0
- package/dist/schemas/heartbeat.d.ts +20 -0
- package/dist/schemas/heartbeat.d.ts.map +1 -0
- package/dist/schemas/heartbeat.js +11 -0
- package/dist/schemas/heartbeat.js.map +1 -0
- package/dist/schemas/index.d.ts +12 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +13 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/list-active-provisioners.d.ts +18 -0
- package/dist/schemas/list-active-provisioners.d.ts.map +1 -0
- package/dist/schemas/list-active-provisioners.js +12 -0
- package/dist/schemas/list-active-provisioners.js.map +1 -0
- package/dist/schemas/manual-registration-token.d.ts +53 -0
- package/dist/schemas/manual-registration-token.d.ts.map +1 -0
- package/dist/schemas/manual-registration-token.js +31 -0
- package/dist/schemas/manual-registration-token.js.map +1 -0
- package/dist/schemas/mint-registration-tokens.d.ts +28 -0
- package/dist/schemas/mint-registration-tokens.d.ts.map +1 -0
- package/dist/schemas/mint-registration-tokens.js +24 -0
- package/dist/schemas/mint-registration-tokens.js.map +1 -0
- package/dist/schemas/poll-demand.d.ts +52 -0
- package/dist/schemas/poll-demand.d.ts.map +1 -0
- package/dist/schemas/poll-demand.js +34 -0
- package/dist/schemas/poll-demand.js.map +1 -0
- package/dist/schemas/provisioner-token.d.ts +72 -0
- package/dist/schemas/provisioner-token.d.ts.map +1 -0
- package/dist/schemas/provisioner-token.js +43 -0
- package/dist/schemas/provisioner-token.js.map +1 -0
- package/dist/schemas/reconcile-provisioned-runners.d.ts +71 -0
- package/dist/schemas/reconcile-provisioned-runners.d.ts.map +1 -0
- package/dist/schemas/reconcile-provisioned-runners.js +36 -0
- package/dist/schemas/reconcile-provisioned-runners.js.map +1 -0
- package/dist/schemas/register.d.ts +27 -0
- package/dist/schemas/register.d.ts.map +1 -0
- package/dist/schemas/register.js +19 -0
- package/dist/schemas/register.js.map +1 -0
- package/dist/schemas/report-provisioned-runners.d.ts +109 -0
- package/dist/schemas/report-provisioned-runners.d.ts.map +1 -0
- package/dist/schemas/report-provisioned-runners.js +58 -0
- package/dist/schemas/report-provisioned-runners.js.map +1 -0
- package/dist/schemas/tool-capabilities.d.ts +17 -0
- package/dist/schemas/tool-capabilities.d.ts.map +1 -0
- package/dist/schemas/tool-capabilities.js +27 -0
- package/dist/schemas/tool-capabilities.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/package.json +56 -0
- package/src/events.ts +46 -0
- package/src/index.ts +107 -0
- package/src/schemas/claim-job.test.ts +53 -0
- package/src/schemas/claim-job.ts +13 -0
- package/src/schemas/heartbeat.ts +16 -0
- package/src/schemas/index.ts +115 -0
- package/src/schemas/list-active-provisioners.ts +17 -0
- package/src/schemas/manual-registration-token.test.ts +36 -0
- package/src/schemas/manual-registration-token.ts +58 -0
- package/src/schemas/mint-registration-tokens.test.ts +28 -0
- package/src/schemas/mint-registration-tokens.ts +46 -0
- package/src/schemas/poll-demand.test.ts +27 -0
- package/src/schemas/poll-demand.ts +49 -0
- package/src/schemas/provisioner-token.test.ts +38 -0
- package/src/schemas/provisioner-token.ts +64 -0
- package/src/schemas/reconcile-provisioned-runners.test.ts +85 -0
- package/src/schemas/reconcile-provisioned-runners.ts +61 -0
- package/src/schemas/register.ts +28 -0
- package/src/schemas/report-provisioned-runners.test.ts +61 -0
- package/src/schemas/report-provisioned-runners.ts +75 -0
- package/src/schemas/tool-capabilities.test.ts +64 -0
- package/src/schemas/tool-capabilities.ts +35 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +8 -0
- package/vitest.config.ts +3 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createProvisionerTokenBodySchema,
|
|
3
|
+
MAX_PROVISIONER_TOKEN_TTL_SECONDS,
|
|
4
|
+
provisionerIdentityResponseSchema,
|
|
5
|
+
} from './provisioner-token.js';
|
|
6
|
+
|
|
7
|
+
describe('provisioner token schemas', () => {
|
|
8
|
+
it('accepts a valid create body', () => {
|
|
9
|
+
const result = createProvisionerTokenBodySchema.parse({
|
|
10
|
+
name: 'autoscaler',
|
|
11
|
+
ttl_seconds: MAX_PROVISIONER_TOKEN_TTL_SECONDS,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(result).toEqual({
|
|
15
|
+
name: 'autoscaler',
|
|
16
|
+
ttl_seconds: MAX_PROVISIONER_TOKEN_TTL_SECONDS,
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('rejects token TTLs above one year', () => {
|
|
21
|
+
const result = createProvisionerTokenBodySchema.safeParse({
|
|
22
|
+
ttl_seconds: MAX_PROVISIONER_TOKEN_TTL_SECONDS + 1,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(result.success).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('parses provisioner identity responses', () => {
|
|
29
|
+
const identity = {
|
|
30
|
+
id: crypto.randomUUID(),
|
|
31
|
+
workspace_id: crypto.randomUUID(),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const result = provisionerIdentityResponseSchema.parse(identity);
|
|
35
|
+
|
|
36
|
+
expect(result).toEqual(identity);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {z} from 'zod';
|
|
2
|
+
|
|
3
|
+
export const MAX_PROVISIONER_TOKEN_TTL_SECONDS = 31_536_000;
|
|
4
|
+
|
|
5
|
+
export const createProvisionerTokenBodySchema = z.object({
|
|
6
|
+
name: z.string().min(1).optional(),
|
|
7
|
+
ttl_seconds: z.number().int().positive().max(MAX_PROVISIONER_TOKEN_TTL_SECONDS).optional(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export type CreateProvisionerTokenBodyDto = z.infer<typeof createProvisionerTokenBodySchema>;
|
|
11
|
+
|
|
12
|
+
export const createProvisionerTokenResponseSchema = z.object({
|
|
13
|
+
id: z.string().uuid(),
|
|
14
|
+
raw_token: z.string(),
|
|
15
|
+
prefix: z.string(),
|
|
16
|
+
name: z.string().nullable(),
|
|
17
|
+
workspace_id: z.string().uuid(),
|
|
18
|
+
created_by_user_id: z.string().uuid(),
|
|
19
|
+
revoked_by_user_id: z.string().uuid().nullable(),
|
|
20
|
+
expires_at: z.string().nullable(),
|
|
21
|
+
revoked_at: z.string().nullable(),
|
|
22
|
+
last_seen_at: z.string().nullable(),
|
|
23
|
+
created_at: z.string(),
|
|
24
|
+
updated_at: z.string(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export type CreateProvisionerTokenResponseDto = z.infer<
|
|
28
|
+
typeof createProvisionerTokenResponseSchema
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
export const provisionerTokenDtoSchema = z.object({
|
|
32
|
+
id: z.string().uuid(),
|
|
33
|
+
workspace_id: z.string().uuid(),
|
|
34
|
+
prefix: z.string(),
|
|
35
|
+
name: z.string().nullable(),
|
|
36
|
+
created_by_user_id: z.string().uuid(),
|
|
37
|
+
revoked_by_user_id: z.string().uuid().nullable(),
|
|
38
|
+
expires_at: z.string().nullable(),
|
|
39
|
+
revoked_at: z.string().nullable(),
|
|
40
|
+
last_seen_at: z.string().nullable(),
|
|
41
|
+
created_at: z.string(),
|
|
42
|
+
updated_at: z.string(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export type ProvisionerTokenDto = z.infer<typeof provisionerTokenDtoSchema>;
|
|
46
|
+
|
|
47
|
+
export const listProvisionerTokensResponseSchema = z.object({
|
|
48
|
+
tokens: z.array(provisionerTokenDtoSchema),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export type ListProvisionerTokensResponseDto = z.infer<typeof listProvisionerTokensResponseSchema>;
|
|
52
|
+
|
|
53
|
+
export const revokeProvisionerTokenResponseSchema = provisionerTokenDtoSchema;
|
|
54
|
+
|
|
55
|
+
export type RevokeProvisionerTokenResponseDto = z.infer<
|
|
56
|
+
typeof revokeProvisionerTokenResponseSchema
|
|
57
|
+
>;
|
|
58
|
+
|
|
59
|
+
export const provisionerIdentityResponseSchema = z.object({
|
|
60
|
+
id: z.string().uuid(),
|
|
61
|
+
workspace_id: z.string().uuid(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export type ProvisionerIdentityResponseDto = z.infer<typeof provisionerIdentityResponseSchema>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MAX_OBSERVED_PROVISIONED_RUNNER_ID_LENGTH,
|
|
3
|
+
MAX_RECONCILE_OBSERVED_RUNNERS,
|
|
4
|
+
reconcileProvisionedRunnersBodySchema,
|
|
5
|
+
reconcileProvisionedRunnersResponseSchema,
|
|
6
|
+
} from './reconcile-provisioned-runners.js';
|
|
7
|
+
|
|
8
|
+
describe('reconcileProvisionedRunnersBodySchema', () => {
|
|
9
|
+
it('accepts observed provisioned runner ids', () => {
|
|
10
|
+
const result = reconcileProvisionedRunnersBodySchema.safeParse({
|
|
11
|
+
observed_provisioned_runner_ids: ['01JPROVISIONEDRUNNER000001'],
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(result.success).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('accepts an empty observed set', () => {
|
|
18
|
+
const result = reconcileProvisionedRunnersBodySchema.safeParse({
|
|
19
|
+
observed_provisioned_runner_ids: [],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(result.success).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('rejects observed sets above the reconcile limit', () => {
|
|
26
|
+
const result = reconcileProvisionedRunnersBodySchema.safeParse({
|
|
27
|
+
observed_provisioned_runner_ids: Array.from(
|
|
28
|
+
{length: MAX_RECONCILE_OBSERVED_RUNNERS + 1},
|
|
29
|
+
(_, index) => `runner-${index}`,
|
|
30
|
+
),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(result.success).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('rejects over-length provisioned runner ids', () => {
|
|
37
|
+
const result = reconcileProvisionedRunnersBodySchema.safeParse({
|
|
38
|
+
observed_provisioned_runner_ids: ['a'.repeat(MAX_OBSERVED_PROVISIONED_RUNNER_ID_LENGTH + 1)],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(result.success).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('rejects duplicate provisioned runner ids', () => {
|
|
45
|
+
const result = reconcileProvisionedRunnersBodySchema.safeParse({
|
|
46
|
+
observed_provisioned_runner_ids: ['provisioned-runner-1', 'provisioned-runner-1'],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(result.success).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('rejects extra fields', () => {
|
|
53
|
+
const result = reconcileProvisionedRunnersBodySchema.safeParse({
|
|
54
|
+
observed_provisioned_runner_ids: [],
|
|
55
|
+
provisioner_hostname: 'worker-1',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(result.success).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('reconcileProvisionedRunnersResponseSchema', () => {
|
|
63
|
+
it('parses reconciled provisioned runner responses', () => {
|
|
64
|
+
const result = reconcileProvisionedRunnersResponseSchema.safeParse({
|
|
65
|
+
runners: [
|
|
66
|
+
{
|
|
67
|
+
provisioned_runner_id: 'provisioned-runner-1',
|
|
68
|
+
state: 'running',
|
|
69
|
+
reservation_id: crypto.randomUUID(),
|
|
70
|
+
runner_session_id: crypto.randomUUID(),
|
|
71
|
+
bound_job: {
|
|
72
|
+
job_id: crypto.randomUUID(),
|
|
73
|
+
workflow_run_attempt_id: crypto.randomUUID(),
|
|
74
|
+
last_heartbeat_at: new Date().toISOString(),
|
|
75
|
+
cancellation_requested_at: null,
|
|
76
|
+
},
|
|
77
|
+
desired_intent: 'keep',
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
terminated_absent_provisioned_runner_ids: ['provisioned-runner-2'],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(result.success).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {z} from 'zod';
|
|
2
|
+
import {provisionedRunnerStateSchema} from './report-provisioned-runners.js';
|
|
3
|
+
|
|
4
|
+
export const MAX_RECONCILE_OBSERVED_RUNNERS = 5000;
|
|
5
|
+
export const MAX_OBSERVED_PROVISIONED_RUNNER_ID_LENGTH = 255;
|
|
6
|
+
|
|
7
|
+
export const reconcileProvisionedRunnersBodySchema = z
|
|
8
|
+
.object({
|
|
9
|
+
observed_provisioned_runner_ids: z
|
|
10
|
+
.array(z.string().min(1).max(MAX_OBSERVED_PROVISIONED_RUNNER_ID_LENGTH))
|
|
11
|
+
.max(MAX_RECONCILE_OBSERVED_RUNNERS),
|
|
12
|
+
})
|
|
13
|
+
.strict()
|
|
14
|
+
.refine(
|
|
15
|
+
(body) =>
|
|
16
|
+
new Set(body.observed_provisioned_runner_ids).size ===
|
|
17
|
+
body.observed_provisioned_runner_ids.length,
|
|
18
|
+
{
|
|
19
|
+
message: 'observed_provisioned_runner_ids values must be unique',
|
|
20
|
+
path: ['observed_provisioned_runner_ids'],
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export const reconcileDesiredIntentSchema = z.enum(['keep', 'terminate']);
|
|
25
|
+
|
|
26
|
+
export const reconciledBoundJobSchema = z
|
|
27
|
+
.object({
|
|
28
|
+
job_id: z.string().uuid(),
|
|
29
|
+
workflow_run_attempt_id: z.string().uuid(),
|
|
30
|
+
last_heartbeat_at: z.string().datetime(),
|
|
31
|
+
cancellation_requested_at: z.string().datetime().nullable(),
|
|
32
|
+
})
|
|
33
|
+
.strict();
|
|
34
|
+
|
|
35
|
+
export const reconciledProvisionedRunnerSchema = z
|
|
36
|
+
.object({
|
|
37
|
+
provisioned_runner_id: z.string(),
|
|
38
|
+
state: provisionedRunnerStateSchema.nullable(),
|
|
39
|
+
reservation_id: z.string().uuid().nullable(),
|
|
40
|
+
runner_session_id: z.string().uuid().nullable(),
|
|
41
|
+
bound_job: reconciledBoundJobSchema.nullable(),
|
|
42
|
+
desired_intent: reconcileDesiredIntentSchema,
|
|
43
|
+
})
|
|
44
|
+
.strict();
|
|
45
|
+
|
|
46
|
+
export const reconcileProvisionedRunnersResponseSchema = z
|
|
47
|
+
.object({
|
|
48
|
+
runners: z.array(reconciledProvisionedRunnerSchema),
|
|
49
|
+
terminated_absent_provisioned_runner_ids: z.array(z.string()),
|
|
50
|
+
})
|
|
51
|
+
.strict();
|
|
52
|
+
|
|
53
|
+
export type ReconcileProvisionedRunnersBodyDto = z.infer<
|
|
54
|
+
typeof reconcileProvisionedRunnersBodySchema
|
|
55
|
+
>;
|
|
56
|
+
export type ReconcileDesiredIntentDto = z.infer<typeof reconcileDesiredIntentSchema>;
|
|
57
|
+
export type ReconciledBoundJobDto = z.infer<typeof reconciledBoundJobSchema>;
|
|
58
|
+
export type ReconciledProvisionedRunnerDto = z.infer<typeof reconciledProvisionedRunnerSchema>;
|
|
59
|
+
export type ReconcileProvisionedRunnersResponseDto = z.infer<
|
|
60
|
+
typeof reconcileProvisionedRunnersResponseSchema
|
|
61
|
+
>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MAX_RUNNER_LABEL_LENGTH,
|
|
3
|
+
MAX_RUNNER_LABELS,
|
|
4
|
+
RUNNER_LABEL_PATTERN,
|
|
5
|
+
} from '@shipfox/runner-labels';
|
|
6
|
+
import {z} from 'zod';
|
|
7
|
+
import {runnerToolCapabilitiesSchema} from './tool-capabilities.js';
|
|
8
|
+
|
|
9
|
+
export const runnerLabelSchema = z
|
|
10
|
+
.string()
|
|
11
|
+
.min(1)
|
|
12
|
+
.max(MAX_RUNNER_LABEL_LENGTH)
|
|
13
|
+
.regex(new RegExp(RUNNER_LABEL_PATTERN.source, 'i'));
|
|
14
|
+
|
|
15
|
+
export const registerRunnerBodySchema = z.object({
|
|
16
|
+
labels: z.array(runnerLabelSchema).min(1).max(MAX_RUNNER_LABELS),
|
|
17
|
+
capabilities: runnerToolCapabilitiesSchema.optional(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const registerRunnerResponseSchema = z.object({
|
|
21
|
+
session_token: z.string().min(1),
|
|
22
|
+
session_id: z.string().uuid(),
|
|
23
|
+
mode: z.enum(['manual', 'ephemeral']),
|
|
24
|
+
max_claims: z.number().int().positive().nullable(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export type RegisterRunnerBodyDto = z.infer<typeof registerRunnerBodySchema>;
|
|
28
|
+
export type RegisterRunnerResponseDto = z.infer<typeof registerRunnerResponseSchema>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MAX_PROVISIONED_RUNNER_REPORT_EVENTS,
|
|
3
|
+
provisionedRunnerReportEventSchema,
|
|
4
|
+
reportProvisionedRunnersBodySchema,
|
|
5
|
+
} from './report-provisioned-runners.js';
|
|
6
|
+
|
|
7
|
+
describe('provisionedRunnerReportEventSchema', () => {
|
|
8
|
+
it('accepts minimal provider-agnostic lifecycle events', () => {
|
|
9
|
+
const result = provisionedRunnerReportEventSchema.safeParse({
|
|
10
|
+
provisioned_runner_id: 'container-1',
|
|
11
|
+
labels: ['linux'],
|
|
12
|
+
state: 'starting',
|
|
13
|
+
reported_at: new Date().toISOString(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
expect(result.success).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('accepts cleanup-confirmed terminated lifecycle events', () => {
|
|
20
|
+
const result = provisionedRunnerReportEventSchema.safeParse({
|
|
21
|
+
provisioned_runner_id: 'container-1',
|
|
22
|
+
labels: ['linux'],
|
|
23
|
+
state: 'terminated',
|
|
24
|
+
reported_at: new Date().toISOString(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
expect(result.success).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('rejects provider-sensitive extra fields', () => {
|
|
31
|
+
const result = provisionedRunnerReportEventSchema.safeParse({
|
|
32
|
+
provisioned_runner_id: 'container-1',
|
|
33
|
+
labels: ['linux'],
|
|
34
|
+
state: 'running',
|
|
35
|
+
reported_at: new Date().toISOString(),
|
|
36
|
+
ip_address: '192.0.2.1',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(result.success).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('reportProvisionedRunnersBodySchema', () => {
|
|
44
|
+
it('enforces the batch size limit', () => {
|
|
45
|
+
const event = {
|
|
46
|
+
provisioned_runner_id: 'container-1',
|
|
47
|
+
labels: ['linux'],
|
|
48
|
+
state: 'running',
|
|
49
|
+
reported_at: new Date().toISOString(),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const result = reportProvisionedRunnersBodySchema.safeParse({
|
|
53
|
+
events: Array.from({length: MAX_PROVISIONED_RUNNER_REPORT_EVENTS + 1}, (_, index) => ({
|
|
54
|
+
...event,
|
|
55
|
+
provisioned_runner_id: `container-${index}`,
|
|
56
|
+
})),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(result.success).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {MAX_RUNNER_LABELS} from '@shipfox/runner-labels';
|
|
2
|
+
import {z} from 'zod';
|
|
3
|
+
import {runnerLabelSchema} from './register.js';
|
|
4
|
+
|
|
5
|
+
export const MAX_PROVISIONED_RUNNER_REPORT_EVENTS = 1000;
|
|
6
|
+
export const MAX_PROVISIONED_RUNNER_REASON_LENGTH = 500;
|
|
7
|
+
export const MAX_PROVIDER_KIND_LENGTH = 64;
|
|
8
|
+
|
|
9
|
+
export const provisionedRunnerStateSchema = z.enum([
|
|
10
|
+
'starting',
|
|
11
|
+
'running',
|
|
12
|
+
'stopping',
|
|
13
|
+
'stopped',
|
|
14
|
+
'failed',
|
|
15
|
+
'terminated',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export const providerKindSchema = z.string().min(1).max(MAX_PROVIDER_KIND_LENGTH);
|
|
19
|
+
|
|
20
|
+
export const provisionedRunnerReportEventSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
provisioned_runner_id: z.string().min(1).max(255),
|
|
23
|
+
reservation_id: z.string().uuid().optional(),
|
|
24
|
+
template_key: z.string().min(1).max(255).optional(),
|
|
25
|
+
labels: z.array(runnerLabelSchema).min(1).max(MAX_RUNNER_LABELS),
|
|
26
|
+
state: provisionedRunnerStateSchema,
|
|
27
|
+
reason: z.string().min(1).max(MAX_PROVISIONED_RUNNER_REASON_LENGTH).optional(),
|
|
28
|
+
runner_session_id: z.string().uuid().optional(),
|
|
29
|
+
reported_at: z.string().datetime(),
|
|
30
|
+
provider_kind: providerKindSchema.optional(),
|
|
31
|
+
})
|
|
32
|
+
.strict();
|
|
33
|
+
|
|
34
|
+
export const reportProvisionedRunnersBodySchema = z.object({
|
|
35
|
+
events: z
|
|
36
|
+
.array(provisionedRunnerReportEventSchema)
|
|
37
|
+
.min(1)
|
|
38
|
+
.max(MAX_PROVISIONED_RUNNER_REPORT_EVENTS),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const reportProvisionedRunnersResponseSchema = z.object({
|
|
42
|
+
accepted: z.number().int().min(0),
|
|
43
|
+
reservations_released: z.number().int().min(0),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const activeRunnerStateSchema = z.enum(['starting', 'running', 'stopping', 'busy']);
|
|
47
|
+
|
|
48
|
+
export const activeRunnerDtoSchema = z.object({
|
|
49
|
+
runner_session_id: z.string().uuid().nullable(),
|
|
50
|
+
provisioned_runner_id: z.string().nullable(),
|
|
51
|
+
provisioner_id: z.string().uuid().nullable(),
|
|
52
|
+
state: activeRunnerStateSchema,
|
|
53
|
+
labels: z.array(z.string()),
|
|
54
|
+
template_key: z.string().nullable(),
|
|
55
|
+
provider_kind: z.string().nullable(),
|
|
56
|
+
job_id: z.string().uuid().nullable(),
|
|
57
|
+
workflow_run_attempt_id: z.string().uuid().nullable(),
|
|
58
|
+
project_id: z.string().uuid().nullable(),
|
|
59
|
+
reported_at: z.string().nullable(),
|
|
60
|
+
last_heartbeat_at: z.string().nullable(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const activeRunnersResponseSchema = z.object({
|
|
64
|
+
runners: z.array(activeRunnerDtoSchema),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export type ProvisionedRunnerStateDto = z.infer<typeof provisionedRunnerStateSchema>;
|
|
68
|
+
export type ProvisionedRunnerReportEventDto = z.infer<typeof provisionedRunnerReportEventSchema>;
|
|
69
|
+
export type ReportProvisionedRunnersBodyDto = z.infer<typeof reportProvisionedRunnersBodySchema>;
|
|
70
|
+
export type ReportProvisionedRunnersResponseDto = z.infer<
|
|
71
|
+
typeof reportProvisionedRunnersResponseSchema
|
|
72
|
+
>;
|
|
73
|
+
export type ActiveRunnerStateDto = z.infer<typeof activeRunnerStateSchema>;
|
|
74
|
+
export type ActiveRunnerDto = z.infer<typeof activeRunnerDtoSchema>;
|
|
75
|
+
export type ActiveRunnersResponseDto = z.infer<typeof activeRunnersResponseSchema>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {runnerToolCapabilitiesSchema} from './tool-capabilities.js';
|
|
2
|
+
|
|
3
|
+
describe('runnerToolCapabilitiesSchema', () => {
|
|
4
|
+
it('accepts a full capability report', () => {
|
|
5
|
+
const result = runnerToolCapabilitiesSchema.safeParse({
|
|
6
|
+
harnesses: {
|
|
7
|
+
pi: {tools: ['read', 'bash', 'web_search']},
|
|
8
|
+
claude: {tools: ['Read', 'Bash', 'WebSearch']},
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(result.success).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('accepts a partial capability report', () => {
|
|
16
|
+
const result = runnerToolCapabilitiesSchema.safeParse({
|
|
17
|
+
harnesses: {
|
|
18
|
+
pi: {tools: ['read']},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(result.success).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('accepts no harness support', () => {
|
|
26
|
+
const result = runnerToolCapabilitiesSchema.safeParse({harnesses: {}});
|
|
27
|
+
|
|
28
|
+
expect(result.success).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('accepts an empty tool array', () => {
|
|
32
|
+
const result = runnerToolCapabilitiesSchema.safeParse({
|
|
33
|
+
harnesses: {
|
|
34
|
+
claude: {tools: []},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(result.success).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('rejects duplicate tool names per harness', () => {
|
|
42
|
+
const result = runnerToolCapabilitiesSchema.safeParse({
|
|
43
|
+
harnesses: {
|
|
44
|
+
pi: {tools: ['read', 'read']},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(result.success).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it.each([
|
|
52
|
+
null,
|
|
53
|
+
{},
|
|
54
|
+
{harnesses: {}, extra: true},
|
|
55
|
+
{harnesses: {pi: {tools: ['read']}, unknown: {tools: ['x']}}},
|
|
56
|
+
{harnesses: {pi: {tools: ['read'], extra: true}}},
|
|
57
|
+
{harnesses: {pi: {tools: ['']}}},
|
|
58
|
+
{harnesses: {pi: {tools: [42]}}},
|
|
59
|
+
])('rejects malformed capability report %#', (value) => {
|
|
60
|
+
const result = runnerToolCapabilitiesSchema.safeParse(value);
|
|
61
|
+
|
|
62
|
+
expect(result.success).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {z} from 'zod';
|
|
2
|
+
|
|
3
|
+
export const runnerHarnessToolCapabilitiesSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
tools: z.array(z.string().min(1)).superRefine((tools, ctx) => {
|
|
6
|
+
const seen = new Set<string>();
|
|
7
|
+
for (const [index, tool] of tools.entries()) {
|
|
8
|
+
if (!seen.has(tool)) {
|
|
9
|
+
seen.add(tool);
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
ctx.addIssue({
|
|
14
|
+
code: z.ZodIssueCode.custom,
|
|
15
|
+
message: 'Duplicate tool name',
|
|
16
|
+
path: [index],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}),
|
|
20
|
+
})
|
|
21
|
+
.strict();
|
|
22
|
+
|
|
23
|
+
export const runnerToolCapabilitiesSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
harnesses: z
|
|
26
|
+
.object({
|
|
27
|
+
pi: runnerHarnessToolCapabilitiesSchema.optional(),
|
|
28
|
+
claude: runnerHarnessToolCapabilitiesSchema.optional(),
|
|
29
|
+
})
|
|
30
|
+
.strict(),
|
|
31
|
+
})
|
|
32
|
+
.strict();
|
|
33
|
+
|
|
34
|
+
export type RunnerHarnessToolCapabilitiesDto = z.infer<typeof runnerHarnessToolCapabilitiesSchema>;
|
|
35
|
+
export type RunnerToolCapabilitiesDto = z.infer<typeof runnerToolCapabilitiesSchema>;
|