Translate README, comments, and notification text to English

Covers the README, all code comments in server.js and .env.example,
diagnostic log messages, and the push/pull_request summaries sent to
the Claude Code routine, so the whole codebase and its output are
consistently in English.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 03:19:14 +02:00
parent 1886e512eb
commit e1eefd96e3
3 changed files with 72 additions and 70 deletions

View File

@@ -1,23 +1,23 @@
# Puerto en el que escucha el proxy (nginx hará proxy_pass hacia aquí) # Port the proxy listens on (nginx will proxy_pass to this)
PORT=3000 PORT=3000
# Valor exacto que Gitea debe enviar en el header "Authorization". # Exact value Gitea must send in the "Authorization" header.
# Configúralo igual en Gitea (Settings -> Webhooks -> Authorization header). # Configure the same value in Gitea (Settings -> Webhooks -> Authorization header).
GITEA_AUTH_HEADER=changeme-secreto-A GITEA_AUTH_HEADER=changeme-secret-A
# Secret configurado en el webhook de Gitea, usado para verificar la firma # Secret configured on the Gitea webhook, used to verify the
# HMAC-SHA256 del header "X-Gitea-Signature". # HMAC-SHA256 signature in the "X-Gitea-Signature" header.
GITEA_WEBHOOK_SECRET=changeme-secreto-gitea GITEA_WEBHOOK_SECRET=changeme-gitea-secret
# ID de la rutina de Claude Code (parte de la URL /routines/{ROUTINE_ID}/fire) # Claude Code routine ID (the {ROUTINE_ID} part of /routines/{ROUTINE_ID}/fire)
ROUTINE_ID=trig_xxxxxxxxxxxxxxxxxxxxxxxx ROUTINE_ID=trig_xxxxxxxxxxxxxxxxxxxxxxxx
# Token de la rutina (sk-ant-oat01-...), se envía como "Authorization: Bearer <token>" # Routine token (sk-ant-oat01-...), sent as "Authorization: Bearer <token>"
ROUTINE_TOKEN=sk-ant-oat01-changeme ROUTINE_TOKEN=sk-ant-oat01-changeme
# TTL en milisegundos para el dedup en memoria de entregas de Gitea (X-Gitea-Delivery) # TTL in milliseconds for the in-memory dedup of Gitea deliveries (X-Gitea-Delivery)
DEDUP_TTL_MS=300000 DEDUP_TTL_MS=300000
# Timeout en milisegundos para la llamada a la API de Anthropic (evita que se # Timeout in milliseconds for the call to the Anthropic API (prevents it from
# quede colgada indefinidamente si no hay salida a internet) # hanging indefinitely if there's no outbound internet access)
FIRE_TIMEOUT_MS=10000 FIRE_TIMEOUT_MS=10000

View File

