feat(client): add rich domain classes, helpers, and refactor GitLabClient internals
This commit is contained in:
18
ts/gitlab.classes.branch.ts
Normal file
18
ts/gitlab.classes.branch.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { IGitLabBranch } from './gitlab.interfaces.js';
|
||||
|
||||
export class GitLabBranch {
|
||||
public readonly name: string;
|
||||
public readonly commitSha: string;
|
||||
|
||||
constructor(raw: IGitLabBranch) {
|
||||
this.name = raw.name || '';
|
||||
this.commitSha = raw.commit?.id || '';
|
||||
}
|
||||
|
||||
toJSON(): IGitLabBranch {
|
||||
return {
|
||||
name: this.name,
|
||||
commit: { id: this.commitSha },
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as plugins from './gitlab.plugins.js';
|
||||
import { logger } from './gitlab.logging.js';
|
||||
import type {
|
||||
IGitLabUser,
|
||||
IGitLabProject,
|
||||
@@ -18,22 +17,26 @@ import type {
|
||||
IPipelineListOptions,
|
||||
IJobListOptions,
|
||||
} from './gitlab.interfaces.js';
|
||||
import { GitLabGroup } from './gitlab.classes.group.js';
|
||||
import { GitLabProject } from './gitlab.classes.project.js';
|
||||
import { GitLabPipeline } from './gitlab.classes.pipeline.js';
|
||||
import { autoPaginate } from './gitlab.helpers.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
|
||||
// ---------------------------------------------------------------------------
|
||||
// ===========================================================================
|
||||
// HTTP helpers (internal)
|
||||
// ===========================================================================
|
||||
|
||||
private async request<T = any>(
|
||||
/** @internal */
|
||||
async request<T = any>(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||
path: string,
|
||||
data?: any,
|
||||
@@ -84,7 +87,8 @@ export class GitLabClient {
|
||||
}
|
||||
}
|
||||
|
||||
private async requestText(
|
||||
/** @internal */
|
||||
async requestText(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||
path: string,
|
||||
): Promise<string> {
|
||||
@@ -119,9 +123,45 @@ export class GitLabClient {
|
||||
return response.text();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Connection
|
||||
// ---------------------------------------------------------------------------
|
||||
/** @internal — multipart form upload (for avatars) */
|
||||
async requestMultipart<T = any>(
|
||||
method: 'PUT' | 'POST',
|
||||
path: string,
|
||||
formData: FormData,
|
||||
): Promise<T> {
|
||||
const url = `${this.baseUrl}${path}`;
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: { 'PRIVATE-TOKEN': this.token },
|
||||
body: formData,
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal — fetch binary data (e.g. avatar images) */
|
||||
async requestBinary(url: string): Promise<Uint8Array> {
|
||||
const fullUrl = url.startsWith('http') ? url : `${this.baseUrl}${url}`;
|
||||
const response = await fetch(fullUrl, {
|
||||
headers: { 'PRIVATE-TOKEN': this.token },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`GET ${url}: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const buf = await response.arrayBuffer();
|
||||
return new Uint8Array(buf);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Public API — Connection
|
||||
// ===========================================================================
|
||||
|
||||
public async testConnection(): Promise<ITestConnectionResult> {
|
||||
try {
|
||||
@@ -132,24 +172,95 @@ export class GitLabClient {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Groups — scoped queries
|
||||
// ---------------------------------------------------------------------------
|
||||
// ===========================================================================
|
||||
// Public API — Groups (returns rich objects)
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Get a single group by its full path (e.g. "foss.global" or "foss.global/push.rocks")
|
||||
* Get all groups (auto-paginated).
|
||||
*/
|
||||
public async getGroupByPath(fullPath: string): Promise<IGitLabGroup> {
|
||||
return this.request<IGitLabGroup>(
|
||||
'GET',
|
||||
`/api/v4/groups/${encodeURIComponent(fullPath)}`,
|
||||
);
|
||||
public async getGroups(opts?: IListOptions): Promise<GitLabGroup[]> {
|
||||
return autoPaginate(
|
||||
(page, perPage) => this.requestGetGroups({ ...opts, page, perPage }),
|
||||
opts,
|
||||
).then(groups => groups.map(g => new GitLabGroup(this, g)));
|
||||
}
|
||||
|
||||
/**
|
||||
* List projects within a group (includes subgroups when include_subgroups=true)
|
||||
* Get a single group by full path.
|
||||
*/
|
||||
public async getGroupProjects(groupId: number | string, opts?: IListOptions): Promise<IGitLabProject[]> {
|
||||
public async getGroup(fullPath: string): Promise<GitLabGroup> {
|
||||
const raw = await this.requestGetGroupByPath(fullPath);
|
||||
return new GitLabGroup(this, raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new group.
|
||||
*/
|
||||
public async createGroup(name: string, path: string, parentId?: number): Promise<GitLabGroup> {
|
||||
const raw = await this.requestCreateGroup(name, path, parentId);
|
||||
return new GitLabGroup(this, raw);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Public API — Projects (returns rich objects)
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Get all projects (auto-paginated, membership=true).
|
||||
*/
|
||||
public async getProjects(opts?: IListOptions): Promise<GitLabProject[]> {
|
||||
return autoPaginate(
|
||||
(page, perPage) => this.requestGetProjects({ ...opts, page, perPage }),
|
||||
opts,
|
||||
).then(projects => projects.map(p => new GitLabProject(this, p)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single project by ID or path.
|
||||
*/
|
||||
public async getProject(idOrPath: number | string): Promise<GitLabProject> {
|
||||
const raw = await this.requestGetProject(idOrPath);
|
||||
return new GitLabProject(this, raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new project.
|
||||
*/
|
||||
public async createProject(name: string, opts?: {
|
||||
path?: string;
|
||||
namespaceId?: number;
|
||||
visibility?: string;
|
||||
description?: string;
|
||||
}): Promise<GitLabProject> {
|
||||
const raw = await this.requestCreateProject(name, opts);
|
||||
return new GitLabProject(this, raw);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Internal request methods — called by domain classes
|
||||
// ===========================================================================
|
||||
|
||||
// --- Groups ---
|
||||
|
||||
/** @internal */
|
||||
async requestGetGroups(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);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestGetGroupByPath(fullPath: string): Promise<IGitLabGroup> {
|
||||
return this.request<IGitLabGroup>('GET', `/api/v4/groups/${encodeURIComponent(fullPath)}`);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestGetGroupProjects(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}`;
|
||||
@@ -159,10 +270,8 @@ export class GitLabClient {
|
||||
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[]> {
|
||||
/** @internal */
|
||||
async requestGetDescendantGroups(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}`;
|
||||
@@ -172,19 +281,58 @@ export class GitLabClient {
|
||||
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> {
|
||||
/** @internal */
|
||||
async requestCreateGroup(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?: {
|
||||
/** @internal */
|
||||
async requestUpdateGroup(groupId: number | string, data: Record<string, any>): Promise<void> {
|
||||
await this.request('PUT', `/api/v4/groups/${encodeURIComponent(groupId)}`, data);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestSetGroupAvatar(groupId: number | string, imageData: Uint8Array, filename: string): Promise<void> {
|
||||
const blob = new Blob([imageData.buffer as ArrayBuffer]);
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', blob, filename);
|
||||
await this.requestMultipart('PUT', `/api/v4/groups/${encodeURIComponent(groupId)}`, formData);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestTransferGroup(groupId: number | string, parentGroupId: number): Promise<void> {
|
||||
await this.request('POST', `/api/v4/groups/${encodeURIComponent(groupId)}/transfer`, {
|
||||
group_id: parentGroupId,
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestDeleteGroup(groupId: number | string): Promise<void> {
|
||||
await this.request('DELETE', `/api/v4/groups/${encodeURIComponent(groupId)}`);
|
||||
}
|
||||
|
||||
// --- Projects ---
|
||||
|
||||
/** @internal */
|
||||
async requestGetProjects(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);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestGetProject(idOrPath: number | string): Promise<IGitLabProject> {
|
||||
return this.request<IGitLabProject>('GET', `/api/v4/projects/${encodeURIComponent(idOrPath)}`);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestCreateProject(name: string, opts?: {
|
||||
path?: string;
|
||||
namespaceId?: number;
|
||||
visibility?: string;
|
||||
@@ -199,46 +347,83 @@ export class GitLabClient {
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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);
|
||||
/** @internal */
|
||||
async requestUpdateProject(projectId: number | string, data: Record<string, any>): Promise<void> {
|
||||
await this.request('PUT', `/api/v4/projects/${encodeURIComponent(projectId)}`, data);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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);
|
||||
/** @internal */
|
||||
async requestSetProjectAvatar(projectId: number | string, imageData: Uint8Array, filename: string): Promise<void> {
|
||||
const blob = new Blob([imageData.buffer as ArrayBuffer]);
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', blob, filename);
|
||||
await this.requestMultipart('PUT', `/api/v4/projects/${encodeURIComponent(projectId)}`, formData);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Project Variables (CI/CD)
|
||||
// ---------------------------------------------------------------------------
|
||||
/** @internal */
|
||||
async requestTransferProject(projectId: number | string, namespaceId: number): Promise<void> {
|
||||
await this.request('PUT', `/api/v4/projects/${encodeURIComponent(projectId)}/transfer`, {
|
||||
namespace: namespaceId,
|
||||
});
|
||||
}
|
||||
|
||||
public async getProjectVariables(projectId: number | string): Promise<IGitLabVariable[]> {
|
||||
/** @internal */
|
||||
async requestDeleteProject(projectId: number | string): Promise<void> {
|
||||
await this.request('DELETE', `/api/v4/projects/${encodeURIComponent(projectId)}`);
|
||||
}
|
||||
|
||||
// --- Repo Branches & Tags ---
|
||||
|
||||
/** @internal */
|
||||
async requestGetRepoBranches(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}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestGetRepoTags(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 ---
|
||||
|
||||
/** @internal */
|
||||
async requestGetProtectedBranches(projectId: number | string): Promise<IGitLabProtectedBranch[]> {
|
||||
return this.request<IGitLabProtectedBranch[]>(
|
||||
'GET',
|
||||
`/api/v4/projects/${encodeURIComponent(projectId)}/protected_branches`,
|
||||
);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestUnprotectBranch(projectId: number | string, branchName: string): Promise<void> {
|
||||
await this.request(
|
||||
'DELETE',
|
||||
`/api/v4/projects/${encodeURIComponent(projectId)}/protected_branches/${encodeURIComponent(branchName)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// --- Project Variables ---
|
||||
|
||||
/** @internal */
|
||||
async requestGetProjectVariables(projectId: number | string): Promise<IGitLabVariable[]> {
|
||||
return this.request<IGitLabVariable[]>(
|
||||
'GET',
|
||||
`/api/v4/projects/${encodeURIComponent(projectId)}/variables`,
|
||||
);
|
||||
}
|
||||
|
||||
public async createProjectVariable(
|
||||
/** @internal */
|
||||
async requestCreateProjectVariable(
|
||||
projectId: number | string,
|
||||
key: string,
|
||||
value: string,
|
||||
@@ -257,7 +442,8 @@ export class GitLabClient {
|
||||
);
|
||||
}
|
||||
|
||||
public async updateProjectVariable(
|
||||
/** @internal */
|
||||
async requestUpdateProjectVariable(
|
||||
projectId: number | string,
|
||||
key: string,
|
||||
value: string,
|
||||
@@ -274,25 +460,26 @@ export class GitLabClient {
|
||||
);
|
||||
}
|
||||
|
||||
public async deleteProjectVariable(projectId: number | string, key: string): Promise<void> {
|
||||
/** @internal */
|
||||
async requestDeleteProjectVariable(projectId: number | string, key: string): Promise<void> {
|
||||
await this.request(
|
||||
'DELETE',
|
||||
`/api/v4/projects/${encodeURIComponent(projectId)}/variables/${encodeURIComponent(key)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Group Variables (CI/CD)
|
||||
// ---------------------------------------------------------------------------
|
||||
// --- Group Variables ---
|
||||
|
||||
public async getGroupVariables(groupId: number | string): Promise<IGitLabVariable[]> {
|
||||
/** @internal */
|
||||
async requestGetGroupVariables(groupId: number | string): Promise<IGitLabVariable[]> {
|
||||
return this.request<IGitLabVariable[]>(
|
||||
'GET',
|
||||
`/api/v4/groups/${encodeURIComponent(groupId)}/variables`,
|
||||
);
|
||||
}
|
||||
|
||||
public async createGroupVariable(
|
||||
/** @internal */
|
||||
async requestCreateGroupVariable(
|
||||
groupId: number | string,
|
||||
key: string,
|
||||
value: string,
|
||||
@@ -311,7 +498,8 @@ export class GitLabClient {
|
||||
);
|
||||
}
|
||||
|
||||
public async updateGroupVariable(
|
||||
/** @internal */
|
||||
async requestUpdateGroupVariable(
|
||||
groupId: number | string,
|
||||
key: string,
|
||||
value: string,
|
||||
@@ -328,22 +516,18 @@ export class GitLabClient {
|
||||
);
|
||||
}
|
||||
|
||||
public async deleteGroupVariable(groupId: number | string, key: string): Promise<void> {
|
||||
/** @internal */
|
||||
async requestDeleteGroupVariable(groupId: number | string, key: string): Promise<void> {
|
||||
await this.request(
|
||||
'DELETE',
|
||||
`/api/v4/groups/${encodeURIComponent(groupId)}/variables/${encodeURIComponent(key)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pipelines
|
||||
// ---------------------------------------------------------------------------
|
||||
// --- Pipelines ---
|
||||
|
||||
/**
|
||||
* List pipelines for a project with optional filters.
|
||||
* Supports status, ref, source, scope, username, date range, ordering.
|
||||
*/
|
||||
public async getPipelines(projectId: number | string, opts?: IPipelineListOptions): Promise<IGitLabPipeline[]> {
|
||||
/** @internal */
|
||||
async requestGetPipelines(projectId: number | string, opts?: IPipelineListOptions): Promise<IGitLabPipeline[]> {
|
||||
const page = opts?.page || 1;
|
||||
const perPage = opts?.perPage || 30;
|
||||
const orderBy = opts?.orderBy || 'updated_at';
|
||||
@@ -359,20 +543,16 @@ export class GitLabClient {
|
||||
return this.request<IGitLabPipeline[]>('GET', url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single pipeline's full details.
|
||||
*/
|
||||
public async getPipeline(projectId: number | string, pipelineId: number): Promise<IGitLabPipeline> {
|
||||
/** @internal */
|
||||
async requestGetPipeline(projectId: number | string, pipelineId: number): Promise<IGitLabPipeline> {
|
||||
return this.request<IGitLabPipeline>(
|
||||
'GET',
|
||||
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a new pipeline on the given ref, optionally with variables.
|
||||
*/
|
||||
public async triggerPipeline(
|
||||
/** @internal */
|
||||
async requestTriggerPipeline(
|
||||
projectId: number | string,
|
||||
ref: string,
|
||||
variables?: { key: string; value: string; variable_type?: string }[],
|
||||
@@ -388,58 +568,50 @@ export class GitLabClient {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a pipeline and all its jobs.
|
||||
*/
|
||||
public async deletePipeline(projectId: number | string, pipelineId: number): Promise<void> {
|
||||
await this.request(
|
||||
'DELETE',
|
||||
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get variables used in a specific pipeline run.
|
||||
*/
|
||||
public async getPipelineVariables(projectId: number | string, pipelineId: number): Promise<IGitLabPipelineVariable[]> {
|
||||
return this.request<IGitLabPipelineVariable[]>(
|
||||
'GET',
|
||||
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/variables`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the test report for a pipeline.
|
||||
*/
|
||||
public async getPipelineTestReport(projectId: number | string, pipelineId: number): Promise<IGitLabTestReport> {
|
||||
return this.request<IGitLabTestReport>(
|
||||
'GET',
|
||||
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/test_report`,
|
||||
);
|
||||
}
|
||||
|
||||
public async retryPipeline(projectId: number | string, pipelineId: number): Promise<IGitLabPipeline> {
|
||||
/** @internal */
|
||||
async requestRetryPipeline(projectId: number | string, pipelineId: number): Promise<IGitLabPipeline> {
|
||||
return this.request<IGitLabPipeline>(
|
||||
'POST',
|
||||
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/retry`,
|
||||
);
|
||||
}
|
||||
|
||||
public async cancelPipeline(projectId: number | string, pipelineId: number): Promise<IGitLabPipeline> {
|
||||
/** @internal */
|
||||
async requestCancelPipeline(projectId: number | string, pipelineId: number): Promise<IGitLabPipeline> {
|
||||
return this.request<IGitLabPipeline>(
|
||||
'POST',
|
||||
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/cancel`,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Jobs
|
||||
// ---------------------------------------------------------------------------
|
||||
/** @internal */
|
||||
async requestDeletePipeline(projectId: number | string, pipelineId: number): Promise<void> {
|
||||
await this.request(
|
||||
'DELETE',
|
||||
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* List jobs for a pipeline with optional scope filter and pagination.
|
||||
*/
|
||||
public async getPipelineJobs(projectId: number | string, pipelineId: number, opts?: IJobListOptions): Promise<IGitLabJob[]> {
|
||||
/** @internal */
|
||||
async requestGetPipelineVariables(projectId: number | string, pipelineId: number): Promise<IGitLabPipelineVariable[]> {
|
||||
return this.request<IGitLabPipelineVariable[]>(
|
||||
'GET',
|
||||
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/variables`,
|
||||
);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestGetPipelineTestReport(projectId: number | string, pipelineId: number): Promise<IGitLabTestReport> {
|
||||
return this.request<IGitLabTestReport>(
|
||||
'GET',
|
||||
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/test_report`,
|
||||
);
|
||||
}
|
||||
|
||||
// --- Jobs ---
|
||||
|
||||
/** @internal */
|
||||
async requestGetPipelineJobs(projectId: number | string, pipelineId: number, opts?: IJobListOptions): Promise<IGitLabJob[]> {
|
||||
const page = opts?.page || 1;
|
||||
const perPage = opts?.perPage || 100;
|
||||
let url = `/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/jobs?page=${page}&per_page=${perPage}`;
|
||||
@@ -451,114 +623,43 @@ export class GitLabClient {
|
||||
return this.request<IGitLabJob[]>('GET', url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single job's full details.
|
||||
*/
|
||||
public async getJob(projectId: number | string, jobId: number): Promise<IGitLabJob> {
|
||||
return this.request<IGitLabJob>(
|
||||
'GET',
|
||||
`/api/v4/projects/${encodeURIComponent(projectId)}/jobs/${jobId}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a job's raw log (trace) output.
|
||||
*/
|
||||
public async getJobLog(projectId: number | string, jobId: number): Promise<string> {
|
||||
/** @internal */
|
||||
async requestGetJobLog(projectId: number | string, jobId: number): Promise<string> {
|
||||
return this.requestText(
|
||||
'GET',
|
||||
`/api/v4/projects/${encodeURIComponent(projectId)}/jobs/${jobId}/trace`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry a single job.
|
||||
*/
|
||||
public async retryJob(projectId: number | string, jobId: number): Promise<IGitLabJob> {
|
||||
/** @internal */
|
||||
async requestRetryJob(projectId: number | string, jobId: number): Promise<IGitLabJob> {
|
||||
return this.request<IGitLabJob>(
|
||||
'POST',
|
||||
`/api/v4/projects/${encodeURIComponent(projectId)}/jobs/${jobId}/retry`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a running job.
|
||||
*/
|
||||
public async cancelJob(projectId: number | string, jobId: number): Promise<IGitLabJob> {
|
||||
/** @internal */
|
||||
async requestCancelJob(projectId: number | string, jobId: number): Promise<IGitLabJob> {
|
||||
return this.request<IGitLabJob>(
|
||||
'POST',
|
||||
`/api/v4/projects/${encodeURIComponent(projectId)}/jobs/${jobId}/cancel`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a manual job (play action).
|
||||
*/
|
||||
public async playJob(projectId: number | string, jobId: number): Promise<IGitLabJob> {
|
||||
/** @internal */
|
||||
async requestPlayJob(projectId: number | string, jobId: number): Promise<IGitLabJob> {
|
||||
return this.request<IGitLabJob>(
|
||||
'POST',
|
||||
`/api/v4/projects/${encodeURIComponent(projectId)}/jobs/${jobId}/play`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase a job's trace and artifacts.
|
||||
*/
|
||||
public async eraseJob(projectId: number | string, jobId: number): Promise<void> {
|
||||
/** @internal */
|
||||
async requestEraseJob(projectId: number | string, jobId: number): Promise<void> {
|
||||
await this.request(
|
||||
'POST',
|
||||
`/api/v4/projects/${encodeURIComponent(projectId)}/jobs/${jobId}/erase`,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
137
ts/gitlab.classes.group.ts
Normal file
137
ts/gitlab.classes.group.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import type { GitLabClient } from './gitlab.classes.gitlabclient.js';
|
||||
import type { IGitLabGroup, IListOptions, IVariableOptions } from './gitlab.interfaces.js';
|
||||
import { GitLabProject } from './gitlab.classes.project.js';
|
||||
import { GitLabVariable } from './gitlab.classes.variable.js';
|
||||
import { autoPaginate } from './gitlab.helpers.js';
|
||||
|
||||
export class GitLabGroup {
|
||||
// Raw data
|
||||
public readonly id: number;
|
||||
public readonly name: string;
|
||||
public readonly fullPath: string;
|
||||
public readonly description: string;
|
||||
public readonly webUrl: string;
|
||||
public readonly visibility: string;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
private client: GitLabClient,
|
||||
raw: IGitLabGroup,
|
||||
) {
|
||||
this.id = raw.id;
|
||||
this.name = raw.name || '';
|
||||
this.fullPath = raw.full_path || '';
|
||||
this.description = raw.description || '';
|
||||
this.webUrl = raw.web_url || '';
|
||||
this.visibility = raw.visibility || 'private';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Projects
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async getProjects(opts?: IListOptions): Promise<GitLabProject[]> {
|
||||
return autoPaginate(
|
||||
(page, perPage) => this.client.requestGetGroupProjects(this.id, { ...opts, page, perPage }),
|
||||
opts,
|
||||
).then(projects => projects.map(p => new GitLabProject(this.client, p)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Descendant Groups
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async getDescendantGroups(opts?: IListOptions): Promise<GitLabGroup[]> {
|
||||
return autoPaginate(
|
||||
(page, perPage) => this.client.requestGetDescendantGroups(this.id, { ...opts, page, perPage }),
|
||||
opts,
|
||||
).then(groups => groups.map(g => new GitLabGroup(this.client, g)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Variables (CI/CD)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async getVariables(): Promise<GitLabVariable[]> {
|
||||
const vars = await this.client.requestGetGroupVariables(this.id);
|
||||
return vars.map(v => new GitLabVariable(v));
|
||||
}
|
||||
|
||||
async createVariable(key: string, value: string, opts?: IVariableOptions): Promise<GitLabVariable> {
|
||||
const raw = await this.client.requestCreateGroupVariable(this.id, key, value, opts);
|
||||
return new GitLabVariable(raw);
|
||||
}
|
||||
|
||||
async updateVariable(key: string, value: string, opts?: IVariableOptions): Promise<GitLabVariable> {
|
||||
const raw = await this.client.requestUpdateGroupVariable(this.id, key, value, opts);
|
||||
return new GitLabVariable(raw);
|
||||
}
|
||||
|
||||
async deleteVariable(key: string): Promise<void> {
|
||||
await this.client.requestDeleteGroupVariable(this.id, key);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mutation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Update group properties.
|
||||
*/
|
||||
async update(data: {
|
||||
name?: string;
|
||||
path?: string;
|
||||
description?: string;
|
||||
visibility?: string;
|
||||
}): Promise<void> {
|
||||
await this.client.requestUpdateGroup(this.id, {
|
||||
name: data.name,
|
||||
path: data.path,
|
||||
description: data.description,
|
||||
visibility: data.visibility,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload an avatar image for this group (multipart FormData).
|
||||
*/
|
||||
async setAvatar(imageData: Uint8Array, filename: string): Promise<void> {
|
||||
await this.client.requestSetGroupAvatar(this.id, imageData, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the group's avatar.
|
||||
*/
|
||||
async deleteAvatar(): Promise<void> {
|
||||
await this.client.requestUpdateGroup(this.id, { avatar: '' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer this group to be a child of another group.
|
||||
*/
|
||||
async transfer(parentGroupId: number): Promise<void> {
|
||||
await this.client.requestTransferGroup(this.id, parentGroupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this group.
|
||||
*/
|
||||
async delete(): Promise<void> {
|
||||
await this.client.requestDeleteGroup(this.id);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Serialization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
toJSON(): IGitLabGroup {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
full_path: this.fullPath,
|
||||
description: this.description,
|
||||
web_url: this.webUrl,
|
||||
visibility: this.visibility,
|
||||
};
|
||||
}
|
||||
}
|
||||
104
ts/gitlab.classes.job.ts
Normal file
104
ts/gitlab.classes.job.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { GitLabClient } from './gitlab.classes.gitlabclient.js';
|
||||
import type { IGitLabJob } from './gitlab.interfaces.js';
|
||||
|
||||
export class GitLabJob {
|
||||
// Raw data
|
||||
public readonly id: number;
|
||||
public readonly name: string;
|
||||
public readonly stage: string;
|
||||
public readonly status: string;
|
||||
public readonly ref: string;
|
||||
public readonly tag: boolean;
|
||||
public readonly webUrl: string;
|
||||
public readonly createdAt: string;
|
||||
public readonly startedAt: string;
|
||||
public readonly finishedAt: string;
|
||||
public readonly duration: number;
|
||||
public readonly queuedDuration: number;
|
||||
public readonly coverage: number;
|
||||
public readonly allowFailure: boolean;
|
||||
public readonly failureReason: string;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
private client: GitLabClient,
|
||||
private projectId: number | string,
|
||||
raw: IGitLabJob,
|
||||
) {
|
||||
this.id = raw.id;
|
||||
this.name = raw.name || '';
|
||||
this.stage = raw.stage || '';
|
||||
this.status = raw.status || '';
|
||||
this.ref = raw.ref || '';
|
||||
this.tag = raw.tag ?? false;
|
||||
this.webUrl = raw.web_url || '';
|
||||
this.createdAt = raw.created_at || '';
|
||||
this.startedAt = raw.started_at || '';
|
||||
this.finishedAt = raw.finished_at || '';
|
||||
this.duration = raw.duration || 0;
|
||||
this.queuedDuration = raw.queued_duration || 0;
|
||||
this.coverage = raw.coverage || 0;
|
||||
this.allowFailure = raw.allow_failure ?? false;
|
||||
this.failureReason = raw.failure_reason || '';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Log
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async getLog(): Promise<string> {
|
||||
return this.client.requestGetJobLog(this.projectId, this.id);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Actions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async retry(): Promise<GitLabJob> {
|
||||
const raw = await this.client.requestRetryJob(this.projectId, this.id);
|
||||
return new GitLabJob(this.client, this.projectId, raw);
|
||||
}
|
||||
|
||||
async cancel(): Promise<GitLabJob> {
|
||||
const raw = await this.client.requestCancelJob(this.projectId, this.id);
|
||||
return new GitLabJob(this.client, this.projectId, raw);
|
||||
}
|
||||
|
||||
async play(): Promise<GitLabJob> {
|
||||
const raw = await this.client.requestPlayJob(this.projectId, this.id);
|
||||
return new GitLabJob(this.client, this.projectId, raw);
|
||||
}
|
||||
|
||||
async erase(): Promise<void> {
|
||||
await this.client.requestEraseJob(this.projectId, this.id);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Serialization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
toJSON(): IGitLabJob {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
stage: this.stage,
|
||||
status: this.status,
|
||||
ref: this.ref,
|
||||
tag: this.tag,
|
||||
web_url: this.webUrl,
|
||||
created_at: this.createdAt,
|
||||
started_at: this.startedAt,
|
||||
finished_at: this.finishedAt,
|
||||
duration: this.duration,
|
||||
queued_duration: this.queuedDuration,
|
||||
coverage: this.coverage,
|
||||
allow_failure: this.allowFailure,
|
||||
failure_reason: this.failureReason,
|
||||
pipeline: { id: 0, project_id: 0, ref: '', sha: '', status: '' },
|
||||
user: { id: 0, username: '', name: '', email: '', avatar_url: '', web_url: '', state: '' },
|
||||
runner: { id: 0, description: '', active: false, is_shared: false },
|
||||
artifacts: [],
|
||||
artifacts_expire_at: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
122
ts/gitlab.classes.pipeline.ts
Normal file
122
ts/gitlab.classes.pipeline.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import type { GitLabClient } from './gitlab.classes.gitlabclient.js';
|
||||
import type { IGitLabPipeline, IJobListOptions } from './gitlab.interfaces.js';
|
||||
import { GitLabJob } from './gitlab.classes.job.js';
|
||||
import { GitLabPipelineVariable } from './gitlab.classes.variable.js';
|
||||
import { GitLabTestReport } from './gitlab.classes.testreport.js';
|
||||
import { autoPaginate } from './gitlab.helpers.js';
|
||||
|
||||
export class GitLabPipeline {
|
||||
// Raw data
|
||||
public readonly id: number;
|
||||
public readonly iid: number;
|
||||
public readonly projectId: number;
|
||||
public readonly status: string;
|
||||
public readonly ref: string;
|
||||
public readonly sha: string;
|
||||
public readonly webUrl: string;
|
||||
public readonly duration: number;
|
||||
public readonly queuedDuration: number;
|
||||
public readonly createdAt: string;
|
||||
public readonly updatedAt: string;
|
||||
public readonly startedAt: string;
|
||||
public readonly finishedAt: string;
|
||||
public readonly source: string;
|
||||
public readonly tag: boolean;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
private client: GitLabClient,
|
||||
raw: IGitLabPipeline,
|
||||
) {
|
||||
this.id = raw.id;
|
||||
this.iid = raw.iid || 0;
|
||||
this.projectId = raw.project_id || 0;
|
||||
this.status = raw.status || '';
|
||||
this.ref = raw.ref || '';
|
||||
this.sha = raw.sha || '';
|
||||
this.webUrl = raw.web_url || '';
|
||||
this.duration = raw.duration || 0;
|
||||
this.queuedDuration = raw.queued_duration || 0;
|
||||
this.createdAt = raw.created_at || '';
|
||||
this.updatedAt = raw.updated_at || '';
|
||||
this.startedAt = raw.started_at || '';
|
||||
this.finishedAt = raw.finished_at || '';
|
||||
this.source = raw.source || '';
|
||||
this.tag = raw.tag ?? false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Jobs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async getJobs(opts?: IJobListOptions): Promise<GitLabJob[]> {
|
||||
return autoPaginate(
|
||||
(page, perPage) => this.client.requestGetPipelineJobs(this.projectId, this.id, { ...opts, page, perPage }),
|
||||
opts,
|
||||
).then(jobs => jobs.map(j => new GitLabJob(this.client, this.projectId, j)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Variables & Test Report
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async getVariables(): Promise<GitLabPipelineVariable[]> {
|
||||
const vars = await this.client.requestGetPipelineVariables(this.projectId, this.id);
|
||||
return vars.map(v => new GitLabPipelineVariable(v));
|
||||
}
|
||||
|
||||
async getTestReport(): Promise<GitLabTestReport> {
|
||||
const raw = await this.client.requestGetPipelineTestReport(this.projectId, this.id);
|
||||
return new GitLabTestReport(raw);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Actions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async retry(): Promise<GitLabPipeline> {
|
||||
const raw = await this.client.requestRetryPipeline(this.projectId, this.id);
|
||||
return new GitLabPipeline(this.client, raw);
|
||||
}
|
||||
|
||||
async cancel(): Promise<GitLabPipeline> {
|
||||
const raw = await this.client.requestCancelPipeline(this.projectId, this.id);
|
||||
return new GitLabPipeline(this.client, raw);
|
||||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
await this.client.requestDeletePipeline(this.projectId, this.id);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Serialization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
toJSON(): IGitLabPipeline {
|
||||
return {
|
||||
id: this.id,
|
||||
iid: this.iid,
|
||||
project_id: this.projectId,
|
||||
status: this.status,
|
||||
ref: this.ref,
|
||||
sha: this.sha,
|
||||
before_sha: '',
|
||||
tag: this.tag,
|
||||
web_url: this.webUrl,
|
||||
duration: this.duration,
|
||||
queued_duration: this.queuedDuration,
|
||||
created_at: this.createdAt,
|
||||
updated_at: this.updatedAt,
|
||||
started_at: this.startedAt,
|
||||
finished_at: this.finishedAt,
|
||||
source: this.source,
|
||||
coverage: '',
|
||||
user: { id: 0, username: '', name: '', email: '', avatar_url: '', web_url: '', state: '' },
|
||||
detailed_status: {
|
||||
icon: '', text: '', label: '', group: '', tooltip: '',
|
||||
has_details: false, details_path: '', favicon: '',
|
||||
},
|
||||
yaml_errors: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
177
ts/gitlab.classes.project.ts
Normal file
177
ts/gitlab.classes.project.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import type { GitLabClient } from './gitlab.classes.gitlabclient.js';
|
||||
import type { IGitLabProject, IListOptions, IVariableOptions, IPipelineListOptions } from './gitlab.interfaces.js';
|
||||
import { GitLabBranch } from './gitlab.classes.branch.js';
|
||||
import { GitLabTag } from './gitlab.classes.tag.js';
|
||||
import { GitLabProtectedBranch } from './gitlab.classes.protectedbranch.js';
|
||||
import { GitLabVariable } from './gitlab.classes.variable.js';
|
||||
import { GitLabPipeline } from './gitlab.classes.pipeline.js';
|
||||
import { autoPaginate } from './gitlab.helpers.js';
|
||||
|
||||
export class GitLabProject {
|
||||
// Raw data
|
||||
public readonly id: number;
|
||||
public readonly name: string;
|
||||
public readonly fullPath: string;
|
||||
public readonly description: string;
|
||||
public readonly defaultBranch: string;
|
||||
public readonly webUrl: string;
|
||||
public readonly visibility: string;
|
||||
public readonly topics: string[];
|
||||
public readonly lastActivityAt: string;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
private client: GitLabClient,
|
||||
raw: IGitLabProject,
|
||||
) {
|
||||
this.id = raw.id;
|
||||
this.name = raw.name || '';
|
||||
this.fullPath = raw.path_with_namespace || '';
|
||||
this.description = raw.description || '';
|
||||
this.defaultBranch = raw.default_branch || 'main';
|
||||
this.webUrl = raw.web_url || '';
|
||||
this.visibility = raw.visibility || 'private';
|
||||
this.topics = raw.topics || [];
|
||||
this.lastActivityAt = raw.last_activity_at || '';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Branches & Tags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async getBranches(opts?: IListOptions): Promise<GitLabBranch[]> {
|
||||
return autoPaginate(
|
||||
(page, perPage) => this.client.requestGetRepoBranches(this.id, { ...opts, page, perPage }),
|
||||
opts,
|
||||
).then(branches => branches.map(b => new GitLabBranch(b)));
|
||||
}
|
||||
|
||||
async getTags(opts?: IListOptions): Promise<GitLabTag[]> {
|
||||
return autoPaginate(
|
||||
(page, perPage) => this.client.requestGetRepoTags(this.id, { ...opts, page, perPage }),
|
||||
opts,
|
||||
).then(tags => tags.map(t => new GitLabTag(t)));
|
||||
}
|
||||
|
||||
async getProtectedBranches(): Promise<GitLabProtectedBranch[]> {
|
||||
const branches = await this.client.requestGetProtectedBranches(this.id);
|
||||
return branches.map(b => new GitLabProtectedBranch(b));
|
||||
}
|
||||
|
||||
async unprotectBranch(branchName: string): Promise<void> {
|
||||
await this.client.requestUnprotectBranch(this.id, branchName);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Variables (CI/CD)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async getVariables(): Promise<GitLabVariable[]> {
|
||||
const vars = await this.client.requestGetProjectVariables(this.id);
|
||||
return vars.map(v => new GitLabVariable(v));
|
||||
}
|
||||
|
||||
async createVariable(key: string, value: string, opts?: IVariableOptions): Promise<GitLabVariable> {
|
||||
const raw = await this.client.requestCreateProjectVariable(this.id, key, value, opts);
|
||||
return new GitLabVariable(raw);
|
||||
}
|
||||
|
||||
async updateVariable(key: string, value: string, opts?: IVariableOptions): Promise<GitLabVariable> {
|
||||
const raw = await this.client.requestUpdateProjectVariable(this.id, key, value, opts);
|
||||
return new GitLabVariable(raw);
|
||||
}
|
||||
|
||||
async deleteVariable(key: string): Promise<void> {
|
||||
await this.client.requestDeleteProjectVariable(this.id, key);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pipelines
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async getPipelines(opts?: IPipelineListOptions): Promise<GitLabPipeline[]> {
|
||||
return autoPaginate(
|
||||
(page, perPage) => this.client.requestGetPipelines(this.id, { ...opts, page, perPage }),
|
||||
opts,
|
||||
).then(pipelines => pipelines.map(p => new GitLabPipeline(this.client, p)));
|
||||
}
|
||||
|
||||
async triggerPipeline(
|
||||
ref: string,
|
||||
variables?: { key: string; value: string; variable_type?: string }[],
|
||||
): Promise<GitLabPipeline> {
|
||||
const raw = await this.client.requestTriggerPipeline(this.id, ref, variables);
|
||||
return new GitLabPipeline(this.client, raw);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mutation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Update project properties.
|
||||
*/
|
||||
async update(data: {
|
||||
name?: string;
|
||||
path?: string;
|
||||
description?: string;
|
||||
defaultBranch?: string;
|
||||
visibility?: string;
|
||||
topics?: string[];
|
||||
}): Promise<void> {
|
||||
await this.client.requestUpdateProject(this.id, {
|
||||
name: data.name,
|
||||
path: data.path,
|
||||
description: data.description,
|
||||
default_branch: data.defaultBranch,
|
||||
visibility: data.visibility,
|
||||
topics: data.topics,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload an avatar image for this project (multipart FormData).
|
||||
*/
|
||||
async setAvatar(imageData: Uint8Array, filename: string): Promise<void> {
|
||||
await this.client.requestSetProjectAvatar(this.id, imageData, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the project's avatar.
|
||||
*/
|
||||
async deleteAvatar(): Promise<void> {
|
||||
await this.client.requestUpdateProject(this.id, { avatar: '' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer this project to a different namespace.
|
||||
*/
|
||||
async transfer(namespaceId: number): Promise<void> {
|
||||
await this.client.requestTransferProject(this.id, namespaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this project.
|
||||
*/
|
||||
async delete(): Promise<void> {
|
||||
await this.client.requestDeleteProject(this.id);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Serialization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
toJSON(): IGitLabProject {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
path_with_namespace: this.fullPath,
|
||||
description: this.description,
|
||||
default_branch: this.defaultBranch,
|
||||
web_url: this.webUrl,
|
||||
visibility: this.visibility,
|
||||
topics: this.topics,
|
||||
last_activity_at: this.lastActivityAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
21
ts/gitlab.classes.protectedbranch.ts
Normal file
21
ts/gitlab.classes.protectedbranch.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { IGitLabProtectedBranch } from './gitlab.interfaces.js';
|
||||
|
||||
export class GitLabProtectedBranch {
|
||||
public readonly id: number;
|
||||
public readonly name: string;
|
||||
public readonly allowForcePush: boolean;
|
||||
|
||||
constructor(raw: IGitLabProtectedBranch) {
|
||||
this.id = raw.id;
|
||||
this.name = raw.name || '';
|
||||
this.allowForcePush = raw.allow_force_push ?? false;
|
||||
}
|
||||
|
||||
toJSON(): IGitLabProtectedBranch {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
allow_force_push: this.allowForcePush,
|
||||
};
|
||||
}
|
||||
}
|
||||
18
ts/gitlab.classes.tag.ts
Normal file
18
ts/gitlab.classes.tag.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { IGitLabTag } from './gitlab.interfaces.js';
|
||||
|
||||
export class GitLabTag {
|
||||
public readonly name: string;
|
||||
public readonly commitSha: string;
|
||||
|
||||
constructor(raw: IGitLabTag) {
|
||||
this.name = raw.name || '';
|
||||
this.commitSha = raw.commit?.id || '';
|
||||
}
|
||||
|
||||
toJSON(): IGitLabTag {
|
||||
return {
|
||||
name: this.name,
|
||||
commit: { id: this.commitSha },
|
||||
};
|
||||
}
|
||||
}
|
||||
97
ts/gitlab.classes.testreport.ts
Normal file
97
ts/gitlab.classes.testreport.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { IGitLabTestReport, IGitLabTestSuite, IGitLabTestCase } from './gitlab.interfaces.js';
|
||||
|
||||
export class GitLabTestCase {
|
||||
public readonly status: string;
|
||||
public readonly name: string;
|
||||
public readonly classname: string;
|
||||
public readonly executionTime: number;
|
||||
public readonly systemOutput: string;
|
||||
public readonly stackTrace: string;
|
||||
|
||||
constructor(raw: IGitLabTestCase) {
|
||||
this.status = raw.status || '';
|
||||
this.name = raw.name || '';
|
||||
this.classname = raw.classname || '';
|
||||
this.executionTime = raw.execution_time || 0;
|
||||
this.systemOutput = raw.system_output || '';
|
||||
this.stackTrace = raw.stack_trace || '';
|
||||
}
|
||||
|
||||
toJSON(): IGitLabTestCase {
|
||||
return {
|
||||
status: this.status,
|
||||
name: this.name,
|
||||
classname: this.classname,
|
||||
execution_time: this.executionTime,
|
||||
system_output: this.systemOutput,
|
||||
stack_trace: this.stackTrace,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class GitLabTestSuite {
|
||||
public readonly name: string;
|
||||
public readonly totalTime: number;
|
||||
public readonly totalCount: number;
|
||||
public readonly successCount: number;
|
||||
public readonly failedCount: number;
|
||||
public readonly skippedCount: number;
|
||||
public readonly errorCount: number;
|
||||
public readonly testCases: GitLabTestCase[];
|
||||
|
||||
constructor(raw: IGitLabTestSuite) {
|
||||
this.name = raw.name || '';
|
||||
this.totalTime = raw.total_time || 0;
|
||||
this.totalCount = raw.total_count || 0;
|
||||
this.successCount = raw.success_count || 0;
|
||||
this.failedCount = raw.failed_count || 0;
|
||||
this.skippedCount = raw.skipped_count || 0;
|
||||
this.errorCount = raw.error_count || 0;
|
||||
this.testCases = (raw.test_cases || []).map(tc => new GitLabTestCase(tc));
|
||||
}
|
||||
|
||||
toJSON(): IGitLabTestSuite {
|
||||
return {
|
||||
name: this.name,
|
||||
total_time: this.totalTime,
|
||||
total_count: this.totalCount,
|
||||
success_count: this.successCount,
|
||||
failed_count: this.failedCount,
|
||||
skipped_count: this.skippedCount,
|
||||
error_count: this.errorCount,
|
||||
test_cases: this.testCases.map(tc => tc.toJSON()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class GitLabTestReport {
|
||||
public readonly totalTime: number;
|
||||
public readonly totalCount: number;
|
||||
public readonly successCount: number;
|
||||
public readonly failedCount: number;
|
||||
public readonly skippedCount: number;
|
||||
public readonly errorCount: number;
|
||||
public readonly testSuites: GitLabTestSuite[];
|
||||
|
||||
constructor(raw: IGitLabTestReport) {
|
||||
this.totalTime = raw.total_time || 0;
|
||||
this.totalCount = raw.total_count || 0;
|
||||
this.successCount = raw.success_count || 0;
|
||||
this.failedCount = raw.failed_count || 0;
|
||||
this.skippedCount = raw.skipped_count || 0;
|
||||
this.errorCount = raw.error_count || 0;
|
||||
this.testSuites = (raw.test_suites || []).map(ts => new GitLabTestSuite(ts));
|
||||
}
|
||||
|
||||
toJSON(): IGitLabTestReport {
|
||||
return {
|
||||
total_time: this.totalTime,
|
||||
total_count: this.totalCount,
|
||||
success_count: this.successCount,
|
||||
failed_count: this.failedCount,
|
||||
skipped_count: this.skippedCount,
|
||||
error_count: this.errorCount,
|
||||
test_suites: this.testSuites.map(ts => ts.toJSON()),
|
||||
};
|
||||
}
|
||||
}
|
||||
50
ts/gitlab.classes.variable.ts
Normal file
50
ts/gitlab.classes.variable.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { IGitLabVariable, IGitLabPipelineVariable } from './gitlab.interfaces.js';
|
||||
|
||||
export class GitLabVariable {
|
||||
public readonly key: string;
|
||||
public readonly value: string;
|
||||
public readonly variableType: string;
|
||||
public readonly protected: boolean;
|
||||
public readonly masked: boolean;
|
||||
public readonly environmentScope: string;
|
||||
|
||||
constructor(raw: IGitLabVariable) {
|
||||
this.key = raw.key || '';
|
||||
this.value = raw.value || '';
|
||||
this.variableType = raw.variable_type || 'env_var';
|
||||
this.protected = raw.protected ?? false;
|
||||
this.masked = raw.masked ?? false;
|
||||
this.environmentScope = raw.environment_scope || '*';
|
||||
}
|
||||
|
||||
toJSON(): IGitLabVariable {
|
||||
return {
|
||||
key: this.key,
|
||||
value: this.value,
|
||||
variable_type: this.variableType,
|
||||
protected: this.protected,
|
||||
masked: this.masked,
|
||||
environment_scope: this.environmentScope,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class GitLabPipelineVariable {
|
||||
public readonly key: string;
|
||||
public readonly value: string;
|
||||
public readonly variableType: string;
|
||||
|
||||
constructor(raw: IGitLabPipelineVariable) {
|
||||
this.key = raw.key || '';
|
||||
this.value = raw.value || '';
|
||||
this.variableType = raw.variable_type || 'env_var';
|
||||
}
|
||||
|
||||
toJSON(): IGitLabPipelineVariable {
|
||||
return {
|
||||
key: this.key,
|
||||
value: this.value,
|
||||
variable_type: this.variableType,
|
||||
};
|
||||
}
|
||||
}
|
||||
26
ts/gitlab.helpers.ts
Normal file
26
ts/gitlab.helpers.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Auto-paginate a list endpoint.
|
||||
* If opts includes a specific page, returns just that page (no auto-pagination).
|
||||
*/
|
||||
export async function autoPaginate<T>(
|
||||
fetchPage: (page: number, perPage: number) => Promise<T[]>,
|
||||
opts?: { page?: number; perPage?: number },
|
||||
): Promise<T[]> {
|
||||
const perPage = opts?.perPage || 50;
|
||||
|
||||
// If caller requests a specific page, return just that page
|
||||
if (opts?.page) {
|
||||
return fetchPage(opts.page, perPage);
|
||||
}
|
||||
|
||||
// Otherwise auto-paginate through all pages
|
||||
const all: T[] = [];
|
||||
let page = 1;
|
||||
while (true) {
|
||||
const items = await fetchPage(page, perPage);
|
||||
all.push(...items);
|
||||
if (items.length < perPage) break;
|
||||
page++;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
19
ts/index.ts
19
ts/index.ts
@@ -1,4 +1,21 @@
|
||||
// Main client
|
||||
export { GitLabClient } from './gitlab.classes.gitlabclient.js';
|
||||
|
||||
// Domain classes
|
||||
export { GitLabGroup } from './gitlab.classes.group.js';
|
||||
export { GitLabProject } from './gitlab.classes.project.js';
|
||||
export { GitLabPipeline } from './gitlab.classes.pipeline.js';
|
||||
export { GitLabJob } from './gitlab.classes.job.js';
|
||||
export { GitLabBranch } from './gitlab.classes.branch.js';
|
||||
export { GitLabTag } from './gitlab.classes.tag.js';
|
||||
export { GitLabProtectedBranch } from './gitlab.classes.protectedbranch.js';
|
||||
export { GitLabVariable, GitLabPipelineVariable } from './gitlab.classes.variable.js';
|
||||
export { GitLabTestReport, GitLabTestSuite, GitLabTestCase } from './gitlab.classes.testreport.js';
|
||||
|
||||
// Helpers
|
||||
export { autoPaginate } from './gitlab.helpers.js';
|
||||
|
||||
// Interfaces (raw API types)
|
||||
export type {
|
||||
IGitLabUser,
|
||||
IGitLabProject,
|
||||
@@ -19,4 +36,6 @@ export type {
|
||||
IPipelineListOptions,
|
||||
IJobListOptions,
|
||||
} from './gitlab.interfaces.js';
|
||||
|
||||
// Commit info
|
||||
export { commitinfo } from './00_commitinfo_data.js';
|
||||
|
||||
Reference in New Issue
Block a user