feat: outgoing webhooks
Owners and admins can point a project at other systems: a signed JSON POST (HMAC-SHA256 of the body) when tickets, comments, releases or sprints change, filtered by event or family. Deliveries are retried a couple of times, the last one is shown, and a ping can be sent from the settings. Private addresses are refused unless WEBHOOKS_ALLOW_PRIVATE is set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
| 📎 Adjuntos | Capturas, logs y vídeos en el ticket o bajo un comentario; arrastrar, elegir o pegar con Ctrl+V; miniaturas de las imágenes |
|
||||
| 🔗 Relaciones | `BLOCKS`, `DEPENDS_ON`, `TESTS`, `DUPLICATES`, `RELATES_TO`, también entre proyectos (FE ↔ BE ↔ QA) |
|
||||
| 🌿 Git | Webhook para GitHub, Gitea o GitLab: commits, ramas y pull requests que nombran un ticket (WEB-12) aparecen en su ficha |
|
||||
| 📡 Webhooks | Un POST firmado (HMAC-SHA256) a otro sistema cuando pasa algo: tickets, comentarios, releases, sprints; con prueba y último envío |
|
||||
| 🏠 Inicio | «Mi trabajo»: mis tickets en todos los proyectos, por categoría (QA fallidos primero), lo que espera QA, próximas releases e «Ir a…» por clave |
|
||||
| 📊 Seguimiento | Panel con contadores, flujo por estado, el equipo con sus tickets abiertos (enlaza a su tablero), feed de actividad e historial por ticket |
|
||||
| 📈 Estadísticas | Creados vs. resueltos, flujo acumulado, burnup de releases, tiempos de ciclo y entrega, tasa de fallo en QA y resueltos por persona |
|
||||
|
||||
@@ -167,6 +167,14 @@ stateDiagram-v2
|
||||
actor `system`); el mismo enlace solo vuelve a contar si cambia (una PR fusionada). Proyecto desconocido,
|
||||
integración apagada o firma mala responden igual: 403. El secreto vive en MongoDB (`git_integrations`), no en el
|
||||
event store.
|
||||
- **Webhooks salientes**: owners y admins dan de alta URLs (hasta 20 por proyecto) suscritas a eventos
|
||||
(`issue.created`, `issue.status_changed`, `comment.added`, `release.shipped`, `sprint.closed`… — la lista en
|
||||
`webhookEvents` / `GET /api/webhook-events`), a familias (`issue.*`) o a todo (`*`). Cada envío es un `POST` JSON con
|
||||
`event`, `deliveryId`, `occurredAt`, `actorId`, `project`, `issue` (si va de un ticket) y `data` (los campos del evento),
|
||||
y las cabeceras `X-TaskStream-Event`, `X-TaskStream-Delivery` y `X-TaskStream-Signature: sha256=<HMAC del cuerpo con el
|
||||
secreto>`. El secreto solo se muestra al crearlo. Se reintenta a los 2 y 10 s (no ante un 4xx), con 5 s de espera
|
||||
máxima; se guarda el último envío. Salen en segundo plano y no son duraderos: si la API se reinicia a mitad, ese
|
||||
envío se pierde. No llaman a direcciones privadas salvo con `WEBHOOKS_ALLOW_PRIVATE=true`.
|
||||
- **Límite WIP**: cada estado puede tener `wipLimit` (1-999) en la configuración del proyecto. Es un aviso, no un
|
||||
bloqueo: la API acepta pasarse y el tablero marca la columna.
|
||||
- **Sprints**: los planifica y gestiona quien administra el proyecto (owner o admin); mete tickets cualquier miembro.
|
||||
@@ -259,6 +267,7 @@ que funcionan también sin token.
|
||||
| `publicProjects` | Proyectos públicos (sin token) |
|
||||
| `project(idOrKey)` | Un proyecto (mío, o público) con su `settings`. Campos calculados: `board(releaseId, assigneeId, unassigned, area, types, priorities, labels, search, perColumn)`, `stats`, `analytics(days)`, `releases` |
|
||||
| `issues(filter, limit, offset)` | Búsqueda de tickets (estado, tipo, área, prioridad, persona, release, etiquetas, ticket padre, texto); `sort: RANK` para el orden del backlog |
|
||||
| `webhooks(projectId)`, `webhookEvents` | Webhooks salientes del proyecto (owners y admins) y los eventos a los que se pueden suscribir |
|
||||
| `gitIntegration(projectId)` | Si el webhook de Git está activo, su ruta y (para owners y admins) el secreto |
|
||||
| `sprints(projectId)`, `sprint(id)` | Sprints del proyecto (en curso, planificados, cerrados). Campos calculados: `progress`, `burndown`, `issues` |
|
||||
| `savedFilters(projectId)` | Mis vistas guardadas del tablero y las compartidas con el proyecto |
|
||||
@@ -276,6 +285,7 @@ Todas devuelven `CommandResult { id, key, version }`.
|
||||
| Proyectos | `createProject`, `updateProject`, `updateProjectSettings`, `deleteProject(confirmKey)`, `addProjectMember`, `changeProjectMemberRole`, `removeProjectMember` |
|
||||
| Tickets | `createIssue`, `updateIssue`, `changeIssueStatus`, `assignIssue`, `linkIssues`, `unlinkIssues`, `setIssueRelease`, `setIssueParent`, `rankIssue`, `addChecklistItem`, `updateChecklistItem`, `removeChecklistItem`, `watchIssue`, `unwatchIssue`, `removeAttachment` (subir: por REST) |
|
||||
| Comentarios | `addComment`, `editComment`, `deleteComment` |
|
||||
| Webhooks | `createWebhook` (devuelve el secreto), `updateWebhook`, `deleteWebhook`, `testWebhook` |
|
||||
| Git | `enableGitIntegration` (también para cambiar el secreto), `disableGitIntegration` |
|
||||
| Sprints | `createSprint`, `updateSprint`, `startSprint`, `closeSprint(carryOverToSprintId)`, `deleteSprint`, `setIssueSprint` |
|
||||
| Vistas | `saveFilter(input)` (devuelve el id), `deleteSavedFilter(id)` |
|
||||
@@ -489,6 +499,7 @@ Proyectos y tickets se pueden indicar por **id o por clave** (`WEB`, `WEB-12`).
|
||||
| Adjuntos | `POST /api/issues/{issue}/attachments` (multipart: `file` y opcional `commentId`), `GET/DELETE /api/issues/{issue}/attachments/{id}` |
|
||||
| Releases | `GET /api/me/releases` (próximas), `GET/POST /api/projects/{project}/releases`, `GET/PATCH /api/releases/{id}`, `PUT …/notes`, `GET …/notes/draft`, `…/progress`, `…/burnup`, `…/issues`, `POST …/publish`, `POST …/archive` |
|
||||
| Explorar | `GET /api/explore/projects` (proyectos públicos, sin token) |
|
||||
| Webhooks | `GET /api/webhook-events`, `GET/POST /api/projects/{project}/webhooks`, `PATCH/DELETE /api/outgoing-webhooks/{id}`, `POST …/test` |
|
||||
| Git | `GET/POST/DELETE /api/projects/{project}/git-integration`, `POST /api/webhooks/git/{project}` (lo llama el host de Git) |
|
||||
| Sistema | `GET /api/health`, `GET /api/auth/config`, `POST /api/auth/token`, `POST /api/webhooks/pireon` |
|
||||
|
||||
|
||||
@@ -70,34 +70,35 @@ donde los lee Compose.
|
||||
|
||||
## 7.2 Variables de entorno de la API
|
||||
|
||||
| Variable | Por defecto | Descripción |
|
||||
| ------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| `PORT` | `3000` | Puerto HTTP |
|
||||
| `NODE_ENV` | `development` | En `production` no se devuelven stacktraces |
|
||||
| `POSTGRES_URL` | — (obligatoria) | Event store y lookups |
|
||||
| `POSTGRES_SYNCHRONIZE` | `false` | Crea las tablas desde las entidades. Cómodo en desarrollo; en producción, migraciones |
|
||||
| `MONGO_URL` | — (obligatoria) | Read model |
|
||||
| `REDIS_URL` | — (obligatoria) | Stream, pub/sub y caché |
|
||||
| `EVENT_STREAM_KEY` | `task-stream:events` | Clave del Redis Stream |
|
||||
| `EVENT_STREAM_MAX_LENGTH` | `1000000` | Longitud aproximada que se conserva en el stream |
|
||||
| `EVENTS_BRIDGE_ENABLED` | `true` | Si esta instancia proyecta eventos hacia Mongo (ver escalado) |
|
||||
| `PIREON_BASE_URL` | `https://oauth.pireon.dev` | Servidor de identidad |
|
||||
| `PIREON_CLIENT_ID` / `PIREON_CLIENT_SECRET` | — | Cliente OAuth `authorization_code`: «Entrar con Pireon» y Swagger UI |
|
||||
| `PIREON_PASSWORD_CLIENT_ID` / `_SECRET` | el anterior | Cliente OAuth `password`: login con email y contraseña |
|
||||
| `PIREON_SCOPES` | `users:read` | Scopes que se piden al iniciar sesión |
|
||||
| `PIREON_PASSWORD_GRANT` | `true` | Permite el login con email y contraseña |
|
||||
| `PIREON_WEBHOOK_SECRET` | — | Activa y firma los webhooks de Pireon |
|
||||
| `PIREON_TIMEOUT_MS` | `5000` | Timeout de las llamadas a Pireon |
|
||||
| `AUTH_SESSION_TTL_SECONDS` | `60` | Cuánto se confía en un token validado antes de volver a preguntar a Pireon |
|
||||
| `GRAPHQL_PLAYGROUND` | `true` | GraphiQL e introspección |
|
||||
| `GRAPHQL_SCHEMA_FILE` | `apps/api/schema.gql` | Dónde escribir el SDL (vacío = no escribir; así está en Docker) |
|
||||
| `SWAGGER_ENABLED` | `true` | API REST documentada: Swagger UI en `/api/docs`, OpenAPI en `/api/docs-json` |
|
||||
| `OPENAPI_FILE` | `apps/api/openapi.json` | Dónde escribir el documento OpenAPI al arrancar (vacío = no escribir; así en Docker) |
|
||||
| `ATTACHMENTS_DIR` | `data/attachments` | Carpeta de los adjuntos de los tickets; en Docker, el volumen `attachments-data` |
|
||||
| `SMTP_URL` | — | Servidor de correo para las copias de las notificaciones (`smtps://user:pass@host`); sin él no se envía nada |
|
||||
| `MAIL_FROM` | `TaskStream <no-reply@pireon.dev>` | Remitente de esos correos |
|
||||
| `WEB_URL` | — | Dirección pública de la web, para los enlaces de los correos |
|
||||
| `ATTACHMENTS_MAX_MB` | `25` | Tamaño máximo de un adjunto (nginx admite hasta 30 MB por petición) |
|
||||
| Variable | Por defecto | Descripción |
|
||||
| ------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `PORT` | `3000` | Puerto HTTP |
|
||||
| `NODE_ENV` | `development` | En `production` no se devuelven stacktraces |
|
||||
| `POSTGRES_URL` | — (obligatoria) | Event store y lookups |
|
||||
| `POSTGRES_SYNCHRONIZE` | `false` | Crea las tablas desde las entidades. Cómodo en desarrollo; en producción, migraciones |
|
||||
| `MONGO_URL` | — (obligatoria) | Read model |
|
||||
| `REDIS_URL` | — (obligatoria) | Stream, pub/sub y caché |
|
||||
| `EVENT_STREAM_KEY` | `task-stream:events` | Clave del Redis Stream |
|
||||
| `EVENT_STREAM_MAX_LENGTH` | `1000000` | Longitud aproximada que se conserva en el stream |
|
||||
| `EVENTS_BRIDGE_ENABLED` | `true` | Si esta instancia proyecta eventos hacia Mongo (ver escalado) |
|
||||
| `PIREON_BASE_URL` | `https://oauth.pireon.dev` | Servidor de identidad |
|
||||
| `PIREON_CLIENT_ID` / `PIREON_CLIENT_SECRET` | — | Cliente OAuth `authorization_code`: «Entrar con Pireon» y Swagger UI |
|
||||
| `PIREON_PASSWORD_CLIENT_ID` / `_SECRET` | el anterior | Cliente OAuth `password`: login con email y contraseña |
|
||||
| `PIREON_SCOPES` | `users:read` | Scopes que se piden al iniciar sesión |
|
||||
| `PIREON_PASSWORD_GRANT` | `true` | Permite el login con email y contraseña |
|
||||
| `PIREON_WEBHOOK_SECRET` | — | Activa y firma los webhooks de Pireon |
|
||||
| `PIREON_TIMEOUT_MS` | `5000` | Timeout de las llamadas a Pireon |
|
||||
| `AUTH_SESSION_TTL_SECONDS` | `60` | Cuánto se confía en un token validado antes de volver a preguntar a Pireon |
|
||||
| `GRAPHQL_PLAYGROUND` | `true` | GraphiQL e introspección |
|
||||
| `GRAPHQL_SCHEMA_FILE` | `apps/api/schema.gql` | Dónde escribir el SDL (vacío = no escribir; así está en Docker) |
|
||||
| `SWAGGER_ENABLED` | `true` | API REST documentada: Swagger UI en `/api/docs`, OpenAPI en `/api/docs-json` |
|
||||
| `OPENAPI_FILE` | `apps/api/openapi.json` | Dónde escribir el documento OpenAPI al arrancar (vacío = no escribir; así en Docker) |
|
||||
| `ATTACHMENTS_DIR` | `data/attachments` | Carpeta de los adjuntos de los tickets; en Docker, el volumen `attachments-data` |
|
||||
| `SMTP_URL` | — | Servidor de correo para las copias de las notificaciones (`smtps://user:pass@host`); sin él no se envía nada |
|
||||
| `MAIL_FROM` | `TaskStream <no-reply@pireon.dev>` | Remitente de esos correos |
|
||||
| `WEB_URL` | — | Dirección pública de la web, para los enlaces de los correos |
|
||||
| `ATTACHMENTS_MAX_MB` | `25` | Tamaño máximo de un adjunto (nginx admite hasta 30 MB por petición) |
|
||||
| `WEBHOOKS_ALLOW_PRIVATE` | `false` | Deja que los webhooks salientes llamen a direcciones de esta máquina o de su red privada (apagado, un webhook podría alcanzar servicios internos) |
|
||||
|
||||
La web no tiene configuración de entorno: llama a la API en su mismo origen (`/api`, `/graphql`).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user