All files / src/entities/runners schema.ts

100% Statements 18/18
100% Branches 0/0
100% Functions 0/0
100% Lines 18/18

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 11717x 17x                       17x 17x     17x         17x       17x 17x 17x       17x             17x                       17x                                   17x             17x           17x           17x           17x               17x                            
import { z } from 'zod';
import { requiredId, flexibleBoolean } from '../utils';
 
// ============================================================================
// manage_runner - CQRS Command Tool (discriminated union schema)
// Actions: create_authentication_token, update, pause, resume, delete,
//          reset_authentication_token
// Backed by the GitLab GraphQL runner mutations (runnerCreate / runnerUpdate /
// runnerDelete). pause/resume map to runnerUpdate(paused). Per-runner token reset
// has no GraphQL mutation, so reset_authentication_token uses the REST endpoint.
// Gated behind USE_RUNNERS. Writes require the create_runner / manage_runner scope.
// ============================================================================
 
const groupPathField = requiredId.describe("Group full path for a group runner (e.g., 'my-group')");
const projectPathField = requiredId.describe(
  "Project full path for a project runner (e.g., 'my-group/my-project')",
);
const runnerIdField = z.coerce
  .number()
  .int()
  .positive()
  .describe('Numeric ID of the runner (from browse_runners); expanded to a global ID internally');
const accessLevelField = z
  .enum(['NOT_PROTECTED', 'REF_PROTECTED'])
  .optional()
  .describe('Access level: NOT_PROTECTED or REF_PROTECTED (protected refs only)');
const tagListField = z.array(z.string()).optional().describe('Tags that determine which jobs run');
const descriptionField = z.string().optional().describe('Runner description');
const maintenanceNoteField = z
  .string()
  .optional()
  .describe('Free-form maintenance note (Markdown)');
const maximumTimeoutField = z.coerce
  .number()
  .int()
  .optional()
  .describe('Maximum job timeout in seconds');
 
// Settings shared by create and update.
const runnerSettings = {
  description: descriptionField,
  paused: flexibleBoolean.optional().describe('Whether the runner is paused (ignores new jobs)'),
  locked: flexibleBoolean.optional().describe('Lock the runner to its current projects'),
  run_untagged: flexibleBoolean.optional().describe('Allow running untagged jobs'),
  tag_list: tagListField,
  access_level: accessLevelField,
  maximum_timeout: maximumTimeoutField,
  maintenance_note: maintenanceNoteField,
};
 
// --- Action: create_authentication_token (GitLab 16+ runner registration) ---
const CreateRunnerSchema = z.object({
  action: z
    .literal('create_authentication_token')
    .describe(
      'Create a new runner and return its one-time authentication token (GitLab 16+ flow). ' +
        'For GROUP_TYPE pass group_id; for PROJECT_TYPE pass project_id.',
    ),
  runner_type: z
    .enum(['INSTANCE_TYPE', 'GROUP_TYPE', 'PROJECT_TYPE'])
    .describe(
      'Runner scope. INSTANCE_TYPE needs admin; GROUP_TYPE/PROJECT_TYPE need the namespace',
    ),
  group_id: groupPathField.optional(),
  project_id: projectPathField.optional(),
  ...runnerSettings,
});
 
// --- Action: update ---
const UpdateRunnerSchema = z.object({
  action: z.literal('update').describe("Update a runner's settings"),
  runner_id: runnerIdField,
  ...runnerSettings,
});
 
// --- Action: pause ---
const PauseRunnerSchema = z.object({
  action: z.literal('pause').describe('Pause a runner (stops it picking up new jobs)'),
  runner_id: runnerIdField,
});
 
// --- Action: resume ---
const ResumeRunnerSchema = z.object({
  action: z.literal('resume').describe('Resume a paused runner'),
  runner_id: runnerIdField,
});
 
// --- Action: delete ---
const DeleteRunnerSchema = z.object({
  action: z.literal('delete').describe('Delete a runner permanently'),
  runner_id: runnerIdField,
});
 
// --- Action: reset_authentication_token ---
const ResetTokenSchema = z.object({
  action: z
    .literal('reset_authentication_token')
    .describe('Rotate the runner authentication token, returning the new value'),
  runner_id: runnerIdField,
});
 
// --- Discriminated union combining all actions ---
export const ManageRunnerSchema = z.discriminatedUnion('action', [
  CreateRunnerSchema,
  UpdateRunnerSchema,
  PauseRunnerSchema,
  ResumeRunnerSchema,
  DeleteRunnerSchema,
  ResetTokenSchema,
]);
 
// ============================================================================
// Type exports
// ============================================================================
 
export type ManageRunnerInput = z.infer<typeof ManageRunnerSchema>;