import * as plugins from './gitea.plugins.js'; import { logger } from './gitea.logging.js'; import type { IGiteaUser, IGiteaRepository, IGiteaOrganization, IGiteaSecret, IGiteaBranch, IGiteaTag, IGiteaActionRun, IGiteaActionRunJob, ITestConnectionResult, IListOptions, IActionRunListOptions, } from './gitea.interfaces.js'; export class GiteaClient { private baseUrl: string; private token: string; constructor(baseUrl: string, token: string) { // Remove trailing slash if present this.baseUrl = baseUrl.replace(/\/+$/, ''); this.token = token; } // --------------------------------------------------------------------------- // HTTP helpers // --------------------------------------------------------------------------- private async request( method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, data?: any, customHeaders?: Record, ): Promise { const url = `${this.baseUrl}${path}`; let builder = plugins.smartrequest.SmartRequest.create() .url(url) .header('Authorization', `token ${this.token}`) .header('Content-Type', 'application/json'); if (customHeaders) { for (const [k, v] of Object.entries(customHeaders)) { builder = builder.header(k, v); } } if (data) { builder = builder.json(data); } let response: Awaited>; switch (method) { case 'GET': response = await builder.get(); break; case 'POST': response = await builder.post(); break; case 'PUT': response = await builder.put(); break; case 'DELETE': response = await builder.delete(); break; } if (!response.ok) { const errorText = await response.text(); throw new Error(`${method} ${path}: ${response.status} ${response.statusText} - ${errorText}`); } try { return await response.json() as T; } catch { return undefined as unknown as T; } } private async requestText( method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, ): Promise { const url = `${this.baseUrl}${path}`; let builder = plugins.smartrequest.SmartRequest.create() .url(url) .header('Authorization', `token ${this.token}`) .header('Accept', 'text/plain'); let response: Awaited>; switch (method) { case 'GET': response = await builder.get(); break; case 'POST': response = await builder.post(); break; case 'PUT': response = await builder.put(); break; case 'DELETE': response = await builder.delete(); break; } if (!response.ok) { const errorText = await response.text(); throw new Error(`${method} ${path}: ${response.status} ${response.statusText} - ${errorText}`); } return response.text(); } // --------------------------------------------------------------------------- // Connection // --------------------------------------------------------------------------- public async testConnection(): Promise { try { await this.request('GET', '/api/v1/user'); return { ok: true }; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : String(err) }; } } // --------------------------------------------------------------------------- // Repositories // --------------------------------------------------------------------------- public async getRepos(opts?: IListOptions): Promise { const page = opts?.page || 1; const limit = opts?.perPage || 50; let url = `/api/v1/repos/search?page=${page}&limit=${limit}&sort=updated`; if (opts?.search) { url += `&q=${encodeURIComponent(opts.search)}`; } const body = await this.request('GET', url); return body.data || body; } // --------------------------------------------------------------------------- // Organizations // --------------------------------------------------------------------------- public async getOrgs(opts?: IListOptions): Promise { const page = opts?.page || 1; const limit = opts?.perPage || 50; return this.request('GET', `/api/v1/orgs?page=${page}&limit=${limit}`); } /** * Get a single organization by name */ public async getOrg(orgName: string): Promise { return this.request('GET', `/api/v1/orgs/${encodeURIComponent(orgName)}`); } /** * List repositories within an organization */ public async getOrgRepos(orgName: string, opts?: IListOptions): Promise { const page = opts?.page || 1; const limit = opts?.perPage || 50; let url = `/api/v1/orgs/${encodeURIComponent(orgName)}/repos?page=${page}&limit=${limit}&sort=updated`; if (opts?.search) { url += `&q=${encodeURIComponent(opts.search)}`; } return this.request('GET', url); } /** * Create a new organization */ public async createOrg(name: string, opts?: { fullName?: string; description?: string; visibility?: string; }): Promise { return this.request('POST', '/api/v1/orgs', { username: name, full_name: opts?.fullName || name, description: opts?.description || '', visibility: opts?.visibility || 'public', }); } /** * Create a repository within an organization */ public async createOrgRepo(orgName: string, name: string, opts?: { description?: string; private?: boolean; }): Promise { return this.request('POST', `/api/v1/orgs/${encodeURIComponent(orgName)}/repos`, { name, description: opts?.description || '', private: opts?.private ?? true, }); } // --------------------------------------------------------------------------- // Repository Branches & Tags // --------------------------------------------------------------------------- public async getRepoBranches(ownerRepo: string, opts?: IListOptions): Promise { const page = opts?.page || 1; const limit = opts?.perPage || 50; return this.request( 'GET', `/api/v1/repos/${ownerRepo}/branches?page=${page}&limit=${limit}`, ); } public async getRepoTags(ownerRepo: string, opts?: IListOptions): Promise { const page = opts?.page || 1; const limit = opts?.perPage || 50; return this.request( 'GET', `/api/v1/repos/${ownerRepo}/tags?page=${page}&limit=${limit}`, ); } // --------------------------------------------------------------------------- // Repository Secrets // --------------------------------------------------------------------------- public async getRepoSecrets(ownerRepo: string): Promise { return this.request('GET', `/api/v1/repos/${ownerRepo}/actions/secrets`); } public async setRepoSecret(ownerRepo: string, key: string, value: string): Promise { await this.request('PUT', `/api/v1/repos/${ownerRepo}/actions/secrets/${key}`, { data: value }); } public async deleteRepoSecret(ownerRepo: string, key: string): Promise { await this.request('DELETE', `/api/v1/repos/${ownerRepo}/actions/secrets/${key}`); } // --------------------------------------------------------------------------- // Organization Secrets // --------------------------------------------------------------------------- public async getOrgSecrets(orgName: string): Promise { return this.request('GET', `/api/v1/orgs/${orgName}/actions/secrets`); } public async setOrgSecret(orgName: string, key: string, value: string): Promise { await this.request('PUT', `/api/v1/orgs/${orgName}/actions/secrets/${key}`, { data: value }); } public async deleteOrgSecret(orgName: string, key: string): Promise { await this.request('DELETE', `/api/v1/orgs/${orgName}/actions/secrets/${key}`); } // --------------------------------------------------------------------------- // Action Runs // --------------------------------------------------------------------------- /** * List action runs for a repository with optional filters. * Supports status, branch, event, actor filtering. */ public async getActionRuns(ownerRepo: string, opts?: IActionRunListOptions): Promise { const page = opts?.page || 1; const limit = opts?.perPage || 30; let url = `/api/v1/repos/${ownerRepo}/actions/runs?page=${page}&limit=${limit}`; if (opts?.status) url += `&status=${encodeURIComponent(opts.status)}`; if (opts?.branch) url += `&branch=${encodeURIComponent(opts.branch)}`; if (opts?.event) url += `&event=${encodeURIComponent(opts.event)}`; if (opts?.actor) url += `&actor=${encodeURIComponent(opts.actor)}`; const body = await this.request('GET', url); return body.workflow_runs || body; } /** * Get a single action run's full details. */ public async getActionRun(ownerRepo: string, runId: number): Promise { return this.request( 'GET', `/api/v1/repos/${ownerRepo}/actions/runs/${runId}`, ); } /** * List jobs for an action run. */ public async getActionRunJobs(ownerRepo: string, runId: number): Promise { const body = await this.request('GET', `/api/v1/repos/${ownerRepo}/actions/runs/${runId}/jobs`); return body.jobs || body; } /** * Get a job's raw log output. */ public async getJobLog(ownerRepo: string, jobId: number): Promise { return this.requestText('GET', `/api/v1/repos/${ownerRepo}/actions/jobs/${jobId}/logs`); } /** * Re-run an action run. */ public async rerunAction(ownerRepo: string, runId: number): Promise { await this.request('POST', `/api/v1/repos/${ownerRepo}/actions/runs/${runId}/rerun`); } /** * Cancel a running action run. */ public async cancelAction(ownerRepo: string, runId: number): Promise { await this.request('POST', `/api/v1/repos/${ownerRepo}/actions/runs/${runId}/cancel`); } /** * Dispatch a workflow (trigger manually). */ public async dispatchWorkflow( ownerRepo: string, workflowId: string, ref: string, inputs?: Record, ): Promise { await this.request( 'POST', `/api/v1/repos/${ownerRepo}/actions/workflows/${encodeURIComponent(workflowId)}/dispatches`, { ref, inputs: inputs || {} }, ); } // --------------------------------------------------------------------------- // Repository Deletion // --------------------------------------------------------------------------- public async deleteRepo(owner: string, repo: string): Promise { await this.request( 'DELETE', `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, ); } }