Saltar para o conteúdo

Pipelines e automatização

Os pipelines são a metade determinística do Control Center. Um agente decide o que dizer; um pipeline decide o que acontece a seguir, sempre da mesma forma. Um pipeline é um DAG de passos que podem despachar agentes, executar scripts, ramificar com base numa condição, distribuir-se em leque sobre uma coleção e passar dados entre passos — persistido, retomável e com custos calculados.

Existem por três razões: encadear um processo repetitivo de vários passos, coordenar vários agentes em sequência ou em paralelo e reagir a algo que aconteceu sem que estivesse presente uma pessoa.

Um modelo (PipelineDefinition) é o DAG declarativo: passos, entradas declaradas e configuração. Vive na base de dados do próprio espaço de trabalho. Uma execução (PipelineRun) é uma execução desse modelo, que transporta um saco de estado mutável, totais de custo e de tokens e o estado de cada passo.

Um modelo tem muitas execuções. As execuções sobrevivem a um reinício: o motor volta a carregar as execuções em curso e retoma-as onde pararam.

Duas redes de segurança fazem falhar deliberadamente toda a execução em vez de retomar um passo, porque retomar seria pior do que parar:

  • Um passo que está suspended há mais tempo do que suspendedStepTimeout (24 horas por predefinição) está à espera de tickets que nunca chegarão a um estado terminal, pelo que é marcado como falhado para libertar a execução. A exceção é extras['awaitApproval'] == true — esse é o portão de aprovação parcial de orchestration.awaitApproval. human.gate não está isento.
  • Um passo declarado extras: {'idempotent': false} que estava em curso quando o servidor parou pode já ter aplicado o seu efeito secundário (um squash merge, uma limpeza de worktree). Voltar a executá-lo poderia aplicá-lo em duplicado, pelo que o motor marca-o como falhado e deixa-lhe a nova tentativa.

Um passo tem duas partes ortogonais e mantê-las separadas é o que permite que o motor se mantenha pequeno.

O tipo (StepKind) indica ao motor quando um passo passa a ser executável em relação aos seus passos a montante — é a única coisa que o agendador precisa de saber. Existem seis: trigger, listen, join, router, forEach e terminal. O corpo (bodyKey) é o trabalho que o passo realiza depois de agendado. Os corpos são closures registadas por chave no arranque do servidor, pelo que muitos passos de tipos diferentes podem partilhar um corpo: por exemplo, vários passos listen a executar todos conversation.promptAgent.

Uma vez que a semântica de agendamento assenta em seis tipos, o catálogo de comportamentos pode crescer sem que ninguém toque no agendador. Consulte Tipos de passo de pipeline para o catálogo completo de tipos, corpos e respetiva configuração.

Vale a pena conhecer duas consequências do desenho do registo antes de criar um modelo à mão:

  • Um passo cujo bodyKey não está registado produz um aviso na carga do modelo e depois nunca é executado. Em tempo de execução, nada falha de forma ruidosa. Os passos trigger e terminal estão isentos desse aviso: o disparador continua a executar o corpo no-op pipeline.trigger para que os listeners a jusante sejam acionados; um passo terminal não tem corpo e é um sentinela do agendador.
  • As chaves do mapa extras de um nó têm de corresponder exatamente; caso contrário, são ignoradas em silêncio. Só extras['spaceId'] coloca o trabalho de um passo de agente numa conversa existente. extras['mode'] (chat / plan / review) é lido no nó que abre a sala (messaging.createSpace, predefinição review) — um passo de agente posterior não pode redefinir as permissões de uma conversa a que se juntou. Qualquer outra grafia de qualquer das chaves é ignorada.

Os passos não se chamam uns aos outros. Leem e escrevem num saco de estado partilhado da execução: um passo lê as suas entradas por chave, executa e volta a escrever a sua saída sob o seu próprio outputKey. Um StateReducer funde as escritas simultâneas quando ramos paralelos visam a mesma chave, e um TemplateRenderer substitui os marcadores {{...}} de prompts e scripts a partir desse mesmo estado.

É por isso que um pipeline é inspecionável a posteriori — o saco de estado é toda a história da execução.

