Ir al contenido

Pipelines y automatización

Los pipelines son la mitad determinista de Control Center. Un agente decide qué decir; un pipeline decide qué ocurre después, siempre de la misma forma. Un pipeline es un DAG de pasos que pueden despachar agentes, ejecutar scripts, ramificar según una condición, distribuirse en abanico sobre una colección y pasar datos entre pasos — persistente, reanudable y con coste calculado.

Existen por tres motivos: encadenar un proceso repetitivo de varios pasos, coordinar varios agentes en secuencia o en paralelo y reaccionar a algo que ha ocurrido sin que haya una persona presente.

Una plantilla (PipelineDefinition) es el DAG declarativo: pasos, entradas declaradas y configuración. Vive en la base de datos del propio espacio de trabajo. Una ejecución (PipelineRun) es una instancia de esa plantilla en marcha, que lleva una bolsa de estado mutable, totales de coste y de tokens y el estado de cada paso.

Una plantilla tiene muchas ejecuciones. Las ejecuciones sobreviven a un reinicio: el motor recarga las ejecuciones en curso y las retoma donde se quedaron.

Dos redes de seguridad hacen fallar deliberadamente toda la ejecución en lugar de reanudar un paso, porque reanudarlo sería peor que detenerse:

  • Un paso que lleva suspended más tiempo que suspendedStepTimeout (24 horas de forma predeterminada) está esperando tickets que nunca alcanzarán un estado terminal, así que se marca como fallido para liberar la ejecución. La excepción es extras['awaitApproval'] == true, que es la puerta de aprobación parcial de orchestration.awaitApproval. human.gate no está exento.
  • Un paso declarado extras: {'idempotent': false} que estaba en curso cuando se detuvo el servidor puede haber aplicado ya su efecto secundario (una fusión squash, la limpieza de un worktree). Volver a ejecutarlo podría aplicarlo por duplicado, así que el motor lo marca como fallido y te deja a ti el reintento.

Un paso tiene dos partes ortogonales, y mantenerlas separadas es lo que permite que el motor siga siendo pequeño.

El tipo (StepKind) indica al motor cuándo un paso pasa a ser ejecutable en relación con sus pasos anteriores: eso es lo único que necesita saber el planificador. Hay seis: trigger, listen, join, router, forEach y terminal. El cuerpo (bodyKey) es el trabajo que realiza el paso una vez programado. Los cuerpos son clausuras registradas por clave al arrancar el servidor, así que muchos pasos de tipos distintos pueden compartir un mismo cuerpo: por ejemplo, varios pasos listen que ejecutan todos conversation.promptAgent.

Como la semántica de planificación vive en seis tipos, el catálogo de comportamientos puede crecer sin que nadie toque el planificador. Consulta Tipos de pasos de pipeline para ver el catálogo completo de tipos, cuerpos y su configuración.

Conviene conocer dos consecuencias del diseño del registro antes de escribir una plantilla a mano:

  • Un paso cuyo bodyKey no está registrado genera un aviso al cargar la plantilla y después nunca se ejecuta. En tiempo de ejecución nada falla de forma ruidosa. Los pasos trigger y terminal están exentos de ese aviso: el disparador sigue ejecutando el cuerpo pipeline.trigger, que no hace nada, para que se activen los escuchadores posteriores; un paso terminal no tiene cuerpo y es un centinela para el planificador.
  • Las claves del mapa extras de un nodo deben coincidir exactamente; de lo contrario se ignoran en silencio. Solo extras['spaceId'] sitúa el trabajo de un paso de agente en una conversación existente. extras['mode'] (chat / plan / review) se lee en el nodo que abre la sala (messaging.createSpace, valor predeterminado review): un paso de agente posterior no puede volver a definir los permisos de una conversación a la que se ha unido. Cualquier otra grafía de cualquiera de las dos claves se ignora.

Los pasos no se llaman entre sí. Leen y escriben en una bolsa de estado compartida de la ejecución: un paso lee sus entradas por clave, se ejecuta y escribe su salida de nuevo bajo su propio outputKey. Un StateReducer fusiona las escrituras simultáneas cuando ramas paralelas apuntan a la misma clave, y un TemplateRenderer sustituye los marcadores {{...}} de prompts y scripts a partir de ese mismo estado.

Por eso un pipeline se puede inspeccionar a posteriori: la bolsa de estado es toda la historia de la ejecución.