@@ -1,66 +1,67 @@
# Gitea-Claude-Webhook-Proxy # Gitea-Claude-Webhook-Proxy
Proxy en Node.js/Express que recibe webhooks de Gitea y los reenvía a la API Node.js/Express proxy that receives Gitea webhooks and forwards them to the
de Claude Code Routines, añadiendo los headers que Gitea no permite Claude Code Routines API, adding the headers Gitea can't be configured to
configurar (`Authorization: Bearer <token>`, `anthropic-beta`, send itself (`Authorization: Bearer <token>`, `anthropic-beta`,
`anthropic-version`). `anthropic-version`).
## Cómo funciona ## How it works
1. Gitea hace `POST` a `/gitea-webhook` con: 1. Gitea sends a `POST` to `/gitea-webhook` with:
- Header `Authorization: <secreto-A>` (configurado a mano en Gitea). - Header `Authorization: <secret-A>` (configured by hand in Gitea).
- Header `X-Gitea-Signature`: HMAC-SHA256 del body crudo con el `Secret` - Header `X-Gitea-Signature`: HMAC-SHA256 of the raw body using the
del webhook. webhook's `Secret`.
2. El proxy valida ambos (comparación constant-time) y responde 401 si no 2. The proxy validates both (constant-time comparison) and returns 401 if
coinciden. either one doesn't match.
3. Deduplica por `X-Gitea-Delivery` (TTL configurable) para no reenviar 3. It deduplicates by `X-Gitea-Delivery` (configurable TTL) to avoid
reintentos de Gitea. reprocessing Gitea retries.
4. Construye un `text` legible a partir del payload (`push`, `pull_request`; 4. It builds a readable `text` from the payload (`push`, `pull_request`;
cualquier otro evento cae en un mensaje genérico) y responde `200` a any other event falls back to a generic message) and responds `200` to
Gitea de inmediato. Gitea immediately.
5. En segundo plano, hace `POST` a 5. In the background, it makes a `POST` to
`https://api.anthropic.com/v1/claude_code/routines/{ROUTINE_ID}/fire` `https://api.anthropic.com/v1/claude_code/routines/{ROUTINE_ID}/fire`
con el `text` y loguea el resultado (éxito, 401/429/5xx, o fallo de red) with that `text` and logs the outcome (success, 401/429/5xx, or a
sin tumbar el proceso. network failure) without crashing the process.
## Variables de entorno ## Environment variables
Ver [.env.example](.env.example). Copia el archivo y rellena los valores See [.env.example](.env.example). Copy it and fill in the real values
reales (no se suben al repo, `.env` está en `.gitignore`): (never committed — `.env` is in `.gitignore`):
```bash ```bash
cp .env.example .env cp .env.example .env
``` ```
| Variable | Descripción | | Variable | Description |
| --- | --- | | --- | --- |
| `PORT` | Puerto donde escucha el proxy (por defecto `3000`). | | `PORT` | Port the proxy listens on (defaults to `3000`). |
| `GITEA_AUTH_HEADER` | Valor esperado del header `Authorization` que envía Gitea. | | `GITEA_AUTH_HEADER` | Expected value of the `Authorization` header Gitea sends. |
| `GITEA_WEBHOOK_SECRET` | Secret del webhook en Gitea, usado para verificar `X-Gitea-Signature`. | | `GITEA_WEBHOOK_SECRET` | Gitea webhook secret, used to verify `X-Gitea-Signature`. |
| `ROUTINE_ID` | ID de la rutina de Claude Code (`trig_...`). | | `ROUTINE_ID` | Claude Code routine ID (`trig_...`). |
| `ROUTINE_TOKEN` | Token de la rutina (`sk-ant-oat01-...`), se envía como `Bearer`. | | `ROUTINE_TOKEN` | Routine token (`sk-ant-oat01-...`), sent as `Bearer`. |
| `DEDUP_TTL_MS` | TTL en ms para el dedup en memoria de entregas (por defecto 300000 = 5 min). | | `DEDUP_TTL_MS` | TTL in ms for the in-memory delivery dedup (defaults to 300000 = 5 min). |
| `FIRE_TIMEOUT_MS` | Timeout in ms for the call to Anthropic's API (defaults to 10000). |
## Ejecutar en local ## Running locally
```bash ```bash
npm install npm install
cp .env.example .env # y rellena los valores reales cp .env.example .env # then fill in the real values
node server.js node server.js
``` ```
Endpoint de salud: `GET /health` → `{"status":"ok"}`. Health check endpoint: `GET /health` → `{"status":"ok"}`.
## Configurar el webhook en Gitea ## Configuring the webhook in Gitea
En el repo/organización → Settings → Webhooks → Add webhook: On the repo/organization → Settings → Webhooks → Add webhook:
- URL: `https://tu-dominio/gitea-webhook` - URL: `https://your-domain/gitea-webhook`
- Content type: `application/json` - Content type: `application/json`
- Secret: el mismo valor que `GITEA_WEBHOOK_SECRET` - Secret: same value as `GITEA_WEBHOOK_SECRET`
- Authorization header: el mismo valor que `GITEA_AUTH_HEADER` - Authorization header: same value as `GITEA_AUTH_HEADER`
- Eventos: `Push` y/o `Pull Request` (los demás eventos se reenvían con un - Events: `Push` and/or `Pull Request` (any other event is forwarded with a
mensaje genérico) generic message)
## Docker ## Docker
@@ -72,5 +73,5 @@ docker run -d --name gitea-claude-webhook-proxy \
gitea-claude-webhook-proxy gitea-claude-webhook-proxy
``` ```
Pensado para ir detrás de un nginx existente haciendo `proxy_pass` hacia Meant to sit behind an existing nginx doing `proxy_pass` to
`http://127.0.0.1:3000/gitea-webhook`. `http://127.0.0.1:3000/gitea-webhook`.

View File

