import * as plugins from './gitlab.plugins.js'; import { logger } from './gitlab.logging.js'; import type { IGitLabUser, IGitLabProject, IGitLabGroup, IGitLabVariable, IVariableOptions, IGitLabPipeline, IGitLabJob, ITestConnectionResult, IListOptions, } from './gitlab.interfaces.js'; export class GitLabClient { private baseUrl: string; private token: string; constructor(baseUrl: string, token: string) { // Remove trailing slash if present this.baseUrl = baseUrl.replace(/\/+$/, ''); this.token = token; } // --------------------------------------------------------------------------- // HTTP helpers // --------------------------------------------------------------------------- private async request( method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, data?: any, customHeaders?: Record, ): Promise { const url = `${this.baseUrl}${path}`; let builder = plugins.smartrequest.SmartRequest.create() .url(url) .header('PRIVATE-TOKEN', this.token) .header('Content-Type', 'application/json'); if (customHeaders) { for (const [k, v] of Object.entries(customHeaders)) { builder = builder.header(k, v); } } if (data) { builder = builder.json(data); } let response: Awaited>; switch (method) { case 'GET': response = await builder.get(); break; case 'POST': response = await builder.post(); break; case 'PUT': response = await builder.put(); break; case 'DELETE': response = await builder.delete(); break; } if (!response.ok) { const errorText = await response.text(); throw new Error(`${method} ${path}: ${response.status} ${response.statusText} - ${errorText}`); } try { return await response.json() as T; } catch { return undefined as unknown as T; } } private async requestText( method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, ): Promise { const url = `${this.baseUrl}${path}`; let builder = plugins.smartrequest.SmartRequest.create() .url(url) .header('PRIVATE-TOKEN', this.token) .header('Accept', 'text/plain'); let response: Awaited>; switch (method) { case 'GET': response = await builder.get(); break; case 'POST': response = await builder.post(); break; case 'PUT': response = await builder.put(); break; case 'DELETE': response = await builder.delete(); break; } if (!response.ok) { const errorText = await response.text(); throw new Error(`${method} ${path}: ${response.status} ${response.statusText} - ${errorText}`); } return response.text(); } // --------------------------------------------------------------------------- // Connection // --------------------------------------------------------------------------- public async testConnection(): Promise { try { await this.request('GET', '/api/v4/user'); return { ok: true }; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : String(err) }; } } // --------------------------------------------------------------------------- // Projects // --------------------------------------------------------------------------- public async getProjects(opts?: IListOptions): Promise { const page = opts?.page || 1; const perPage = opts?.perPage || 50; let url = `/api/v4/projects?membership=true&order_by=updated_at&sort=desc&page=${page}&per_page=${perPage}`; if (opts?.search) { url += `&search=${encodeURIComponent(opts.search)}`; } return this.request('GET', url); } // --------------------------------------------------------------------------- // Groups // --------------------------------------------------------------------------- public async getGroups(opts?: IListOptions): Promise { const page = opts?.page || 1; const perPage = opts?.perPage || 50; let url = `/api/v4/groups?order_by=name&sort=asc&page=${page}&per_page=${perPage}`; if (opts?.search) { url += `&search=${encodeURIComponent(opts.search)}`; } return this.request('GET', url); } // --------------------------------------------------------------------------- // Project Variables (CI/CD) // --------------------------------------------------------------------------- public async getProjectVariables(projectId: number | string): Promise { return this.request( 'GET', `/api/v4/projects/${encodeURIComponent(projectId)}/variables`, ); } public async createProjectVariable( projectId: number | string, key: string, value: string, opts?: IVariableOptions, ): Promise { return this.request( 'POST', `/api/v4/projects/${encodeURIComponent(projectId)}/variables`, { key, value, protected: opts?.protected ?? false, masked: opts?.masked ?? false, environment_scope: opts?.environment_scope ?? '*', }, ); } public async updateProjectVariable( projectId: number | string, key: string, value: string, opts?: IVariableOptions, ): Promise { const body: any = { value }; if (opts?.protected !== undefined) body.protected = opts.protected; if (opts?.masked !== undefined) body.masked = opts.masked; if (opts?.environment_scope !== undefined) body.environment_scope = opts.environment_scope; return this.request( 'PUT', `/api/v4/projects/${encodeURIComponent(projectId)}/variables/${encodeURIComponent(key)}`, body, ); } public async deleteProjectVariable(projectId: number | string, key: string): Promise { await this.request( 'DELETE', `/api/v4/projects/${encodeURIComponent(projectId)}/variables/${encodeURIComponent(key)}`, ); } // --------------------------------------------------------------------------- // Group Variables (CI/CD) // --------------------------------------------------------------------------- public async getGroupVariables(groupId: number | string): Promise { return this.request( 'GET', `/api/v4/groups/${encodeURIComponent(groupId)}/variables`, ); } public async createGroupVariable( groupId: number | string, key: string, value: string, opts?: IVariableOptions, ): Promise { return this.request( 'POST', `/api/v4/groups/${encodeURIComponent(groupId)}/variables`, { key, value, protected: opts?.protected ?? false, masked: opts?.masked ?? false, environment_scope: opts?.environment_scope ?? '*', }, ); } public async updateGroupVariable( groupId: number | string, key: string, value: string, opts?: IVariableOptions, ): Promise { const body: any = { value }; if (opts?.protected !== undefined) body.protected = opts.protected; if (opts?.masked !== undefined) body.masked = opts.masked; if (opts?.environment_scope !== undefined) body.environment_scope = opts.environment_scope; return this.request( 'PUT', `/api/v4/groups/${encodeURIComponent(groupId)}/variables/${encodeURIComponent(key)}`, body, ); } public async deleteGroupVariable(groupId: number | string, key: string): Promise { await this.request( 'DELETE', `/api/v4/groups/${encodeURIComponent(groupId)}/variables/${encodeURIComponent(key)}`, ); } // --------------------------------------------------------------------------- // Pipelines // --------------------------------------------------------------------------- public async getPipelines(projectId: number | string, opts?: IListOptions): Promise { const page = opts?.page || 1; const perPage = opts?.perPage || 30; return this.request( 'GET', `/api/v4/projects/${encodeURIComponent(projectId)}/pipelines?page=${page}&per_page=${perPage}&order_by=updated_at&sort=desc`, ); } public async getPipelineJobs(projectId: number | string, pipelineId: number): Promise { return this.request( 'GET', `/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/jobs`, ); } public async getJobLog(projectId: number | string, jobId: number): Promise { return this.requestText( 'GET', `/api/v4/projects/${encodeURIComponent(projectId)}/jobs/${jobId}/trace`, ); } public async retryPipeline(projectId: number | string, pipelineId: number): Promise { await this.request( 'POST', `/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/retry`, ); } public async cancelPipeline(projectId: number | string, pipelineId: number): Promise { await this.request( 'POST', `/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/cancel`, ); } }