feat(client): add rich domain classes, helpers, and refactor GitLabClient internals

This commit is contained in:
2026-03-02 13:10:54 +00:00
parent 4d90bb01cf
commit 7ba0fc984e
13 changed files with 1587 additions and 392 deletions

675
readme.md
View File

@@ -1,16 +1,82 @@
# @apiclient.xyz/gitlab
A TypeScript client for the GitLab API, providing easy access to projects, groups, CI/CD variables, and pipelines.
A powerful, fully-typed TypeScript client for the GitLab API 🚀
[![npm version](https://img.shields.io/npm/v/@apiclient.xyz/gitlab.svg)](https://www.npmjs.com/package/@apiclient.xyz/gitlab)
[![license](https://img.shields.io/npm/l/@apiclient.xyz/gitlab.svg)](./LICENSE)
`@apiclient.xyz/gitlab` gives you a clean, object-oriented interface to the GitLab REST API. It wraps projects, groups, pipelines, jobs, CI/CD variables, branches, tags, protected branches, and test reports into rich domain classes with full auto-pagination support. Works with any GitLab instance -- cloud or self-hosted.
## Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
## Install
```bash
npm install @apiclient.xyz/gitlab
# or
pnpm install @apiclient.xyz/gitlab
```
## Usage
## Quick Start ⚡
All examples below use ESM imports and async/await.
```typescript
import { GitLabClient } from '@apiclient.xyz/gitlab';
const client = new GitLabClient('https://gitlab.example.com', 'your-private-token');
// Verify connectivity
const { ok, error } = await client.testConnection();
console.log(ok ? '✅ Connected!' : `${error}`);
// Grab all your projects
const projects = await client.getProjects();
for (const project of projects) {
console.log(`${project.name}${project.webUrl}`);
}
```
That's it. No boilerplate, no callback soup, just straightforward async/await.
## Core Concepts 🧠
### Rich Domain Objects
Every API response is wrapped in a **domain class** that exposes typed properties and chainable methods. You never deal with raw JSON unless you want to -- every class has a `.toJSON()` escape hatch.
```
GitLabClient
├── GitLabGroup → .getProjects(), .getVariables(), .update(), .delete(), ...
├── GitLabProject → .getPipelines(), .getBranches(), .getVariables(), .triggerPipeline(), ...
│ ├── GitLabBranch
│ ├── GitLabTag
│ ├── GitLabProtectedBranch
│ └── GitLabVariable
├── GitLabPipeline → .getJobs(), .getTestReport(), .retry(), .cancel(), .delete()
│ ├── GitLabJob → .getLog(), .retry(), .cancel(), .play(), .erase()
│ ├── GitLabPipelineVariable
│ └── GitLabTestReport
│ └── GitLabTestSuite
│ └── GitLabTestCase
└── GitLabVariable
```
### Auto-Pagination 🔄
List endpoints automatically page through **all** results by default. Need just one page? Pass `{ page: N }` explicitly.
```typescript
// Fetches ALL groups (auto-paginates transparently)
const allGroups = await client.getGroups();
// Fetches only page 2, 10 per page
const page2 = await client.getGroups({ page: 2, perPage: 10 });
```
The `autoPaginate` helper is also exported if you want to use it in custom integrations.
## Usage
### Creating a Client
@@ -20,7 +86,7 @@ import { GitLabClient } from '@apiclient.xyz/gitlab';
const client = new GitLabClient('https://gitlab.example.com', 'your-private-token');
```
The constructor accepts the base URL of your GitLab instance and a personal access token (or project/group token) used for authentication via the `PRIVATE-TOKEN` header.
The constructor takes the base URL of your GitLab instance and a personal access token (or project/group token) for `PRIVATE-TOKEN` header authentication.
### Testing the Connection
@@ -34,165 +100,469 @@ if (result.ok) {
}
```
### Listing Projects
---
Retrieve projects you are a member of, ordered by most recently updated.
### Groups 🏢
```typescript
// First page, default 50 per page
const projects = await client.getProjects();
// Search with pagination
const filtered = await client.getProjects({
search: 'my-service',
page: 1,
perPage: 20,
});
for (const project of filtered) {
console.log(`${project.id} - ${project.path_with_namespace}`);
}
```
### Listing Groups
#### List All Groups
```typescript
const groups = await client.getGroups();
// Search groups by name
const matchingGroups = await client.getGroups({ search: 'platform' });
// Search by name
const devGroups = await client.getGroups({ search: 'platform' });
for (const group of matchingGroups) {
console.log(`${group.id} - ${group.full_path}`);
for (const group of devGroups) {
console.log(`${group.id} ${group.fullPath} (${group.visibility})`);
}
```
### Managing Project Variables
#### Get a Single Group
```typescript
const projectId = 42;
const group = await client.getGroup('my-org/my-team');
console.log(group.name, group.webUrl);
```
// List all variables
const vars = await client.getProjectVariables(projectId);
#### Create a Group
// Create a variable
await client.createProjectVariable(projectId, 'DATABASE_URL', 'postgres://...', {
```typescript
const newGroup = await client.createGroup('backend-team', 'backend-team');
// Create a sub-group under an existing parent
const subGroup = await client.createGroup('api-squad', 'api-squad', parentGroup.id);
```
#### Group Operations
```typescript
// Update properties
await group.update({ description: 'The backend crew 🔧', visibility: 'internal' });
// Upload / remove avatar
await group.setAvatar(imageBytes, 'logo.png');
await group.deleteAvatar();
// Transfer to another parent group
await group.transfer(targetGroupId);
// List descendant sub-groups
const children = await group.getDescendantGroups();
// List projects inside the group
const projects = await group.getProjects({ search: 'api' });
// Delete
await group.delete();
```
---
### Projects 📦
#### List All Projects
```typescript
const projects = await client.getProjects();
// Search with pagination
const matches = await client.getProjects({ search: 'my-service', perPage: 20 });
for (const p of matches) {
console.log(`${p.id}${p.fullPath} [${p.defaultBranch}]`);
}
```
#### Get a Single Project
```typescript
// By numeric ID
const project = await client.getProject(42);
// By full path
const project = await client.getProject('my-org/my-repo');
```
#### Create a Project
```typescript
const project = await client.createProject('new-service', {
namespaceId: group.id,
visibility: 'internal',
description: 'Our shiny new microservice',
});
```
#### Project Operations
```typescript
// Update properties
await project.update({
description: 'Updated description',
defaultBranch: 'develop',
topics: ['typescript', 'api'],
});
// Avatar management
await project.setAvatar(imageBytes, 'project-logo.png');
await project.deleteAvatar();
// Transfer to a different namespace
await project.transfer(otherNamespaceId);
// Delete
await project.delete();
```
---
### Branches & Tags 🌿
```typescript
// List all branches (auto-paginated)
const branches = await project.getBranches();
for (const b of branches) {
console.log(`${b.name} @ ${b.commitSha}`);
}
// List all tags
const tags = await project.getTags();
for (const t of tags) {
console.log(`${t.name} @ ${t.commitSha}`);
}
```
#### Protected Branches 🔒
```typescript
const protectedBranches = await project.getProtectedBranches();
for (const pb of protectedBranches) {
console.log(`${pb.name} — force push: ${pb.allowForcePush}`);
}
// Remove branch protection
await project.unprotectBranch('develop');
```
---
### CI/CD Variables 🔑
Variables work identically on both projects and groups.
#### Project Variables
```typescript
// List all
const vars = await project.getVariables();
// Create
const dbVar = await project.createVariable('DATABASE_URL', 'postgres://localhost/db', {
protected: true,
masked: true,
environment_scope: 'production',
});
// Update a variable
await client.updateProjectVariable(projectId, 'DATABASE_URL', 'postgres://new-host/...', {
// Update
await project.updateVariable('DATABASE_URL', 'postgres://newhost/db', {
protected: true,
});
// Delete a variable
await client.deleteProjectVariable(projectId, 'DATABASE_URL');
// Delete
await project.deleteVariable('DATABASE_URL');
```
### Managing Group Variables
The group variable API mirrors the project variable API.
#### Group Variables
```typescript
const groupId = 7;
const groupVars = await group.getVariables();
// List all variables
const groupVars = await client.getGroupVariables(groupId);
// Create a variable
await client.createGroupVariable(groupId, 'NPM_TOKEN', 'tok-xxx', {
await group.createVariable('NPM_TOKEN', 'tok-xxx', {
protected: false,
masked: true,
environment_scope: '*',
});
// Update a variable
await client.updateGroupVariable(groupId, 'NPM_TOKEN', 'tok-yyy', {
masked: true,
});
// Delete a variable
await client.deleteGroupVariable(groupId, 'NPM_TOKEN');
await group.updateVariable('NPM_TOKEN', 'tok-yyy');
await group.deleteVariable('NPM_TOKEN');
```
### Working with Pipelines
Each `GitLabVariable` instance exposes typed properties:
```typescript
const projectId = 42;
// List recent pipelines
const pipelines = await client.getPipelines(projectId, { page: 1, perPage: 10 });
for (const pipeline of pipelines) {
console.log(`Pipeline #${pipeline.id} [${pipeline.status}] on ${pipeline.ref}`);
}
// Get jobs for a specific pipeline
const jobs = await client.getPipelineJobs(projectId, pipelines[0].id);
for (const job of jobs) {
console.log(` Job "${job.name}" (${job.stage}): ${job.status}`);
}
// Read the raw log output of a job
const log = await client.getJobLog(projectId, jobs[0].id);
console.log(log);
// Retry a failed pipeline
await client.retryPipeline(projectId, pipelines[0].id);
// Cancel a running pipeline
await client.cancelPipeline(projectId, pipelines[0].id);
console.log(dbVar.key); // 'DATABASE_URL'
console.log(dbVar.value); // 'postgres://...'
console.log(dbVar.variableType); // 'env_var'
console.log(dbVar.protected); // true
console.log(dbVar.masked); // true
console.log(dbVar.environmentScope); // 'production'
```
## API Reference
---
### Pipelines 🚀
#### List & Filter Pipelines
```typescript
// Recent pipelines (auto-paginated)
const pipelines = await project.getPipelines();
// Advanced filtering
const failed = await project.getPipelines({
status: 'failed',
ref: 'main',
source: 'push',
orderBy: 'updated_at',
sort: 'desc',
updatedAfter: '2025-01-01T00:00:00Z',
});
for (const p of failed) {
console.log(`Pipeline #${p.id} [${p.status}] on ${p.ref}${p.webUrl}`);
}
```
#### Trigger a Pipeline
```typescript
const pipeline = await project.triggerPipeline('main', [
{ key: 'DEPLOY_ENV', value: 'staging' },
]);
console.log(`Triggered pipeline #${pipeline.id}`);
```
#### Pipeline Actions
```typescript
// Retry all failed jobs
const retried = await pipeline.retry();
// Cancel a running pipeline
const cancelled = await pipeline.cancel();
// Delete a pipeline
await pipeline.delete();
```
#### Pipeline Variables & Test Reports
```typescript
// Get variables used in a pipeline run
const pipelineVars = await pipeline.getVariables();
for (const v of pipelineVars) {
console.log(`${v.key} = ${v.value} (${v.variableType})`);
}
// Get the test report
const report = await pipeline.getTestReport();
console.log(`Tests: ${report.totalCount} total, ${report.successCount} passed, ${report.failedCount} failed`);
for (const suite of report.testSuites) {
console.log(` Suite "${suite.name}": ${suite.totalCount} tests (${suite.failedCount} failures)`);
for (const tc of suite.testCases) {
if (tc.status === 'failed') {
console.log(`${tc.name}: ${tc.systemOutput}`);
}
}
}
```
---
### Jobs ⚙️
```typescript
// Get all jobs for a pipeline
const jobs = await pipeline.getJobs();
// Filter by scope
const failedJobs = await pipeline.getJobs({ scope: ['failed'] });
for (const job of jobs) {
console.log(`Job "${job.name}" (${job.stage}) — ${job.status} [${job.duration}s]`);
}
```
#### Job Actions
```typescript
// Read the raw log output
const log = await job.getLog();
console.log(log);
// Retry a failed job
const retriedJob = await job.retry();
// Cancel a running job
const cancelledJob = await job.cancel();
// Trigger a manual job
const played = await job.play();
// Erase job artifacts and trace
await job.erase();
```
#### Job Properties
Each `GitLabJob` exposes rich metadata:
```typescript
job.id // number
job.name // 'build'
job.stage // 'test'
job.status // 'success' | 'failed' | 'running' | ...
job.ref // 'main'
job.tag // false
job.webUrl // full URL to the job
job.duration // seconds
job.queuedDuration // seconds waiting in queue
job.coverage // code coverage percentage
job.allowFailure // whether failure is acceptable
job.failureReason // reason for failure, if any
```
---
## API Reference 📚
### `GitLabClient`
| Method | Signature | Returns | Description |
|---|---|---|---|
| `constructor` | `(baseUrl: string, token: string)` | `GitLabClient` | Create a new client instance. |
| `testConnection` | `()` | `Promise<ITestConnectionResult>` | Verify the token and connectivity. |
| `getProjects` | `(opts?: IListOptions)` | `Promise<IGitLabProject[]>` | List projects you are a member of. |
| `getGroups` | `(opts?: IListOptions)` | `Promise<IGitLabGroup[]>` | List accessible groups. |
| `getProjectVariables` | `(projectId: number \| string)` | `Promise<IGitLabVariable[]>` | List all CI/CD variables for a project. |
| `createProjectVariable` | `(projectId: number \| string, key: string, value: string, opts?: IVariableOptions)` | `Promise<IGitLabVariable>` | Create a CI/CD variable on a project. |
| `updateProjectVariable` | `(projectId: number \| string, key: string, value: string, opts?: IVariableOptions)` | `Promise<IGitLabVariable>` | Update an existing project variable. |
| `deleteProjectVariable` | `(projectId: number \| string, key: string)` | `Promise<void>` | Delete a project variable. |
| `getGroupVariables` | `(groupId: number \| string)` | `Promise<IGitLabVariable[]>` | List all CI/CD variables for a group. |
| `createGroupVariable` | `(groupId: number \| string, key: string, value: string, opts?: IVariableOptions)` | `Promise<IGitLabVariable>` | Create a CI/CD variable on a group. |
| `updateGroupVariable` | `(groupId: number \| string, key: string, value: string, opts?: IVariableOptions)` | `Promise<IGitLabVariable>` | Update an existing group variable. |
| `deleteGroupVariable` | `(groupId: number \| string, key: string)` | `Promise<void>` | Delete a group variable. |
| `getPipelines` | `(projectId: number \| string, opts?: IListOptions)` | `Promise<IGitLabPipeline[]>` | List pipelines for a project, newest first. |
| `getPipelineJobs` | `(projectId: number \| string, pipelineId: number)` | `Promise<IGitLabJob[]>` | Get all jobs for a pipeline. |
| `getJobLog` | `(projectId: number \| string, jobId: number)` | `Promise<string>` | Retrieve the raw trace/log of a job. |
| `retryPipeline` | `(projectId: number \| string, pipelineId: number)` | `Promise<void>` | Retry all failed jobs in a pipeline. |
| `cancelPipeline` | `(projectId: number \| string, pipelineId: number)` | `Promise<void>` | Cancel a running pipeline. |
| Method | Returns | Description |
|---|---|---|
| `new GitLabClient(baseUrl, token)` | `GitLabClient` | Create a client instance |
| `.testConnection()` | `Promise<ITestConnectionResult>` | Verify token and connectivity |
| `.getGroups(opts?)` | `Promise<GitLabGroup[]>` | List all accessible groups (auto-paginated) |
| `.getGroup(fullPath)` | `Promise<GitLabGroup>` | Get a single group by full path |
| `.createGroup(name, path, parentId?)` | `Promise<GitLabGroup>` | Create a new group |
| `.getProjects(opts?)` | `Promise<GitLabProject[]>` | List your projects (auto-paginated) |
| `.getProject(idOrPath)` | `Promise<GitLabProject>` | Get a single project by ID or path |
| `.createProject(name, opts?)` | `Promise<GitLabProject>` | Create a new project |
## Types
### `GitLabGroup`
| Method | Returns | Description |
|---|---|---|
| `.getProjects(opts?)` | `Promise<GitLabProject[]>` | List projects in the group |
| `.getDescendantGroups(opts?)` | `Promise<GitLabGroup[]>` | List descendant sub-groups |
| `.getVariables()` | `Promise<GitLabVariable[]>` | List CI/CD variables |
| `.createVariable(key, value, opts?)` | `Promise<GitLabVariable>` | Create a CI/CD variable |
| `.updateVariable(key, value, opts?)` | `Promise<GitLabVariable>` | Update a CI/CD variable |
| `.deleteVariable(key)` | `Promise<void>` | Delete a CI/CD variable |
| `.update(data)` | `Promise<void>` | Update group properties |
| `.setAvatar(imageData, filename)` | `Promise<void>` | Upload avatar image |
| `.deleteAvatar()` | `Promise<void>` | Remove avatar |
| `.transfer(parentGroupId)` | `Promise<void>` | Transfer to another parent group |
| `.delete()` | `Promise<void>` | Delete the group |
| `.toJSON()` | `IGitLabGroup` | Serialize to raw interface |
### `GitLabProject`
| Method | Returns | Description |
|---|---|---|
| `.getBranches(opts?)` | `Promise<GitLabBranch[]>` | List repository branches |
| `.getTags(opts?)` | `Promise<GitLabTag[]>` | List repository tags |
| `.getProtectedBranches()` | `Promise<GitLabProtectedBranch[]>` | List protected branches |
| `.unprotectBranch(name)` | `Promise<void>` | Remove branch protection |
| `.getVariables()` | `Promise<GitLabVariable[]>` | List CI/CD variables |
| `.createVariable(key, value, opts?)` | `Promise<GitLabVariable>` | Create a CI/CD variable |
| `.updateVariable(key, value, opts?)` | `Promise<GitLabVariable>` | Update a CI/CD variable |
| `.deleteVariable(key)` | `Promise<void>` | Delete a CI/CD variable |
| `.getPipelines(opts?)` | `Promise<GitLabPipeline[]>` | List pipelines (with rich filtering) |
| `.triggerPipeline(ref, variables?)` | `Promise<GitLabPipeline>` | Trigger a new pipeline |
| `.update(data)` | `Promise<void>` | Update project properties |
| `.setAvatar(imageData, filename)` | `Promise<void>` | Upload avatar image |
| `.deleteAvatar()` | `Promise<void>` | Remove avatar |
| `.transfer(namespaceId)` | `Promise<void>` | Transfer to another namespace |
| `.delete()` | `Promise<void>` | Delete the project |
| `.toJSON()` | `IGitLabProject` | Serialize to raw interface |
### `GitLabPipeline`
| Method | Returns | Description |
|---|---|---|
| `.getJobs(opts?)` | `Promise<GitLabJob[]>` | List jobs (with scope filtering) |
| `.getVariables()` | `Promise<GitLabPipelineVariable[]>` | Get pipeline variables |
| `.getTestReport()` | `Promise<GitLabTestReport>` | Get the test report |
| `.retry()` | `Promise<GitLabPipeline>` | Retry failed jobs |
| `.cancel()` | `Promise<GitLabPipeline>` | Cancel the pipeline |
| `.delete()` | `Promise<void>` | Delete the pipeline |
| `.toJSON()` | `IGitLabPipeline` | Serialize to raw interface |
### `GitLabJob`
| Method | Returns | Description |
|---|---|---|
| `.getLog()` | `Promise<string>` | Get raw job trace/log |
| `.retry()` | `Promise<GitLabJob>` | Retry the job |
| `.cancel()` | `Promise<GitLabJob>` | Cancel the job |
| `.play()` | `Promise<GitLabJob>` | Trigger a manual job |
| `.erase()` | `Promise<void>` | Erase artifacts and trace |
| `.toJSON()` | `IGitLabJob` | Serialize to raw interface |
### Value Classes
| Class | Key Properties |
|---|---|
| `GitLabBranch` | `name`, `commitSha` |
| `GitLabTag` | `name`, `commitSha` |
| `GitLabProtectedBranch` | `id`, `name`, `allowForcePush` |
| `GitLabVariable` | `key`, `value`, `variableType`, `protected`, `masked`, `environmentScope` |
| `GitLabPipelineVariable` | `key`, `value`, `variableType` |
| `GitLabTestReport` | `totalTime`, `totalCount`, `successCount`, `failedCount`, `skippedCount`, `errorCount`, `testSuites` |
| `GitLabTestSuite` | `name`, `totalTime`, `totalCount`, `successCount`, `failedCount`, `skippedCount`, `errorCount`, `testCases` |
| `GitLabTestCase` | `status`, `name`, `classname`, `executionTime`, `systemOutput`, `stackTrace` |
---
## TypeScript Interfaces 🏗️
All raw GitLab API shapes are exported as TypeScript interfaces so you can use them in your own type definitions.
### `IListOptions`
Pagination and search options used by `getProjects`, `getGroups`, and `getPipelines`.
```typescript
interface IListOptions {
search?: string; // Filter results by keyword
page?: number; // Page number (default: 1)
perPage?: number; // Items per page (default: 50 for projects/groups, 30 for pipelines)
perPage?: number; // Items per page (default: 50)
}
```
### `IPipelineListOptions`
Extends `IListOptions` with pipeline-specific filters:
```typescript
interface IPipelineListOptions extends IListOptions {
status?: string; // Filter by pipeline status
ref?: string; // Filter by branch/tag ref
source?: string; // Filter by trigger source (push, web, schedule, api, ...)
scope?: string; // Filter by scope (running, pending, finished, branches, tags)
username?: string; // Filter by the triggering user
updatedAfter?: string; // ISO 8601 date — only pipelines updated after this
updatedBefore?: string; // ISO 8601 date — only pipelines updated before this
orderBy?: string; // Order by field (id, status, ref, updated_at, user_id)
sort?: string; // Sort direction (asc, desc)
}
```
### `IJobListOptions`
```typescript
interface IJobListOptions extends IListOptions {
scope?: string[]; // Filter by job scope(s): created, pending, running, failed, success, ...
}
```
### `IVariableOptions`
Options when creating or updating CI/CD variables.
```typescript
interface IVariableOptions {
protected?: boolean; // Only expose to protected branches/tags (default: false)
masked?: boolean; // Mask the value in job logs (default: false)
environment_scope?: string; // Environment scope (default: '*' for all environments)
protected?: boolean; // Only expose to protected branches/tags
masked?: boolean; // Mask the value in job logs
environment_scope?: string; // Environment scope (default: '*')
}
```
@@ -205,92 +575,27 @@ interface ITestConnectionResult {
}
```
### `IGitLabProject`
All raw data interfaces (`IGitLabProject`, `IGitLabGroup`, `IGitLabPipeline`, `IGitLabJob`, `IGitLabVariable`, `IGitLabBranch`, `IGitLabTag`, `IGitLabProtectedBranch`, `IGitLabPipelineVariable`, `IGitLabTestReport`, `IGitLabTestSuite`, `IGitLabTestCase`, `IGitLabUser`) are also exported for advanced use cases.
```typescript
interface IGitLabProject {
id: number;
name: string;
path_with_namespace: string;
description: string;
default_branch: string;
web_url: string;
visibility: string;
topics: string[];
last_activity_at: string;
}
```
---
### `IGitLabGroup`
## License and Legal Information
```typescript
interface IGitLabGroup {
id: number;
name: string;
full_path: string;
description: string;
web_url: string;
visibility: string;
}
```
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
### `IGitLabVariable`
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
```typescript
interface IGitLabVariable {
key: string;
value: string;
variable_type: string;
protected: boolean;
masked: boolean;
environment_scope: string;
}
```
### Trademarks
### `IGitLabPipeline`
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
```typescript
interface IGitLabPipeline {
id: number;
project_id: number;
status: string;
ref: string;
sha: string;
web_url: string;
duration: number;
created_at: string;
source: string;
}
```
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
### `IGitLabJob`
### Company Information
```typescript
interface IGitLabJob {
id: number;
name: string;
stage: string;
status: string;
duration: number;
}
```
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
### `IGitLabUser`
For any legal inquiries or further information, please contact us via email at hello@task.vc.
Returned internally by `testConnection` to verify credentials.
```typescript
interface IGitLabUser {
id: number;
username: string;
name: string;
email: string;
avatar_url: string;
web_url: string;
state: string;
}
```
## License
MIT
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View 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 },
};
}
}

View File

@@ -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
View 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
View 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: '',
};
}
}

View 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: '',
};
}
}

View 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,
};
}
}

View 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
View 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 },
};
}
}

View 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()),
};
}
}

View 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
View 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;
}

View File

@@ -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';