Files
aiworker/docs/02-backend/gitea-integration.md
Hector Ros db71705842 Complete documentation for future sessions
- CLAUDE.md for AI agents to understand the codebase
- GITEA-GUIDE.md centralizes all Gitea operations (API, Registry, Auth)
- DEVELOPMENT-WORKFLOW.md explains complete dev process
- ROADMAP.md, NEXT-SESSION.md for planning
- QUICK-REFERENCE.md, TROUBLESHOOTING.md for daily use
- 40+ detailed docs in /docs folder
- Backend as submodule from Gitea

Everything documented for autonomous operation.

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
2026-01-20 00:37:19 +01:00

12 KiB

Integración con Gitea

Cliente de Gitea

// 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

// 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

// api/routes/webhooks.ts
import { Router } from 'express'
import { handleGiteaWebhook } from '../../services/gitea/webhooks'

const router = Router()

router.post('/gitea', handleGiteaWebhook)

export default router