@@ -20,7 +20,7 @@ for (const [name, value] of Object.entries({
ROUTINE_TOKEN, ROUTINE_TOKEN,
})) { })) {
if (!value) { if (!value) {
console.error(JSON.stringify({ level: 'error', message: `Falta la variable de entorno ${name}` })); console.error(JSON.stringify({ level: 'error', message: `Missing environment variable ${name}` }));
process.exit(1); process.exit(1);
} }
} }
@@ -37,8 +37,9 @@ app.disable('x-powered-by');
function log(level, fields) { function log(level, fields) {
const line = { level, timestamp: new Date().toISOString(), ...fields }; const line = { level, timestamp: new Date().toISOString(), ...fields };
// Todo por stdout a propósito: así un solo `tail`/log driver ve info y // Everything goes to stdout on purpose: a single tail/log driver then sees
// error juntos, sin depender de que algo separe stdout/stderr aguas abajo. // info and error together, instead of relying on something downstream to
// split stdout/stderr back apart.
console.log(JSON.stringify(line)); console.log(JSON.stringify(line));
} }
@@ -63,7 +64,7 @@ function verifyGiteaSignature(rawBody, signatureHeader) {
return safeCompare(expected.toLowerCase(), String(signatureHeader).toLowerCase()); return safeCompare(expected.toLowerCase(), String(signatureHeader).toLowerCase());
} }
// Dedup de entregas de Gitea (por X-Gitea-Delivery) con TTL en memoria. // Dedup of Gitea deliveries (by X-Gitea-Delivery) with an in-memory TTL.
const seenDeliveries = new Map(); const seenDeliveries = new Map();
function isDuplicateDelivery(deliveryId) { function isDuplicateDelivery(deliveryId) {
@@ -89,35 +90,35 @@ function truncate(text) {
} }
function buildTextFromPush(payload) { function buildTextFromPush(payload) {
const repo = payload.repository?.full_name || 'repo desconocido'; const repo = payload.repository?.full_name || 'unknown repo';
const branch = (payload.ref || '').replace(/^refs\/heads\//, '') || 'rama desconocida'; const branch = (payload.ref || '').replace(/^refs\/heads\//, '') || 'unknown branch';
const pusher = payload.pusher?.full_name || payload.pusher?.username || payload.sender?.login || 'alguien'; const pusher = payload.pusher?.full_name || payload.pusher?.username || payload.sender?.login || 'someone';
const commits = Array.isArray(payload.commits) ? payload.commits : []; const commits = Array.isArray(payload.commits) ? payload.commits : [];
const lastCommit = commits[commits.length - 1]; const lastCommit = commits[commits.length - 1];
const message = lastCommit?.message?.split('\n')[0] || '(sin commits)'; const message = lastCommit?.message?.split('\n')[0] || '(no commits)';
const compareUrl = payload.compare_url || payload.repository?.html_url || ''; const compareUrl = payload.compare_url || payload.repository?.html_url || '';
return [ return [
`[${repo}] ${pusher} hizo push a "${branch}" (${commits.length} commit(s))`, `[${repo}] ${pusher} pushed to "${branch}" (${commits.length} commit(s))`,
`Último commit: ${message}`, `Latest commit: ${message}`,
compareUrl ? `Comparar: ${compareUrl}` : null, compareUrl ? `Compare: ${compareUrl}` : null,
] ]
.filter(Boolean) .filter(Boolean)
.join('\n'); .join('\n');
} }
function buildTextFromPullRequest(payload) { function buildTextFromPullRequest(payload) {
const repo = payload.repository?.full_name || 'repo desconocido'; const repo = payload.repository?.full_name || 'unknown repo';
const pr = payload.pull_request || {}; const pr = payload.pull_request || {};
const action = payload.action || 'actualizado'; const action = payload.action || 'updated';
const author = pr.user?.login || pr.user?.full_name || 'alguien'; const author = pr.user?.login || pr.user?.full_name || 'someone';
const title = pr.title || '(sin título)'; const title = pr.title || '(no title)';
const head = pr.head?.ref || '?'; const head = pr.head?.ref || '?';
const base = pr.base?.ref || '?'; const base = pr.base?.ref || '?';
const url = pr.html_url || payload.repository?.html_url || ''; const url = pr.html_url || payload.repository?.html_url || '';
return [ return [
`[${repo}] Pull request ${action} #${payload.number ?? pr.number ?? ''} por ${author}`, `[${repo}] Pull request ${action} #${payload.number ?? pr.number ?? ''} by ${author}`,
`${title}`, `${title}`,
`${head} -> ${base}`, `${head} -> ${base}`,
url ? `URL: ${url}` : null, url ? `URL: ${url}` : null,
@@ -136,8 +137,8 @@ function buildText(event, payload) {
text = buildTextFromPullRequest(payload); text = buildTextFromPullRequest(payload);
break; break;
default: { default: {
const repo = payload.repository?.full_name || 'repo desconocido'; const repo = payload.repository?.full_name || 'unknown repo';
text = `[${repo}] Evento de Gitea recibido: ${event}`; text = `[${repo}] Gitea event received: ${event}`;
} }
} }
return truncate(text); return truncate(text);
@@ -230,5 +231,5 @@ app.use((err, req, res, next) => {
const port = Number(PORT); const port = Number(PORT);
app.listen(port, () => { app.listen(port, () => {
log('info', { message: `Servidor escuchando en el puerto ${port}` }); log('info', { message: `Server listening on port ${port}` });
}); });