Sari la conținut

Pipeline-uri și automatizare

Pipeline-urile sunt jumătatea deterministă a Control Center. Un agent decide ce spune; un pipeline decide ce se întâmplă în continuare, la fel de fiecare dată. Un pipeline este un DAG de pași care pot trimite agenți, pot rula scripturi, pot ramifica după o condiție, se pot desfășura în paralel peste o colecție și pot transmite date între pași — persistat, reluabil și cu costuri calculate.

Există din trei motive: să înlănțuie un proces repetitiv cu mai mulți pași, să coordoneze mai mulți agenți în secvență sau în paralel și să reacționeze la ceva ce s-a întâmplat fără ca un om să fie acolo.

Un șablon (PipelineDefinition) este DAG-ul declarativ: pași, intrări declarate și configurație. El trăiește în baza de date proprie a spațiului de lucru. O rulare (PipelineRun) este o singură execuție a acelui șablon, care duce cu ea un sac de stare mutabil, totaluri de cost și de tokenuri și starea fiecărui pas.

Un șablon are multe rulări. Rulările supraviețuiesc unei reporniri: motorul reîncarcă rulările în curs și le reia de unde s-au oprit.

Două plase de siguranță eșuează deliberat întreaga rulare în loc să reia un pas, pentru că reluarea ar fi mai rea decât oprirea:

  • Un pas care a fost suspended mai mult de suspendedStepTimeout (24 de ore implicit) așteaptă tichete care nu vor ajunge niciodată într-o stare finală, așa că este marcat ca eșuat pentru a elibera rularea. Excepția este extras['awaitApproval'] == true — aceea este poarta de aprobare parțială orchestration.awaitApproval. human.gate nu face excepție.
  • Un pas declarat extras: {'idempotent': false} care era în curs când serverul s-a oprit poate să fi aplicat deja efectul secundar (un squash merge, o curățare de worktree). Reluarea lui ar putea aplica efectul de două ori, așa că motorul îl marchează ca eșuat și îți lasă reluarea ție.

Un pas are două părți ortogonale, iar faptul de a le ține separate este ceea ce permite motorului să rămână mic.

Tipul (StepKind) îi spune motorului când poate rula un pas în raport cu pașii din amonte — acesta este singurul lucru pe care trebuie să îl știe planificatorul. Există șase: trigger, listen, join, router, forEach și terminal. Corpul (bodyKey) este munca pe care pasul o execută odată ce a fost programat. Corpurile sunt closure-uri înregistrate după cheie la pornirea serverului, așa că mulți pași de tipuri diferite pot împărți un singur corp: de exemplu, mai mulți pași listen care rulează toți conversation.promptAgent.

Pentru că semantica de programare trăiește în șase tipuri, catalogul de comportamente poate crește fără ca cineva să atingă planificatorul. Vezi Tipuri de pași din pipeline pentru catalogul complet de tipuri, corpuri și configurația lor.

Două consecințe ale designului bazat pe registru merită știute înainte să scrii de mână un șablon:

  • Un pas al cărui bodyKey nu este înregistrat produce un avertisment la încărcarea șablonului și apoi nu se execută niciodată. Nimic nu eșuează zgomotos la rulare. Pașii trigger și terminal sunt exceptați de la acest avertisment: trigger-ul rulează totuși corpul no-op pipeline.trigger, ca să se declanșeze ascultătorii din aval; un pas terminal nu are corp și este o santinelă a planificatorului.
  • Cheile din harta extras a unui nod sunt potrivite exact și ignorate în tăcere altfel. Doar extras['spaceId'] pune munca unui pas de agent într-o conversație existentă. extras['mode'] (chat / plan / review) este citit pe nodul care deschide camera (messaging.createSpace, implicit review) — un pas de agent ulterior nu poate re-permisiona o conversație în care a intrat. Orice altă ortografiere a oricăreia dintre chei este ignorată.

Pașii nu se apelează unul pe altul. Citesc și scriu într-un sac de stare partajat al rulării: un pas își citește intrările după cheie, execută și își scrie ieșirea înapoi sub propriul outputKey. Un StateReducer îmbină scrierile concurente când ramuri paralele țintesc aceeași cheie, iar un TemplateRenderer înlocuiește marcajele {{...}} din prompturi și scripturi cu date din aceeași stare.

De aceea un pipeline poate fi inspectat ulterior — sacul de stare este întreaga poveste a rulării.

Rândurile PipelineTrigger declară când pornește automat un șablon. Există patru tipuri:

