import * as plugins from '../plugins.ts'; import type * as interfaces from '../../ts_interfaces/index.ts'; import { BaseProvider, type ITestConnectionResult, type IListOptions, type IPipelineListOptions } from './classes.baseprovider.ts'; /** * Gitea API v1 provider implementation */ export class GiteaProvider extends BaseProvider { private client: plugins.giteaClient.GiteaClient; constructor(connectionId: string, baseUrl: string, token: string, groupFilterId?: string) { super(connectionId, baseUrl, token, groupFilterId); this.client = new plugins.giteaClient.GiteaClient(baseUrl, token); } async testConnection(): Promise { return this.client.testConnection(); } async getProjects(opts?: IListOptions): Promise { const repos = this.groupFilterId ? await (await this.client.getOrg(this.groupFilterId)).getRepos(opts) : await this.client.getRepos(opts); return repos.map((r) => this.mapProject(r)); } async getGroups(opts?: IListOptions): Promise { if (this.groupFilterId) { const org = await this.client.getOrg(this.groupFilterId); return [this.mapGroup(org)]; } const orgs = await this.client.getOrgs(opts); return orgs.map((o) => this.mapGroup(o)); } async getGroupProjects(groupId: string, opts?: IListOptions): Promise { const org = await this.client.getOrg(groupId); const repos = await org.getRepos(opts); return repos.map((r) => this.mapProject(r)); } // --- Branches / Tags --- async getBranches(projectFullPath: string, opts?: IListOptions): Promise { const repo = await this.client.getRepo(projectFullPath); const branches = await repo.getBranches(opts); return branches.map((b) => ({ name: b.name, commitSha: b.commitSha })); } async getTags(projectFullPath: string, opts?: IListOptions): Promise { const repo = await this.client.getRepo(projectFullPath); const tags = await repo.getTags(opts); return tags.map((t) => ({ name: t.name, commitSha: t.commitSha })); } // --- Project Secrets --- async getProjectSecrets(projectId: string): Promise { const repo = await this.client.getRepo(projectId); const secrets = await repo.getSecrets(); return secrets.map((s) => this.mapSecret(s, 'project', projectId)); } async createProjectSecret( projectId: string, key: string, value: string, ): Promise { const repo = await this.client.getRepo(projectId); await repo.setSecret(key, value); return { key, value: '***', protected: false, masked: true, scope: 'project', scopeId: projectId, scopeName: projectId, connectionId: this.connectionId, environment: '*' }; } async updateProjectSecret( projectId: string, key: string, value: string, ): Promise { return this.createProjectSecret(projectId, key, value); } async deleteProjectSecret(projectId: string, key: string): Promise { const repo = await this.client.getRepo(projectId); await repo.deleteSecret(key); } // --- Group Secrets --- async getGroupSecrets(groupId: string): Promise { const org = await this.client.getOrg(groupId); const secrets = await org.getSecrets(); return secrets.map((s) => this.mapSecret(s, 'group', groupId)); } async createGroupSecret( groupId: string, key: string, value: string, ): Promise { const org = await this.client.getOrg(groupId); await org.setSecret(key, value); return { key, value: '***', protected: false, masked: true, scope: 'group', scopeId: groupId, scopeName: groupId, connectionId: this.connectionId, environment: '*' }; } async updateGroupSecret( groupId: string, key: string, value: string, ): Promise { return this.createGroupSecret(groupId, key, value); } async deleteGroupSecret(groupId: string, key: string): Promise { const org = await this.client.getOrg(groupId); await org.deleteSecret(key); } // --- Pipelines (Action Runs) --- async getPipelines( projectId: string, opts?: IPipelineListOptions, ): Promise { const repo = await this.client.getRepo(projectId); const runs = await repo.getActionRuns({ page: opts?.page, perPage: opts?.perPage, status: opts?.status, branch: opts?.ref, event: opts?.source, }); return runs.map((r) => this.mapPipeline(r, projectId)); } async getPipelineJobs( projectId: string, pipelineId: string, ): Promise { // Use the client's internal method directly to avoid an extra getRepo call const jobs = await this.client.requestGetActionRunJobs(projectId, Number(pipelineId)); return jobs.map((j) => { const resolvedStatus = plugins.giteaClient.resolveGiteaStatus(j.status, j.conclusion); return this.mapJob(resolvedStatus, j, pipelineId); }); } async getJobLog(projectId: string, jobId: string): Promise { return this.client.requestGetJobLog(projectId, Number(jobId)); } async retryPipeline(projectId: string, pipelineId: string): Promise { // Fetch the run to get its workflow path, then dispatch const run = await this.client.requestGetActionRun(projectId, Number(pipelineId)); const wfId = plugins.giteaClient.extractWorkflowIdFromPath(run.path); const ref = run.head_branch || 'main'; if (!wfId) { throw new Error(`Cannot retry: no workflow ID found in path "${run.path}"`); } await this.client.requestDispatchWorkflow(projectId, wfId, ref); } async cancelPipeline(_projectId: string, _pipelineId: string): Promise { throw new Error('Cancel is not supported by Gitea 1.25'); } // --- Mappers --- private mapProject(r: plugins.giteaClient.GiteaRepository): interfaces.data.IProject { return { id: r.fullName || String(r.id), name: r.name, fullPath: r.fullName, description: r.description, defaultBranch: r.defaultBranch, webUrl: r.htmlUrl, connectionId: this.connectionId, visibility: r.isPrivate ? 'private' : 'public', topics: r.topics, lastActivity: r.updatedAt, }; } private mapGroup(o: plugins.giteaClient.GiteaOrganization): interfaces.data.IGroup { return { id: o.name || String(o.id), name: o.name, fullPath: o.name, description: o.description, webUrl: `${this.baseUrl}/${o.name}`, connectionId: this.connectionId, visibility: o.visibility || 'public', projectCount: o.repoCount, }; } private mapSecret(s: plugins.giteaClient.GiteaSecret, scope: 'project' | 'group', scopeId: string): interfaces.data.ISecret { return { key: s.name, value: '***', protected: false, masked: true, scope, scopeId, scopeName: scopeId, connectionId: this.connectionId, environment: '*', }; } private mapPipeline(r: plugins.giteaClient.GiteaActionRun, projectId: string): interfaces.data.IPipeline { return { id: String(r.id), projectId, projectName: projectId, connectionId: this.connectionId, status: this.mapStatus(r.resolvedStatus), ref: r.ref, sha: r.headSha, webUrl: r.htmlUrl, duration: r.duration, createdAt: r.startedAt, source: r.event || 'push', }; } private mapJob(resolvedStatus: string, j: plugins.giteaClient.IGiteaActionRunJob, pipelineId: string): interfaces.data.IPipelineJob { return { id: String(j.id), pipelineId, name: j.name || '', stage: j.name || 'default', status: this.mapStatus(resolvedStatus), duration: plugins.giteaClient.computeDuration(j.started_at, j.completed_at), }; } private mapStatus(status: string): interfaces.data.TPipelineStatus { const map: Record = { success: 'success', completed: 'success', failure: 'failed', cancelled: 'canceled', waiting: 'waiting', running: 'running', queued: 'pending', in_progress: 'running', skipped: 'skipped', }; return map[status?.toLowerCase()] || 'pending'; } }