Replaces legacy @mojoio/gitlab with modern ESM TypeScript client supporting projects, groups, CI/CD variables, and pipelines.
305 lines
9.3 KiB
TypeScript
305 lines
9.3 KiB
TypeScript
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<T = any>(
|
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
|
path: string,
|
|
data?: any,
|
|
customHeaders?: Record<string, string>,
|
|
): Promise<T> {
|
|
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<ReturnType<typeof builder.get>>;
|
|
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<string> {
|
|
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<ReturnType<typeof builder.get>>;
|
|
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<ITestConnectionResult> {
|
|
try {
|
|
await this.request<IGitLabUser>('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<IGitLabProject[]> {
|
|
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<IGitLabProject[]>('GET', url);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Groups
|
|
// ---------------------------------------------------------------------------
|
|
|
|
public async getGroups(opts?: IListOptions): Promise<IGitLabGroup[]> {
|
|
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<IGitLabGroup[]>('GET', url);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Project Variables (CI/CD)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
public async getProjectVariables(projectId: number | string): Promise<IGitLabVariable[]> {
|
|
return this.request<IGitLabVariable[]>(
|
|
'GET',
|
|
`/api/v4/projects/${encodeURIComponent(projectId)}/variables`,
|
|
);
|
|
}
|
|
|
|
public async createProjectVariable(
|
|
projectId: number | string,
|
|
key: string,
|
|
value: string,
|
|
opts?: IVariableOptions,
|
|
): Promise<IGitLabVariable> {
|
|
return this.request<IGitLabVariable>(
|
|
'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<IGitLabVariable> {
|
|
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<IGitLabVariable>(
|
|
'PUT',
|
|
`/api/v4/projects/${encodeURIComponent(projectId)}/variables/${encodeURIComponent(key)}`,
|
|
body,
|
|
);
|
|
}
|
|
|
|
public async deleteProjectVariable(projectId: number | string, key: string): Promise<void> {
|
|
await this.request(
|
|
'DELETE',
|
|
`/api/v4/projects/${encodeURIComponent(projectId)}/variables/${encodeURIComponent(key)}`,
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Group Variables (CI/CD)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
public async getGroupVariables(groupId: number | string): Promise<IGitLabVariable[]> {
|
|
return this.request<IGitLabVariable[]>(
|
|
'GET',
|
|
`/api/v4/groups/${encodeURIComponent(groupId)}/variables`,
|
|
);
|
|
}
|
|
|
|
public async createGroupVariable(
|
|
groupId: number | string,
|
|
key: string,
|
|
value: string,
|
|
opts?: IVariableOptions,
|
|
): Promise<IGitLabVariable> {
|
|
return this.request<IGitLabVariable>(
|
|
'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<IGitLabVariable> {
|
|
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<IGitLabVariable>(
|
|
'PUT',
|
|
`/api/v4/groups/${encodeURIComponent(groupId)}/variables/${encodeURIComponent(key)}`,
|
|
body,
|
|
);
|
|
}
|
|
|
|
public async deleteGroupVariable(groupId: number | string, key: string): Promise<void> {
|
|
await this.request(
|
|
'DELETE',
|
|
`/api/v4/groups/${encodeURIComponent(groupId)}/variables/${encodeURIComponent(key)}`,
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Pipelines
|
|
// ---------------------------------------------------------------------------
|
|
|
|
public async getPipelines(projectId: number | string, opts?: IListOptions): Promise<IGitLabPipeline[]> {
|
|
const page = opts?.page || 1;
|
|
const perPage = opts?.perPage || 30;
|
|
return this.request<IGitLabPipeline[]>(
|
|
'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<IGitLabJob[]> {
|
|
return this.request<IGitLabJob[]>(
|
|
'GET',
|
|
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/jobs`,
|
|
);
|
|
}
|
|
|
|
public async getJobLog(projectId: number | string, jobId: number): Promise<string> {
|
|
return this.requestText(
|
|
'GET',
|
|
`/api/v4/projects/${encodeURIComponent(projectId)}/jobs/${jobId}/trace`,
|
|
);
|
|
}
|
|
|
|
public async retryPipeline(projectId: number | string, pipelineId: number): Promise<void> {
|
|
await this.request(
|
|
'POST',
|
|
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/retry`,
|
|
);
|
|
}
|
|
|
|
public async cancelPipeline(projectId: number | string, pipelineId: number): Promise<void> {
|
|
await this.request(
|
|
'POST',
|
|
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/cancel`,
|
|
);
|
|
}
|
|
}
|