Las filas de PipelineTrigger declaran cuándo se inicia una plantilla automáticamente. Hay cuatro tipos:

Disparador Inicia una ejecución cuando
Evento de dominio Se dispara un evento de dominio coincidente — por ejemplo, PullRequestPublished o TicketAssigned
Programación Vence una expresión cron/de intervalo
Manual Lo ejecutas a mano; la presencia de un disparador manual activo es lo que hace que la plantilla aparezca en el selector de ejecuciones
Webhook Un sistema externo hace POST a /webhooks/<token> en el puerto HTTP del servidor

Los disparadores de eventos son un conjunto cerrado: el despachador conoce 16 tipos de evento y los ofrece en el selector (EventPayloadMapper.knownEventTypes), y nada más puede iniciar un pipeline. Catorce de esos 16 tienen un mapeo de payload. Dos —TicketCreated y TicketStatusChanged— no lo tienen, así que un disparador sobre cualquiera de ellos queda activo y nunca se dispara. Consulta Eventos de dominio para ver la lista completa.

El token del webhook hace también de secreto HMAC: la cabecera X-Hub-Signature-256: sha256=<hex> de la petición se verifica contra HMAC-SHA256(body, token), las entregas no verificadas se rechazan y no se pueden reproducir, y los duplicados se descartan mediante dedupe_key. Como las filas de disparador son únicas por (espacio de trabajo, tipo de evento, plantilla), una plantilla puede tener como máximo un disparador de webhook y un disparador por cada tipo de evento de dominio.

Las plantillas integradas siembran sus disparadores manuales y de eventos activados; los disparadores programados se siembran desactivados, así que un barrido recurrente siempre es opcional.

Trece plantillas de pipeline se siembran en cada espacio de trabajo y ocho de ellas se entregan desactivadas: external_pr_welcome, cross_review, ticket_to_pr, pr_triage, pre_merge_gate, release_notes, dep_audit y pr_digest.

Esta es la parte que sorprende a la gente. Sus disparadores se siembran activados, pero PipelineEngine.start rechaza una plantilla desactivada y devuelve null, así que el disparador se lanza al vacío y no aparece ninguna ejecución. La plantilla, y no el disparador, es el interruptor. Actívala en Ajustes → Espacio de trabajo → Plantillas de pipeline antes de esperar que cualquiera de esas ocho haga algo.

Volver a sembrar un espacio de trabajo conserva lo que hayas elegido: tu ajuste isEnabled y tus filas de disparador existentes sobreviven. Hay exactamente una excepción deliberada: la programación diaria de pr_merged_cleanup se activa a la fuerza aunque antes estuviera desactivada, porque es recolección de basura. Sin ella se acumulan filas de worktree, particiones del grafo de código y copias en escritura, y eso nunca pretendió ser una decisión del usuario.

Otra asimetría que conviene conocer: la plantilla integrada pr_review ya no publica deliberadamente en GitHub. Publicar una revisión es un paso aparte que requiere la aprobación del usuario, y un pipeline que publicara directamente lo haría por duplicado. cross_review, ticket_to_pr y pr_triage sí siguen publicando un comentario directamente en GitHub cuando se ejecutan.

PipelineEngine crea el registro de la ejecución, programa el nodo de inicio que corresponde al tipo de evento de la ejecución y luego recorre el DAG: a medida que los pasos se completan, evalúa los disparadores posteriores, resuelve las ramas del enrutador, espera en los puntos de unión, aplica las políticas de reintento y de continuar en caso de fallo, y persiste el estado, el coste y los errores en cada paso. Las aristas que salen de otro inicio se ignoran para esa ejecución, así que un disparo por programación no espera a una condición exclusiva de la ejecución manual.

Lo que no sabe es nada sobre tickets, despacho, GitHub o mensajería. Su único punto de extensión es PipelineBodyRegistry. Los cuerpos registrados allí son los que contienen AgentDispatchPort, MessagingPort y sus hermanos; por eso añadir una capacidad a los pipelines nunca implica cambiar el planificador.

Un paso que falla registra su mensaje de error y pasa a failed. Si el paso declara continueOnFail, los pasos posteriores continúan y el error se guarda en el estado de la ejecución; de lo contrario, la ejecución falla. Un paso con una StepRetryPolicy se reintenta primero, con retroceso lineal o exponencial, antes de que se aplique cualquiera de las dos opciones anteriores.