# Integración con Gitea ## Cliente de Gitea ```typescript // services/gitea/client.ts import axios, { AxiosInstance } from 'axios' import { logger } from '../../utils/logger' export interface GiteaConfig { url: string token: string owner: string } export class GiteaClient { private client: AxiosInstance private owner: string constructor(config?: GiteaConfig) { const url = config?.url || process.env.GITEA_URL! const token = config?.token || process.env.GITEA_TOKEN! this.owner = config?.owner || process.env.GITEA_OWNER! this.client = axios.create({ baseURL: `${url}/api/v1`, headers: { 'Authorization': `token ${token}`, 'Content-Type': 'application/json' }, timeout: 30000 }) // Log requests this.client.interceptors.request.use((config) => { logger.debug(`Gitea API: ${config.method?.toUpperCase()} ${config.url}`) return config }) // Handle errors this.client.interceptors.response.use( (response) => response, (error) => { logger.error('Gitea API Error:', { url: error.config?.url, status: error.response?.status, data: error.response?.data }) throw error } ) } // ============================================ // REPOSITORIES // ============================================ async createRepo(name: string, options: { description?: string private?: boolean autoInit?: boolean defaultBranch?: string } = {}) { const response = await this.client.post('/user/repos', { name, description: options.description || '', private: options.private !== false, auto_init: options.autoInit !== false, default_branch: options.defaultBranch || 'main', trust_model: 'default' }) logger.info(`Gitea: Created repo ${name}`) return response.data } async getRepo(owner: string, repo: string) { const response = await this.client.get(`/repos/${owner}/${repo}`) return response.data } async deleteRepo(owner: string, repo: string) { await this.client.delete(`/repos/${owner}/${repo}`) logger.info(`Gitea: Deleted repo ${owner}/${repo}`) } async listRepos(owner?: string) { const targetOwner = owner || this.owner const response = await this.client.get(`/users/${targetOwner}/repos`) return response.data } // ============================================ // BRANCHES // ============================================ async createBranch(owner: string, repo: string, branchName: string, fromBranch: string = 'main') { // Get reference commit const refResponse = await this.client.get( `/repos/${owner}/${repo}/git/refs/heads/${fromBranch}` ) const sha = refResponse.data.object.sha // Create new branch const response = await this.client.post( `/repos/${owner}/${repo}/git/refs`, { ref: `refs/heads/${branchName}`, sha } ) logger.info(`Gitea: Created branch ${branchName} from ${fromBranch}`) return response.data } async getBranch(owner: string, repo: string, branch: string) { const response = await this.client.get( `/repos/${owner}/${repo}/branches/${branch}` ) return response.data } async listBranches(owner: string, repo: string) { const response = await this.client.get( `/repos/${owner}/${repo}/branches` ) return response.data } async deleteBranch(owner: string, repo: string, branch: string) { await this.client.delete( `/repos/${owner}/${repo}/branches/${branch}` ) logger.info(`Gitea: Deleted branch ${branch}`) } // ============================================ // PULL REQUESTS // ============================================ async createPullRequest(owner: string, repo: string, data: { title: string body: string head: string base: string }) { const response = await this.client.post( `/repos/${owner}/${repo}/pulls`, { title: data.title, body: data.body, head: data.head, base: data.base } ) logger.info(`Gitea: Created PR #${response.data.number}`) return response.data } async getPullRequest(owner: string, repo: string, index: number) { const response = await this.client.get( `/repos/${owner}/${repo}/pulls/${index}` ) return response.data } async listPullRequests(owner: string, repo: string, state: 'open' | 'closed' | 'all' = 'open') { const response = await this.client.get( `/repos/${owner}/${repo}/pulls`, { params: { state } } ) return response.data } async mergePullRequest(owner: string, repo: string, index: number, method: 'merge' | 'rebase' | 'squash' = 'merge') { const response = await this.client.post( `/repos/${owner}/${repo}/pulls/${index}/merge`, { Do: method, MergeMessageField: '', MergeTitleField: '' } ) logger.info(`Gitea: Merged PR #${index}`) return response.data } async closePullRequest(owner: string, repo: string, index: number) { const response = await this.client.patch( `/repos/${owner}/${repo}/pulls/${index}`, { state: 'closed' } ) logger.info(`Gitea: Closed PR #${index}`) return response.data } // ============================================ // COMMITS // ============================================ async getCommit(owner: string, repo: string, sha: string) { const response = await this.client.get( `/repos/${owner}/${repo}/git/commits/${sha}` ) return response.data } async listCommits(owner: string, repo: string, options: { sha?: string path?: string page?: number limit?: number } = {}) { const response = await this.client.get( `/repos/${owner}/${repo}/commits`, { params: options } ) return response.data } // ============================================ // WEBHOOKS // ============================================ async createWebhook(owner: string, repo: string, config: { url: string contentType?: 'json' | 'form' secret?: string events?: string[] }) { const response = await this.client.post( `/repos/${owner}/${repo}/hooks`, { type: 'gitea', config: { url: config.url, content_type: config.contentType || 'json', secret: config.secret || '' }, events: config.events || ['push', 'pull_request'], active: true } ) logger.info(`Gitea: Created webhook for ${owner}/${repo}`) return response.data } async listWebhooks(owner: string, repo: string) { const response = await this.client.get( `/repos/${owner}/${repo}/hooks` ) return response.data } async deleteWebhook(owner: string, repo: string, hookId: number) { await this.client.delete( `/repos/${owner}/${repo}/hooks/${hookId}` ) logger.info(`Gitea: Deleted webhook ${hookId}`) } // ============================================ // FILES // ============================================ async getFileContents(owner: string, repo: string, filepath: string, ref: string = 'main') { const response = await this.client.get( `/repos/${owner}/${repo}/contents/${filepath}`, { params: { ref } } ) return response.data } async createOrUpdateFile(owner: string, repo: string, filepath: string, data: { content: string // base64 encoded message: string branch?: string sha?: string // for updates }) { const response = await this.client.post( `/repos/${owner}/${repo}/contents/${filepath}`, { content: data.content, message: data.message, branch: data.branch || 'main', sha: data.sha } ) logger.info(`Gitea: Updated file ${filepath}`) return response.data } // ============================================ // USERS // ============================================ async getCurrentUser() { const response = await this.client.get('/user') return response.data } async getUser(username: string) { const response = await this.client.get(`/users/${username}`) return response.data } // ============================================ // ORGANIZATIONS (if needed) // ============================================ async createOrg(name: string, options: { fullName?: string description?: string } = {}) { const response = await this.client.post('/orgs', { username: name, full_name: options.fullName || name, description: options.description || '' }) logger.info(`Gitea: Created org ${name}`) return response.data } } // Export singleton instance export const giteaClient = new GiteaClient() ``` ## Webhook Handler ```typescript // services/gitea/webhooks.ts import { Request, Response } from 'express' import crypto from 'crypto' import { logger } from '../../utils/logger' import { db } from '../../db/client' import { tasks } from '../../db/schema' import { eq } from 'drizzle-orm' import { emitWebSocketEvent } from '../../api/websocket/server' export async function handleGiteaWebhook(req: Request, res: Response) { const signature = req.headers['x-gitea-signature'] as string const event = req.headers['x-gitea-event'] as string const payload = req.body // Verify signature const secret = process.env.GITEA_WEBHOOK_SECRET || '' if (secret && signature) { const hmac = crypto.createHmac('sha256', secret) hmac.update(JSON.stringify(payload)) const calculatedSignature = hmac.digest('hex') if (signature !== calculatedSignature) { logger.warn('Gitea webhook: Invalid signature') return res.status(401).json({ error: 'Invalid signature' }) } } logger.info(`Gitea webhook: ${event}`, { repo: payload.repository?.full_name, ref: payload.ref }) try { switch (event) { case 'push': await handlePushEvent(payload) break case 'pull_request': await handlePullRequestEvent(payload) break default: logger.debug(`Unhandled webhook event: ${event}`) } res.status(200).json({ success: true }) } catch (error) { logger.error('Webhook handler error:', error) res.status(500).json({ error: 'Internal error' }) } } async function handlePushEvent(payload: any) { const branch = payload.ref.replace('refs/heads/', '') const commits = payload.commits || [] logger.info(`Push to ${branch}: ${commits.length} commits`) // Find task by branch name const task = await db.query.tasks.findFirst({ where: eq(tasks.branchName, branch) }) if (task) { emitWebSocketEvent('task:push', { taskId: task.id, branch, commitsCount: commits.length }) } } async function handlePullRequestEvent(payload: any) { const action = payload.action // opened, closed, reopened, edited, synchronized const prNumber = payload.pull_request.number const state = payload.pull_request.state logger.info(`PR #${prNumber}: ${action}`) // Find task by PR number const task = await db.query.tasks.findFirst({ where: eq(tasks.prNumber, prNumber) }) if (task) { if (action === 'closed' && payload.pull_request.merged) { // PR was merged await db.update(tasks) .set({ state: 'staging' }) .where(eq(tasks.id, task.id)) emitWebSocketEvent('task:merged', { taskId: task.id, prNumber }) } emitWebSocketEvent('task:pr_updated', { taskId: task.id, prNumber, action, state }) } } ``` ## Router para Webhooks ```typescript // api/routes/webhooks.ts import { Router } from 'express' import { handleGiteaWebhook } from '../../services/gitea/webhooks' const router = Router() router.post('/gitea', handleGiteaWebhook) export default router ```