Pular para o conteúdo

Pipelines e automação

Os pipelines são a metade determinística do Control Center. Um agente decide o que dizer; um pipeline decide o que acontece em seguida, sempre da mesma forma. Um pipeline é um DAG de etapas que podem despachar agentes, executar scripts, ramificar com base em uma condição, distribuir em paralelo por uma coleção e passar dados entre etapas — com persistência, retomável e com o custo contabilizado.

Eles existem por três motivos: encadear um processo repetitivo de várias etapas, coordenar vários agentes em sequência ou em paralelo e reagir a algo que aconteceu sem que houvesse uma pessoa presente.

Um modelo (PipelineDefinition) é o DAG declarativo: etapas, entradas declaradas e configuração. Ele fica no banco de dados do próprio espaço de trabalho. Uma execução (PipelineRun) é uma instância de execução desse modelo, que carrega um contêiner de estado mutável, totais de custo e de tokens e o status de cada etapa.

Um modelo tem muitas execuções. As execuções sobrevivem a uma reinicialização: o motor recarrega as execuções em andamento e as retoma de onde pararam.

Duas salvaguardas fazem a execução inteira falhar de propósito em vez de retomar uma etapa, porque retomar seria pior do que parar:

  • Uma etapa que está suspended há mais tempo que suspendedStepTimeout (24 horas por padrão) está esperando tickets que nunca chegarão a um estado terminal, então ela é marcada como falha para liberar a execução. A isenção é extras['awaitApproval'] == true — esse é o portão de aprovação parcial orchestration.awaitApproval. human.gate não tem isenção.
  • Uma etapa declarada com extras: {'idempotent': false} que estava em andamento quando o servidor parou pode já ter aplicado seu efeito colateral (um squash merge, uma limpeza de worktree). Executá-la de novo poderia aplicar o efeito em duplicidade, então o motor a marca como falha e deixa a nova tentativa para você.

Uma etapa tem duas partes ortogonais, e mantê-las separadas é o que permite que o motor continue pequeno.

O tipo (StepKind) informa ao motor quando uma etapa se torna executável em relação às suas etapas a montante — isso é a única coisa que o agendador precisa saber. São seis: trigger, listen, join, router, forEach e terminal. O corpo (bodyKey) é o trabalho que a etapa realiza depois de agendada. Os corpos são closures registradas por chave na inicialização do servidor, então muitas etapas de tipos diferentes podem compartilhar um mesmo corpo: várias etapas listen executando todas conversation.promptAgent, por exemplo.

Como a semântica de agendamento vive em seis tipos, o catálogo de comportamentos pode crescer sem que ninguém precise mexer no agendador. Consulte Tipos de etapa de pipeline para ver o catálogo completo de tipos, corpos e sua configuração.

Vale conhecer duas consequências do design de registro antes de escrever um modelo à mão:

  • Uma etapa cujo bodyKey não está registrado produz um aviso no carregamento do modelo e depois nunca é executada. Nada falha de forma ruidosa em tempo de execução. As etapas trigger e terminal são isentas desse aviso: o gatilho ainda executa o corpo pipeline.trigger, que não faz nada, para que as etapas de escuta a jusante sejam disparadas; uma etapa terminal não tem corpo e é uma sentinela do agendador.
  • As chaves do mapa extras de um nó exigem correspondência exata e, caso contrário, são ignoradas em silêncio. Somente extras['spaceId'] coloca o trabalho de uma etapa de agente em uma conversa existente. extras['mode'] (chat / plan / review) é lido no nó que abre a sala (messaging.createSpace, padrão review) — uma etapa de agente posterior não pode alterar as permissões de uma conversa à qual entrou. Qualquer outra grafia de qualquer uma das chaves é ignorada.

As etapas não chamam umas às outras. Elas leem e gravam em um contêiner de estado compartilhado da execução: uma etapa lê suas entradas por chave, executa e grava sua saída de volta sob sua própria outputKey. Um StateReducer mescla gravações simultâneas quando ramos paralelos miram a mesma chave, e um TemplateRenderer substitui os marcadores {{...}} em prompts e scripts a partir desse mesmo estado.

É por isso que um pipeline pode ser inspecionado depois do ocorrido — o contêiner de estado é toda a história da execução.

As linhas de PipelineTrigger declaram quando um modelo é iniciado automaticamente. Há quatro tipos:

