Aller au contenu

Pipelines et automatisation

Les pipelines sont la moitié déterministe de Control Center. Un agent décide quoi dire ; un pipeline décide ce qui se passe ensuite, toujours de la même façon. Un pipeline est un DAG d’étapes qui peuvent lancer des agents, exécuter des scripts, créer une branche selon une condition, parcourir une collection en parallèle et transmettre des données entre les étapes — avec persistance, reprise possible et suivi des coûts.

Ils existent pour trois raisons : enchaîner un processus répétitif à plusieurs étapes, coordonner plusieurs agents en séquence ou en parallèle et réagir à quelque chose qui s’est produit sans qu’un humain soit présent.

Un modèle (PipelineDefinition) est le DAG déclaratif : étapes, entrées déclarées et configuration. Il vit dans la base de données propre à l’espace de travail. Une exécution (PipelineRun) est un passage de ce modèle, qui porte un conteneur d’état modifiable, les totaux de coût et de jetons ainsi que le statut de chaque étape.

Un modèle compte de nombreuses exécutions. Les exécutions survivent à un redémarrage : le moteur recharge les exécutions en cours et les reprend là où elles se sont arrêtées.

Deux filets de sécurité font délibérément échouer toute l’exécution plutôt que de reprendre une étape, car reprendre serait pire que s’arrêter :

  • Une étape suspended depuis plus longtemps que suspendedStepTimeout (24 heures par défaut) attend des tickets qui n’atteindront jamais un état terminal : elle est donc mise en échec pour libérer l’exécution. L’exception est extras['awaitApproval'] == true — c’est le portail d’approbation partielle orchestration.awaitApproval. human.gate n’en bénéficie pas.
  • Une étape déclarée extras: {'idempotent': false} qui était en cours lorsque le serveur s’est arrêté peut avoir déjà appliqué son effet de bord (un squash merge, un nettoyage d’arbre de travail). La relancer pourrait l’appliquer deux fois : le moteur la met donc en échec et vous laisse décider d’une nouvelle tentative.

Une étape a deux parties orthogonales, et c’est en les gardant séparées que le moteur reste petit.

Le type (StepKind) indique au moteur quand une étape devient exécutable par rapport à ses étapes amont — c’est la seule chose que l’ordonnanceur ait besoin de savoir. Il en existe six : trigger, listen, join, router, forEach et terminal. Le corps (bodyKey) est le travail que l’étape accomplit une fois ordonnancée. Les corps sont des closures enregistrées par clé au démarrage du serveur, si bien que de nombreuses étapes de types différents peuvent partager un même corps : plusieurs étapes listen exécutant toutes conversation.promptAgent, par exemple.

Comme la sémantique d’ordonnancement tient dans six types, le catalogue des comportements peut s’enrichir sans que personne ne touche à l’ordonnanceur. Voir Étapes de pipeline pour le catalogue complet des types, des corps et de leur configuration.

Deux conséquences de la conception par registre méritent d’être connues avant d’écrire un modèle à la main :

  • Une étape dont le bodyKey n’est pas enregistré produit un avertissement au chargement du modèle puis ne s’exécute jamais. Rien n’échoue bruyamment à l’exécution. Les étapes trigger et terminal sont exemptées de cet avertissement : le déclencheur exécute quand même le corps pipeline.trigger qui ne fait rien, afin que les écouteurs en aval se déclenchent ; une étape terminal n’a pas de corps et sert de sentinelle à l’ordonnanceur.
  • Les clés de la table extras d’un nœud sont à correspondance exacte et silencieusement ignorées dans le cas contraire. Seul extras['spaceId'] place le travail d’une étape d’agent dans une conversation existante. extras['mode'] (chat / plan / review) est lu sur le nœud qui ouvre l’espace (messaging.createSpace, review par défaut) — une étape d’agent ultérieure ne peut pas modifier les permissions d’une conversation qu’elle a rejointe. Toute autre orthographe de l’une ou l’autre clé est ignorée.

Les étapes ne s’appellent pas entre elles. Elles lisent et écrivent dans un conteneur d’état partagé au niveau de l’exécution : une étape lit ses entrées par clé, s’exécute et réécrit sa sortie sous sa propre clé outputKey. Un StateReducer fusionne les écritures concurrentes lorsque des branches parallèles visent la même clé, et un TemplateRenderer remplace les espaces réservés {{...}} dans les prompts et les scripts à partir de ce même état.

C’est pourquoi un pipeline est inspectable après coup — le conteneur d’état raconte toute l’exécution.

Les lignes PipelineTrigger déclarent quand un modèle démarre automatiquement. Il en existe quatre types :

