- 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>
460 lines
12 KiB
Markdown
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
|
|
```
|