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

460 lines
12 KiB
Markdown

# 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
```