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:
24
.env.example
24
.env.example
@@ -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
|
||||||
|
|||||||
79
README.md
79
README.md
@@ -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`.
|
||||||
|
|||||||
39
server.js
39
server.js
@@ -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}` });
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user