Gatilho Inicia uma execução quando
Evento de domínio Um evento de domínio correspondente é disparado — por exemplo PullRequestPublished ou TicketAssigned
Agendamento Uma expressão cron/intervalo vence
Manual Você o executa à mão; a presença de um gatilho manual habilitado é o que coloca o modelo no seletor de execuções
Webhook Um sistema externo faz POST em /webhooks/<token> na porta HTTP do servidor

Os gatilhos de evento formam um conjunto fechado: 16 tipos de evento são conhecidos pelo despachante e oferecidos no seletor (EventPayloadMapper.knownEventTypes), e nada mais pode iniciar um pipeline. Quatorze desses 16 têm mapeamento de payload. Dois — TicketCreated e TicketStatusChanged — não têm nenhum, então um gatilho sobre qualquer um deles fica habilitado e nunca dispara. Consulte Eventos de domínio para ver a lista completa.

O token do webhook também funciona como o segredo HMAC: o cabeçalho X-Hub-Signature-256: sha256=<hex> da requisição é verificado com HMAC-SHA256(body, token), entregas não verificadas são rejeitadas e não podem ser reproduzidas, e as duplicatas são descartadas por dedupe_key. Como as linhas de gatilho são únicas em (espaço de trabalho, tipo de evento, modelo), um modelo pode ter no máximo um gatilho de webhook e um gatilho por tipo de evento de domínio.

Os modelos integrados semeiam seus gatilhos manuais e de evento habilitados; os gatilhos agendados são semeados desabilitados, então uma varredura recorrente é sempre opcional.

Treze modelos de pipeline são semeados em cada espaço de trabalho e oito deles vêm desabilitados: external_pr_welcome, cross_review, ticket_to_pr, pr_triage, pre_merge_gate, release_notes, dep_audit e pr_digest.

É esta a parte que surpreende as pessoas. Seus gatilhos são semeados habilitados, mas PipelineEngine.start recusa um modelo desabilitado e retorna null — então o gatilho dispara no vazio e nenhuma execução aparece. O interruptor é o modelo, não o gatilho. Habilite-o em Configurações → Espaço de trabalho → Modelos de pipeline antes de esperar que qualquer um desses oito faça alguma coisa.

Semear novamente um espaço de trabalho preserva o que você escolheu: sua configuração isEnabled e suas linhas de gatilho existentes sobrevivem. Há exatamente uma exceção deliberada — o agendamento diário do pr_merged_cleanup é habilitado à força mesmo que estivesse desativado antes, porque se trata de coleta de lixo. Linhas de worktree, partições do grafo de código e cópias copy-on-write se acumulam sem ele, e isso nunca foi pensado para ser uma escolha do usuário.

Mais uma assimetria que vale conhecer: o modelo integrado pr_review deliberadamente não publica mais no GitHub. Publicar uma revisão é uma etapa separada, sujeita à aprovação do usuário, e um pipeline que publicasse diretamente publicaria em duplicidade. Já cross_review, ticket_to_pr e pr_triage continuam publicando um comentário diretamente no GitHub quando são executados.

O PipelineEngine cria o registro da execução, agenda o nó inicial que corresponde ao tipo de evento da execução e então percorre o DAG: conforme as etapas são concluídas, ele avalia os gatilhos a jusante, resolve os ramos do roteador, espera nas junções, aplica políticas de nova tentativa e continue-on-fail, e persiste estado, custo e erros em cada etapa. As arestas que saem de outro início são ignoradas nessa execução, então um disparo por agendamento não espera por uma condição exclusiva do modo manual.

O que ele não sabe é nada sobre tickets, despacho, GitHub ou mensagens. Seu único ponto de extensão é o PipelineBodyRegistry. Os corpos registrados ali são o que contém AgentDispatchPort, MessagingPort e seus semelhantes — e é por isso que adicionar uma capacidade aos pipelines nunca significa mudar o agendador.

Uma etapa que falha registra sua mensagem de erro e passa para failed. Se a etapa declarar continueOnFail, as etapas a jusante prosseguem e o erro é guardado no estado da execução; caso contrário, a execução falha. Uma etapa com uma StepRetryPolicy é repetida primeiro, com espera linear ou exponencial, antes que qualquer um dos dois casos se aplique.