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 manera. Un pipeline es un DAG de pasos que puede despachar agentes, ejecutar scripts, bifurcar según una condición, ramificarse en abanico sobre una colección y pasar datos entre pasos: persistido, reanudable y con costos contabilizados.

Existen por tres razones: encadenar un proceso repetitivo de varios pasos, coordinar varios agentes en secuencia o en paralelo y reaccionar ante algo que ocurrió sin que hubiera 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 en marcha de esa plantilla, que conlleva una bolsa de estado mutable, totales de costo y de tokens, y el estado de cada paso.

Una plantilla tiene muchas ejecuciones. Las ejecuciones sobreviven a un reinicio: el motor vuelve a cargar las que están en curso y las retoma donde se detuvieron.

Dos salvaguardas hacen fallar deliberadamente toda la ejecución en lugar de reanudar un paso, porque reanudar 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 orchestration.awaitApproval. human.gate no queda exento.
  • Un paso declarado como extras: {'idempotent': false} que estaba en curso cuando el servidor se detuvo puede que ya haya aplicado su efecto secundario (una fusión squash, una limpieza de worktree). Volver a ejecutarlo podría aplicarlo dos veces, 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) le dice al motor cuándo un paso puede ejecutarse en relación con los pasos previos; eso es lo único que el planificador necesita saber. Hay seis: trigger, listen, join, router, forEach y terminal. El cuerpo (bodyKey) es el trabajo que realiza el paso una vez que se programa. Los cuerpos son clausuras registradas por clave al iniciar el servidor, así que muchos pasos de tipos distintos pueden compartir un mismo cuerpo: varios pasos listen que ejecutan conversation.promptAgent, por ejemplo.

Como la semántica de programació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.

Vale la pena conocer dos consecuencias del diseño del registro antes de crear una plantilla a mano:

  • Un paso cuyo bodyKey no está registrado genera una advertencia al cargar la plantilla y después nunca se ejecuta. Nada falla de forma ruidosa en tiempo de ejecución. Los pasos trigger y terminal están exentos de esa advertencia: el paso trigger igualmente ejecuta el cuerpo pipeline.trigger, que no hace nada, para que se activen los pasos de escucha posteriores; un paso terminal no tiene cuerpo y es un centinela del planificador.
  • Las claves del mapa extras de un nodo se comparan por coincidencia exacta y, en caso contrario, se ignoran en silencio. Solo extras['spaceId'] coloca 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, review de forma predeterminada); un paso de agente posterior no puede volver a definir los permisos de una conversación a la que se unió. Cualquier otra forma de escribir 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 vuelve a escribir su salida bajo su propio outputKey. Un StateReducer combina las escrituras concurrentes cuando ramas paralelas apuntan a la misma clave, y un TemplateRenderer sustituye los marcadores {{...}} en prompts y scripts a partir de ese mismo estado.

Por eso un pipeline se puede inspeccionar después de los hechos: la bolsa de estado es toda la historia de la ejecución.

Las filas de PipelineTrigger declaran cuándo se inicia automáticamente una plantilla. 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 de cron o de intervalo
Manual La ejecutas a mano; la presencia de un disparador manual habilitado es lo que pone la plantilla en el selector de ejecuciones
Webhook Un sistema externo hace un 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 carga útil. Dos —TicketCreated y TicketStatusChanged— no tienen ninguno, así que un disparador sobre cualquiera de los dos queda habilitado y nunca se dispara. Consulta Eventos de dominio para ver la lista completa.

El token del webhook también hace las veces de secreto HMAC: el encabezado X-Hub-Signature-256: sha256=<hex> de la solicitud se verifica contra HMAC-SHA256(body, token), las entregas sin verificar se rechazan y no se pueden reproducir, y los duplicados se descartan mediante dedupe_key. Como las filas de disparadores 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 se crean con sus disparadores manuales y de eventos habilitados; los disparadores programados se crean deshabilitados, así que un barrido recurrente siempre es opcional.

Las plantillas que se entregan deshabilitadas

Sección titulada «Las plantillas que se entregan deshabilitadas»

Se siembran trece plantillas de pipeline en cada espacio de trabajo y ocho de ellas se entregan deshabilitadas: 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 crean habilitados, pero PipelineEngine.start rechaza una plantilla deshabilitada y devuelve null, así que el disparador se activa hacia la nada y no aparece ninguna ejecución. La plantilla, no el disparador, es el interruptor. Habilítala en Ajustes → Espacio de trabajo → Plantillas de pipeline antes de esperar que alguna de esas ocho haga algo.

Volver a sembrar un espacio de trabajo conserva lo que hayas elegido: tu configuración de isEnabled y tus filas de disparadores existentes se mantienen. Hay exactamente una excepción deliberada: la programación diaria de pr_merged_cleanup se habilita a la fuerza aunque antes estuviera desactivada, porque es recolección de basura. Las filas de worktree, las particiones del grafo de código y las copias copy-on-write se acumulan sin ella, y eso nunca debió ser una decisión del usuario.

Otra asimetría que vale la pena conocer: la plantilla integrada pr_review deliberadamente ya no publica en GitHub. Publicar una revisión es un paso aparte que requiere aprobación del usuario, y un pipeline que publicara directamente duplicaría la publicación. 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 bifurcaciones del router, espera en las uniones, aplica políticas de reintento y de continuar en caso de fallo, y persiste el estado, el costo y los errores en cada paso. Las aristas que salen de otro inicio se ignoran en esa ejecución, así que el disparo de una programación no espera una condición que solo es manual.

Lo que no sabe es nada sobre tickets, despacho, GitHub ni mensajería. Su única costura de extensión es el PipelineBodyRegistry. Los cuerpos registrados ahí son los que contienen AgentDispatchPort, MessagingPort y sus similares; por eso agregar 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 los dos casos anteriores.