As linhas de PipelineTrigger declaram quando um modelo arranca automaticamente. Existem quatro tipos:

Disparador Inicia uma execução quando
Evento de domínio Dispara um evento de domínio correspondente — por exemplo PullRequestPublished ou TicketAssigned
Agendamento Vence uma expressão cron/de intervalo
Manual Executa-o à mão; a presença de um disparador manual ativo é o que coloca o modelo no seletor de execuções
Webhook Um sistema externo faz um POST para /webhooks/<token> na porta HTTP do servidor

Os disparadores de eventos são um conjunto fechado: o despachante conhece 16 tipos de evento e oferece-os no seletor (EventPayloadMapper.knownEventTypes), e nada mais pode iniciar um pipeline. Catorze desses 16 têm um mapeamento de payload. Dois — TicketCreated e TicketStatusChanged — não têm nenhum, pelo que um disparador sobre qualquer um deles fica ativo e nunca dispara. Consulte Eventos de domínio para a lista completa.

O token do webhook funciona também como segredo HMAC: o cabeçalho X-Hub-Signature-256: sha256=<hex> do pedido é verificado em relação a HMAC-SHA256(body, token), as entregas não verificadas são rejeitadas e não podem ser reproduzidas, e os duplicados são descartados por dedupe_key. Uma vez que as linhas de disparador são únicas por (espaço de trabalho, tipo de evento, modelo), um modelo pode ter no máximo um disparador de webhook e um disparador por cada tipo de evento de domínio.

Os modelos incorporados são criados com os seus disparadores manuais e de eventos ativados; os disparadores agendados são criados desativados, pelo que uma varredura recorrente é sempre opcional.

São criados treze modelos de pipeline em cada espaço de trabalho e oito deles vêm desativados: 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. Os seus disparadores são criados ativados, mas PipelineEngine.start recusa um modelo desativado e devolve null — pelo que o disparador dispara para o vazio e não aparece nenhuma execução. O interruptor é o modelo, e não o disparador. Ative-o em Definições → Espaço de trabalho → Modelos de pipeline antes de esperar que qualquer desses oito faça seja o que for.

Voltar a criar os dados iniciais de um espaço de trabalho preserva o que escolheu: a sua definição isEnabled e as suas linhas de disparador existentes sobrevivem. Há exatamente uma exceção deliberada — o agendamento diário de pr_merged_cleanup é ativado à força mesmo que anteriormente estivesse desligado, porque se trata de recolha de lixo. Sem ele acumulam-se linhas de worktree, partições do grafo de código e cópias copy-on-write, e isso nunca pretendeu ser uma decisão do utilizador.

Vale a pena conhecer mais uma assimetria: o modelo incorporado pr_review já não publica deliberadamente no GitHub. Publicar uma revisão é um passo à parte, controlado pelo utilizador, e um pipeline que publicasse diretamente estaria a publicar em duplicado. Já cross_review, ticket_to_pr e pr_triage continuam a publicar um comentário diretamente no GitHub quando são executados.

O PipelineEngine cria o registo da execução, agenda o nó de início que corresponde ao tipo de evento da execução e depois percorre o DAG: à medida que os passos terminam, avalia os disparadores a jusante, resolve os ramos do router, aguarda os joins, aplica políticas de nova tentativa e continue-on-fail, e persiste o estado, o custo e os erros em cada passo. As arestas que partem de outro nó de início são ignoradas nessa execução, pelo que um disparo por agendamento não espera por uma condição exclusiva da execução manual.

Aquilo que não sabe é tudo o que diga respeito a tickets, despacho, GitHub ou mensagens. O seu único ponto de extensão é o PipelineBodyRegistry. Os corpos aí registados são os que contêm AgentDispatchPort, MessagingPort e congéneres — e é por isso que acrescentar uma capacidade aos pipelines nunca implica alterar o agendador.

Um passo que falha regista a sua mensagem de erro e passa a failed. Se o passo declarar continueOnFail, os passos a jusante prosseguem e o erro é guardado no estado da execução; caso contrário, a execução falha. Um passo com uma StepRetryPolicy é tentado novamente primeiro, com recuo linear ou exponencial, antes de se aplicar qualquer uma dessas situações.