Aller au contenu

Pipelines et automatisation

Les pipelines forment la moitié déterministe de Control Center. Un agent décide quoi dire; un pipeline décide ce qui se passe ensuite, de la même façon chaque fois. Un pipeline est un DAG d’étapes qui peuvent lancer des agents, exécuter des scripts, créer un embranchement selon une condition, se déployer en éventail sur une collection et transmettre des données entre les étapes — avec persistance, reprise 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 intervention humaine.

Un modèle (PipelineDefinition) est le DAG déclaratif : les étapes, les entrées déclarées et la configuration. Il réside dans la base de données propre à l’espace de travail. Une exécution (PipelineRun) est l’instanciation de ce modèle : elle porte un magasin d’état modifiable, des totaux de coût et de jetons ainsi qu’un état par étape.

Un même modèle compte plusieurs 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 une reprise serait pire qu’un arrêt :

  • Une étape qui est 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’exemption est extras['awaitApproval'] == true — c’est la porte d’approbation partielle orchestration.awaitApproval. human.gate n’est pas exempté.
  • Une étape déclarée extras: {'idempotent': false} qui était en cours au moment de l’arrêt du serveur peut avoir déjà appliqué son effet de bord (une fusion squash, un nettoyage d’arbre de travail). La réexécuter pourrait appliquer l’effet deux fois; le moteur la met donc en échec et vous laisse décider de la nouvelle tentative.

Une étape comporte deux parties orthogonales, et c’est en les gardant distinctes que le moteur reste petit.

Le type (StepKind) indique au moteur quand une étape devient exécutable par rapport à ses étapes en amont — c’est la seule chose que l’ordonnanceur a besoin de savoir. Il y en a six : trigger, listen, join, router, forEach et terminal. Le corps (bodyKey) est le travail que l’étape accomplit une fois planifiée. Les corps sont des fermetures enregistrées par clé au démarrage du serveur, de sorte que plusieurs é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 grandir sans que personne ne touche à l’ordonnanceur. Voir Types d’étapes de pipeline pour le catalogue complet des types, des corps et de leur configuration.

Deux conséquences de la conception par registre valent la peine d’être connues avant de rédiger 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 en cours d’exécution. Les étapes trigger et terminal sont exemptées de cet avertissement : le déclencheur exécute quand même le corps sans effet pipeline.trigger pour 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 exigent une correspondance exacte et sont ignorées silencieusement dans le cas contraire. Seule 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 autorisations d’une conversation à laquelle elle s’est jointe. Toute autre variante de l’une ou l’autre clé est ignorée.

Les étapes ne s’appellent pas entre elles. Elles lisent et écrivent dans un magasin d’état partagé rattaché à l’exécution : une étape lit ses entrées par clé, s’exécute, puis réécrit sa sortie sous sa propre 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 magasin d’état constitue tout le récit de l’exécution.

Des lignes PipelineTrigger déclarent quand un modèle démarre automatiquement. Il 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 ou d’intervalle arrive à échéance
Manuel Vous l’exécutez à la main; c’est la présence d’un déclencheur manuel activé qui fait apparaître 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 types ont une correspondance de payload. Deux — TicketCreated et TicketStatusChanged — n’en ont aucune; un déclencheur posé 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 de 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 au moyen de dedupe_key. Comme les lignes de déclencheur sont uniques pour (espace de travail, type d’événement, modèle), un modèle peut détenir au plus un déclencheur de webhook et un déclencheur par type d’événement de domaine.

Les modèles intégrés amorcent leurs déclencheurs manuels et d’événement activés; les déclencheurs planifiés sont amorcés désactivés, de sorte qu’un balayage récurrent reste toujours optionnel.

Treize modèles de pipeline sont amorcé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 amorcés activés, mais PipelineEngine.start refuse un modèle désactivé et retourne 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 est l’interrupteur. Activez-le dans Paramètres → Espace de travail → Modèles de pipeline avant de vous attendre à ce que l’un de ces huit fasse quoi que ce soit.

Réamorcer un espace de travail préserve ce que vous avez choisi : votre réglage isEnabled et vos lignes de déclencheur existantes survivent. 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 désactivée auparavant, parce qu’il s’agit de la collecte des déchets. Sans elle, les lignes d’arbres de travail, les partitions de graphe de code et les copies par copie sur écriture s’accumulent, et cela n’a jamais été censé être un choix de l’utilisateur.

Une autre asymétrie vaut la peine d’être connue : le modèle intégré pr_review ne publie délibérément plus dans GitHub. Publier une révision est une étape distincte, soumise à l’approbation de l’utilisateur, et un pipeline qui publierait directement ferait une double publication. cross_review, ticket_to_pr et pr_triage publient toujours un commentaire directement dans GitHub lorsqu’ils s’exécutent.

PipelineEngine crée l’enregistrement d’exécution, planifie 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 du routeur, attend les jointures, applique les politiques de nouvelle tentative et le mode « continuer en cas d’échec », puis persiste l’état, les coûts et les erreurs à chaque étape. Les arêtes qui quittent un autre point de départ sont ignorées pour cette exécution, de sorte qu’un déclenchement planifié n’attend 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, à 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 pairs — 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 conservée dans l’état de l’exécution; sinon, l’exécution échoue. Une étape dotée d’un StepRetryPolicy est d’abord réessayée, avec un délai de reprise linéaire ou exponentiel, avant que l’un ou l’autre de ces comportements ne s’applique.