/**
* GitLab API Client
*
* Typed wrapper for GitLab CI/CD API endpoints.
*
* Token lookup order:
* 1. git config gitlab.token
* 2. GITLAB_PAT environment variable
*
* Project lookup:
* - GITLAB_PROJECT_ID environment variable (required)
*/
import https from 'https'
import { URL } from 'url'
import { execSync } from 'child_process'
/**
* Get GitLab token from git config or environment
*/
function getGitLabToken(): string | undefined {
// 1. Try git config
try {
const token = execSync('git config --get gitlab.token', { encoding: 'utf-8' }).trim()
if (token) return token
} catch {
// git config not set, continue
}
// 2. Try environment variables
return process.env.GITLAB_PAT || process.env.GPLAT
}
/**
* Get GitLab project ID from environment
*/
function getProjectId(): string {
const projectId = process.env.GITLAB_PROJECT_ID
if (!projectId) {
throw new Error(
'GITLAB_PROJECT_ID environment variable not set. ' +
'Set it to your GitLab project path (e.g., "myorg/myproject")'
)
}
return projectId
}
const API_BASE = process.env.GITLAB_API_URL || 'https://gitlab.com/api/v4'
export interface Pipeline {
id: number
iid: number
project_id: number
status: 'created' | 'waiting_for_resource' | 'preparing' | 'pending' | 'running' | 'success' | 'failed' | 'canceled' | 'skipped' | 'manual' | 'scheduled'
source: string
ref: string
sha: string
web_url: string
created_at: string
updated_at: string
started_at?: string
finished_at?: string
duration?: number
queued_duration?: number
}
export interface Job {
id: number
status: 'created' | 'pending' | 'running' | 'success' | 'failed' | 'canceled' | 'skipped' | 'manual'
stage: string
name: string
ref: string
tag: boolean
coverage?: number
allow_failure: boolean
created_at: string
started_at?: string
finished_at?: string
duration?: number
queued_duration?: number
user?: {
id: number
username: string
name: string
}
commit: {
id: string
short_id: string
title: string
author_name: string
author_email: string
created_at: string
}
pipeline: {
id: number
project_id: number
ref: string
sha: string
status: string
}
web_url: string
failure_reason?: string
}
/**
* Make authenticated GitLab API request
*/
async function gitlabRequest<T>(endpoint: string): Promise<T> {
const token = getGitLabToken()
if (!token) {
throw new Error(
'GitLab token not found. Set it via:\n' +
' git config --global gitlab.token "glpat-xxx"\n' +
'Or set GITLAB_PAT environment variable.\n' +
'Get a token at: https://gitlab.com/-/user_settings/personal_access_tokens'
)
}
const url = new URL(`${API_BASE}${endpoint}`)
return new Promise((resolve, reject) => {
const options = {
method: 'GET',
headers: {
'PRIVATE-TOKEN': token,
'Content-Type': 'application/json',
},
}
https.get(url, options, (res) => {
let data = ''
res.on('data', (chunk) => {
data += chunk
})
res.on('end', () => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(JSON.parse(data))
} catch (e) {
// If response is not JSON, return as is
resolve(data as unknown as T)
}
} else {
reject(new Error(`HTTP ${res.statusCode}: ${data}`))
}
})
}).on('error', reject)
})
}
/**
* List recent pipelines for the project
*/
export async function listPipelines(count = 10): Promise<Pipeline[]> {
const projectId = getProjectId()
return gitlabRequest<Pipeline[]>(
`/projects/${encodeURIComponent(projectId)}/pipelines?per_page=${count}`
)
}
/**
* Get a specific pipeline by ID
*/
export async function getPipeline(pipelineId: number | string): Promise<Pipeline> {
const projectId = getProjectId()
return gitlabRequest<Pipeline>(
`/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}`
)
}
/**
* List all jobs in a pipeline
*/
export async function listJobs(pipelineId: number | string): Promise<Job[]> {
const projectId = getProjectId()
return gitlabRequest<Job[]>(
`/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/jobs`
)
}
/**
* Get a specific job by ID
*/
export async function getJob(jobId: number | string): Promise<Job> {
const projectId = getProjectId()
return gitlabRequest<Job>(
`/projects/${encodeURIComponent(projectId)}/jobs/${jobId}`
)
}
/**
* Get job trace (logs) for a specific job
*/
export async function getJobTrace(jobId: number | string): Promise<string> {
const projectId = getProjectId()
return gitlabRequest<string>(
`/projects/${encodeURIComponent(projectId)}/jobs/${jobId}/trace`
)
}
/**
* Find a job by name in a pipeline
*/
export async function findJobByName(
pipelineId: number | string,
jobName: string
): Promise<Job | undefined> {
const jobs = await listJobs(pipelineId)
return jobs.find(j => j.name === jobName)
}
/**
* Get logs for a job by name or ID
*/
export async function getJobLogs(
pipelineId: number | string,
jobNameOrId: string | number
): Promise<{ job: Job; logs: string }> {
const jobs = await listJobs(pipelineId)
const job = jobs.find(
j => j.name === jobNameOrId || j.id.toString() === jobNameOrId.toString()
)
if (!job) {
throw new Error(`Job "${jobNameOrId}" not found in pipeline ${pipelineId}`)
}
const logs = await getJobTrace(job.id)
return { job, logs }
}
/**
* Get all failed jobs in recent pipelines
*/
export async function getRecentFailures(count = 5): Promise<{
pipeline: Pipeline
failedJobs: Job[]
}[]> {
const pipelines = await listPipelines(count * 2)
const failedPipelines = pipelines.filter(p => p.status === 'failed').slice(0, count)
const results = []
for (const pipeline of failedPipelines) {
const jobs = await listJobs(pipeline.id)
const failedJobs = jobs.filter(j => j.status === 'failed')
results.push({
pipeline,
failedJobs,
})
}
return results
}
/**
* GitLab project info (dynamic based on GITLAB_PROJECT_ID)
*/
export function getGitLabProject() {
const projectId = getProjectId()
const baseUrl = (process.env.GITLAB_URL || 'https://gitlab.com').replace(/\/$/, '')
return {
id: projectId,
url: `${baseUrl}/${projectId}`,
pipelinesUrl: `${baseUrl}/${projectId}/-/pipelines`,
}
}
// For backwards compatibility
export const GITLAB_PROJECT = {
get id() { return getProjectId() },
get url() { return getGitLabProject().url },
get pipelinesUrl() { return getGitLabProject().pipelinesUrl },
}