Trigger Pornește o rulare când
Eveniment de domeniu Se declanșează un eveniment de domeniu care corespunde — de exemplu PullRequestPublished sau TicketAssigned
Program Devine scadentă o expresie cron/interval
Manual Îl rulezi manual; prezența unui trigger manual activ este ceea ce pune șablonul în selectorul de rulări
Webhook Un sistem extern trimite un POST către /webhooks/<token> pe portul HTTP al serverului

Trigger-ele de eveniment sunt un set închis: 16 tipuri de evenimente sunt cunoscute de dispecer și oferite în selector (EventPayloadMapper.knownEventTypes), iar nimic altceva nu poate porni un pipeline. Paisprezece dintre aceste 16 au o mapare de payload. Două — TicketCreated și TicketStatusChanged — nu au niciuna, așa că un trigger pe oricare dintre ele rămâne activ și nu se declanșează niciodată. Vezi Evenimente de domeniu pentru lista completă.

Tokenul de webhook servește și drept secret HMAC: antetul cererii X-Hub-Signature-256: sha256=<hex> este verificat față de HMAC-SHA256(body, token), livrările neverificate sunt respinse și nu pot fi reluate, iar duplicatele sunt eliminate prin dedupe_key. Pentru că rândurile de trigger sunt unice pe (spațiu de lucru, tip de eveniment, șablon), un șablon poate deține cel mult un trigger de webhook și câte un trigger pentru fiecare tip de eveniment de domeniu.

Șabloanele incluse își seamănă trigger-ele manuale și de eveniment activate; trigger-ele programate se seamănă dezactivate, așa că o baleiere recurentă trebuie activată întotdeauna explicit.

Treisprezece șabloane de pipeline sunt incluse în fiecare spațiu de lucru, iar opt dintre ele vin dezactivate: external_pr_welcome, cross_review, ticket_to_pr, pr_triage, pre_merge_gate, release_notes, dep_audit și pr_digest.

Aceasta este partea care surprinde lumea. Trigger-ele lor sunt create activate, dar PipelineEngine.start refuză un șablon dezactivat și întoarce null — așa că trigger-ul se declanșează în gol și nu apare nicio rulare. Șablonul, nu trigger-ul, este întrerupătorul. Activează-l în Setări → Spațiu de lucru → Șabloane pipeline înainte să te aștepți ca vreunul dintre cele opt să facă ceva.

Resemănarea unui spațiu de lucru păstrează ce ai ales: setarea ta isEnabled și rândurile de trigger existente supraviețuiesc. Există exact o excepție deliberată — programul zilnic al pr_merged_cleanup este activat forțat chiar dacă era oprit anterior, pentru că este colectare de gunoi. Rândurile de worktree, partițiile grafului de cod și copiile copy-on-write se acumulează fără el, iar asta nu a fost niciodată menită să fie o alegere a utilizatorului.

Încă o asimetrie de știut: șablonul inclus pr_review nu mai postează deliberat pe GitHub. Publicarea unei revizuiri este un pas separat, condiționat de utilizator, iar un pipeline care ar posta direct ar publica de două ori. cross_review, ticket_to_pr și pr_triage postează totuși un comentariu direct pe GitHub când rulează.

PipelineEngine creează înregistrarea rulării, programează nodul de start care corespunde tipului de eveniment al rulării și apoi parcurge DAG-ul: pe măsură ce pașii se încheie, evaluează trigger-ele din aval, rezolvă ramurile de router, așteaptă îmbinările, aplică politicile de reîncercare și continuarea la eșec și persistă starea, costul și erorile la fiecare pas. Muchiile care pleacă dintr-un alt start sunt ignorate pentru rularea respectivă, așa că o declanșare programată nu așteaptă o condiție doar-manuală.

Ce nu știe este orice despre tichetare, trimitere, GitHub sau mesagerie. Singurul său punct de extensie este PipelineBodyRegistry. Corpurile înregistrate acolo sunt cele care dețin AgentDispatchPort, MessagingPort și suratele lor — de aceea adăugarea unei capacități la pipeline-uri nu înseamnă niciodată schimbarea planificatorului.

Un pas care eșuează își înregistrează mesajul de eroare și trece în failed. Dacă pasul declară continueOnFail, pașii din aval continuă, iar eroarea este pusă deoparte în starea rulării; altfel rularea eșuează. Un pas cu un StepRetryPolicy este reîncercat mai întâi, cu întârziere crescătoare liniară sau exponențială, înainte să se aplice oricare dintre cele două.