Files
gitlab/ts/gitlab.classes.gitlabclient.ts

426 lines
14 KiB
TypeScript

import * as plugins from './gitlab.plugins.js';
import { logger } from './gitlab.logging.js';
import type {
IGitLabUser,
IGitLabProject,
IGitLabGroup,
IGitLabVariable,
IVariableOptions,
IGitLabProtectedBranch,
IGitLabBranch,
IGitLabTag,
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) };
}
}
// ---------------------------------------------------------------------------
// Groups — scoped queries
// ---------------------------------------------------------------------------
/**
* Get a single group by its full path (e.g. "foss.global" or "foss.global/push.rocks")
*/
public async getGroupByPath(fullPath: string): Promise<IGitLabGroup> {
return this.request<IGitLabGroup>(
'GET',
`/api/v4/groups/${encodeURIComponent(fullPath)}`,
);
}
/**
* List projects within a group (includes subgroups when include_subgroups=true)
*/
public async getGroupProjects(groupId: number | string, opts?: IListOptions): Promise<IGitLabProject[]> {
const page = opts?.page || 1;
const perPage = opts?.perPage || 50;
let url = `/api/v4/groups/${encodeURIComponent(groupId)}/projects?include_subgroups=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);
}
/**
* List all descendant groups (recursive subgroups) within a group
*/
public async getDescendantGroups(groupId: number | string, opts?: IListOptions): Promise<IGitLabGroup[]> {
const page = opts?.page || 1;
const perPage = opts?.perPage || 50;
let url = `/api/v4/groups/${encodeURIComponent(groupId)}/descendant_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);
}
/**
* Create a new group. Optionally nested under a parent group.
*/
public async createGroup(name: string, path: string, parentId?: number): Promise<IGitLabGroup> {
const body: any = { name, path, visibility: 'private' };
if (parentId) body.parent_id = parentId;
return this.request<IGitLabGroup>('POST', '/api/v4/groups', body);
}
/**
* Create a new project (repository).
*/
public async createProject(name: string, opts?: {
path?: string;
namespaceId?: number;
visibility?: string;
description?: string;
}): Promise<IGitLabProject> {
return this.request<IGitLabProject>('POST', '/api/v4/projects', {
name,
path: opts?.path || name,
namespace_id: opts?.namespaceId,
visibility: opts?.visibility || 'private',
description: opts?.description || '',
});
}
// ---------------------------------------------------------------------------
// 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`,
);
}
// ---------------------------------------------------------------------------
// Repository Branches & Tags
// ---------------------------------------------------------------------------
public async getRepoBranches(projectId: number | string, opts?: IListOptions): Promise<IGitLabBranch[]> {
const page = opts?.page || 1;
const perPage = opts?.perPage || 50;
return this.request<IGitLabBranch[]>(
'GET',
`/api/v4/projects/${encodeURIComponent(projectId)}/repository/branches?page=${page}&per_page=${perPage}`,
);
}
public async getRepoTags(projectId: number | string, opts?: IListOptions): Promise<IGitLabTag[]> {
const page = opts?.page || 1;
const perPage = opts?.perPage || 50;
return this.request<IGitLabTag[]>(
'GET',
`/api/v4/projects/${encodeURIComponent(projectId)}/repository/tags?page=${page}&per_page=${perPage}`,
);
}
// ---------------------------------------------------------------------------
// Protected Branches
// ---------------------------------------------------------------------------
public async getProtectedBranches(projectId: number | string): Promise<IGitLabProtectedBranch[]> {
return this.request<IGitLabProtectedBranch[]>(
'GET',
`/api/v4/projects/${encodeURIComponent(projectId)}/protected_branches`,
);
}
public async unprotectBranch(projectId: number | string, branchName: string): Promise<void> {
await this.request(
'DELETE',
`/api/v4/projects/${encodeURIComponent(projectId)}/protected_branches/${encodeURIComponent(branchName)}`,
);
}
// ---------------------------------------------------------------------------
// Project Deletion
// ---------------------------------------------------------------------------
public async deleteProject(projectId: number | string): Promise<void> {
await this.request(
'DELETE',
`/api/v4/projects/${encodeURIComponent(projectId)}`,
);
}
}