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
# Valor exacto que Gitea debe enviar en el header "Authorization".
# Configúralo igual en Gitea (Settings -> Webhooks -> Authorization header).
GITEA_AUTH_HEADER=changeme-secreto-A
# Exact value Gitea must send in the "Authorization" header.
# Configure the same value in Gitea (Settings -> Webhooks -> Authorization header).
GITEA_AUTH_HEADER=changeme-secret-A
# Secret configurado en el webhook de Gitea, usado para verificar la firma
# HMAC-SHA256 del header "X-Gitea-Signature".
GITEA_WEBHOOK_SECRET=changeme-secreto-gitea
# Secret configured on the Gitea webhook, used to verify the
# HMAC-SHA256 signature in the "X-Gitea-Signature" header.
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
# 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
# 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
# Timeout en milisegundos para la llamada a la API de Anthropic (evita que se
# quede colgada indefinidamente si no hay salida a internet)
# Timeout in milliseconds for the call to the Anthropic API (prevents it from
# hanging indefinitely if there's no outbound internet access)
FIRE_TIMEOUT_MS=10000

View File

@@ -1,66 +1,67 @@
# Gitea-Claude-Webhook-Proxy
Proxy en Node.js/Express que recibe webhooks de Gitea y los reenvía a la API
de Claude Code Routines, añadiendo los headers que Gitea no permite
configurar (`Authorization: Bearer <token>`, `anthropic-beta`,
Node.js/Express proxy that receives Gitea webhooks and forwards them to the
Claude Code Routines API, adding the headers Gitea can't be configured to
send itself (`Authorization: Bearer <token>`, `anthropic-beta`,
`anthropic-version`).
## Cómo funciona
## How it works
1. Gitea hace `POST` a `/gitea-webhook` con:
- Header `Authorization: <secreto-A>` (configurado a mano en Gitea).
- Header `X-Gitea-Signature`: HMAC-SHA256 del body crudo con el `Secret`
del webhook.
2. El proxy valida ambos (comparación constant-time) y responde 401 si no
coinciden.
3. Deduplica por `X-Gitea-Delivery` (TTL configurable) para no reenviar
reintentos de Gitea.
4. Construye un `text` legible a partir del payload (`push`, `pull_request`;
cualquier otro evento cae en un mensaje genérico) y responde `200` a
Gitea de inmediato.
5. En segundo plano, hace `POST` a
1. Gitea sends a `POST` to `/gitea-webhook` with:
- Header `Authorization: <secret-A>` (configured by hand in Gitea).
- Header `X-Gitea-Signature`: HMAC-SHA256 of the raw body using the
webhook's `Secret`.
2. The proxy validates both (constant-time comparison) and returns 401 if
either one doesn't match.
3. It deduplicates by `X-Gitea-Delivery` (configurable TTL) to avoid
reprocessing Gitea retries.
4. It builds a readable `text` from the payload (`push`, `pull_request`;
any other event falls back to a generic message) and responds `200` to
Gitea immediately.
5. In the background, it makes a `POST` to
`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)
sin tumbar el proceso.
with that `text` and logs the outcome (success, 401/429/5xx, or a
network failure) without crashing the process.
## Variables de entorno
## Environment variables
Ver [.env.example](.env.example). Copia el archivo y rellena los valores
reales (no se suben al repo, `.env` está en `.gitignore`):
See [.env.example](.env.example). Copy it and fill in the real values
(never committed — `.env` is in `.gitignore`):
```bash
cp .env.example .env
```
| Variable | Descripción |
| Variable | Description |
| --- | --- |
| `PORT` | Puerto donde escucha el proxy (por defecto `3000`). |
| `GITEA_AUTH_HEADER` | Valor esperado del header `Authorization` que envía Gitea. |
| `GITEA_WEBHOOK_SECRET` | Secret del webhook en Gitea, usado para verificar `X-Gitea-Signature`. |
| `ROUTINE_ID` | ID de la rutina de Claude Code (`trig_...`). |
| `ROUTINE_TOKEN` | Token de la rutina (`sk-ant-oat01-...`), se envía como `Bearer`. |
| `DEDUP_TTL_MS` | TTL en ms para el dedup en memoria de entregas (por defecto 300000 = 5 min). |
| `PORT` | Port the proxy listens on (defaults to `3000`). |
| `GITEA_AUTH_HEADER` | Expected value of the `Authorization` header Gitea sends. |
| `GITEA_WEBHOOK_SECRET` | Gitea webhook secret, used to verify `X-Gitea-Signature`. |
| `ROUTINE_ID` | Claude Code routine ID (`trig_...`). |
| `ROUTINE_TOKEN` | Routine token (`sk-ant-oat01-...`), sent as `Bearer`. |
| `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
npm install
cp .env.example .env # y rellena los valores reales
cp .env.example .env # then fill in the real values
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`
- Secret: el mismo valor que `GITEA_WEBHOOK_SECRET`
- Authorization header: el mismo valor que `GITEA_AUTH_HEADER`
- Eventos: `Push` y/o `Pull Request` (los demás eventos se reenvían con un
mensaje genérico)
- Secret: same value as `GITEA_WEBHOOK_SECRET`
- Authorization header: same value as `GITEA_AUTH_HEADER`
- Events: `Push` and/or `Pull Request` (any other event is forwarded with a
generic message)
## Docker
@@ -72,5 +73,5 @@ docker run -d --name 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`.

View File

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