Déclencheur Démarre une exécution quand
Événement de domaine Un événement de domaine correspondant se produit — par exemple PullRequestPublished ou TicketAssigned
Planification Une expression cron/intervalle arrive à échéance
Manuel Vous le lancez à la main ; c’est la présence d’un déclencheur manuel activé qui fait figurer le modèle dans le sélecteur d’exécution
Webhook Un système externe envoie une requête POST à /webhooks/<token> sur le port HTTP du serveur

Les déclencheurs d’événement forment un ensemble fermé : 16 types d’événements sont connus du répartiteur et proposés dans le sélecteur (EventPayloadMapper.knownEventTypes), et rien d’autre ne peut démarrer un pipeline. Quatorze de ces 16 disposent d’une correspondance de charge utile. Deux — TicketCreated et TicketStatusChanged — n’en ont aucune : un déclencheur sur l’un ou l’autre reste donc activé sans jamais se déclencher. Voir Événements de domaine pour la liste complète.

Le jeton du webhook sert aussi de secret HMAC : l’en-tête X-Hub-Signature-256: sha256=<hex> de la requête est vérifié par rapport à HMAC-SHA256(body, token), les livraisons non vérifiées sont rejetées et ne peuvent pas être rejouées, et les doublons sont écartés par dedupe_key. Comme les lignes de déclencheur sont uniques sur (espace de travail, type d’événement, modèle), un modèle peut contenir au plus un déclencheur webhook et un déclencheur par type d’événement de domaine.

Les modèles intégrés sont initialisés avec leurs déclencheurs manuels et d’événement activés ; les déclencheurs planifiés sont initialisés désactivés, si bien qu’un balayage récurrent est toujours à activer explicitement.

Treize modèles de pipeline sont initialisés dans chaque espace de travail et huit d’entre eux sont livrés désactivés : external_pr_welcome, cross_review, ticket_to_pr, pr_triage, pre_merge_gate, release_notes, dep_audit et pr_digest.

C’est le point qui surprend. Leurs déclencheurs sont initialisés activés, mais PipelineEngine.start refuse un modèle désactivé et renvoie null — le déclencheur se déclenche donc dans le vide et aucune exécution n’apparaît. C’est le modèle, et non le déclencheur, qui fait office d’interrupteur. Activez-le dans Paramètres → Espace de travail → Modèles de pipeline avant d’attendre que l’un de ces huit fasse quoi que ce soit.

Réinitialiser un espace de travail préserve ce que vous avez choisi : votre réglage isEnabled et vos lignes de déclencheur existantes sont conservés. Il existe exactement une exception délibérée — la planification quotidienne de pr_merged_cleanup est activée de force, même si elle était auparavant désactivée, car elle fait office de ramasse-miettes. Sans elle, les lignes d’arbre de travail, les partitions du graphe de code et les copies sur écriture s’accumulent, et cela n’a jamais été prévu comme un choix de l’utilisateur.

Une autre asymétrie mérite d’être connue : le modèle intégré pr_review ne publie délibérément plus sur GitHub. Publier une revue est une étape distincte, soumise à validation par l’utilisateur, et un pipeline qui publierait directement entraînerait une double publication. cross_review, ticket_to_pr et pr_triage, eux, publient toujours un commentaire directement sur GitHub lorsqu’ils s’exécutent.

PipelineEngine crée l’enregistrement d’exécution, ordonnance le nœud de départ correspondant au type d’événement de l’exécution, puis parcourt le DAG : à mesure que les étapes se terminent, il évalue les déclencheurs en aval, résout les branches des routeurs, attend les jointures, applique les politiques de nouvelle tentative et le continue-on-fail, et persiste l’état, le coût et les erreurs à chaque étape. Les arêtes issues d’un autre point de départ sont ignorées pour cette exécution : un déclenchement planifié n’attend donc pas une condition réservée au mode manuel.

Ce qu’il ne connaît pas, c’est tout ce qui touche aux tickets, au lancement d’agents, à GitHub ou à la messagerie. Son seul point d’extension est le PipelineBodyRegistry. Ce sont les corps qui y sont enregistrés qui détiennent AgentDispatchPort, MessagingPort et leurs homologues — c’est pourquoi ajouter une capacité aux pipelines n’oblige jamais à modifier l’ordonnanceur.

Une étape qui échoue enregistre son message d’erreur et passe à failed. Si l’étape déclare continueOnFail, les étapes en aval se poursuivent et l’erreur est rangée dans l’état de l’exécution ; sinon, l’exécution échoue. Une étape dotée d’une StepRetryPolicy est d’abord relancée, avec un délai d’attente linéaire ou exponentiel, avant que l’un ou l’autre de ces cas ne s’applique.