Tema
Versioni di pipeline
Ogni pipeline del catalogo ha una versione di pipeline: un intero che parte da 1, cresce di uno e non torna mai indietro. Dice con quale codice della pipeline è girata una run, e la scheda della pipeline ne racconta la storia.
Una pipeline versionata porta nella sua cartella due cose:
src/pipelines/<nome>/
changelog.ts # le versioni di pipeline, dalla v1 alla corrente
versions/
v1.plan.json # lo snapshot del piano di ogni versione
v2.plan.json
run.ts # esporta il RunnablePipeline, con `changelog: CHANGELOG`In quale repo
etl versions è un comando della CLI del core e lavora sul repo che contiene l'etl.config.ts trovato risalendo dalla cartella corrente, o indicato con --config: quello del core come quello di un cliente, con le stesse regole.
- La cartella è fissa:
src/pipelines/accanto all'etl.config.ts, senza chiave di configurazione. La cartella di una pipeline è quella delrun.tsche esporta lo stesso oggetto dichiarato nel catalogo. Una pipeline dichiarata senza cartella è un errore, salvo le pipeline pronte. - Le pipeline pronte le versiona solo il core. In un repo cliente non hanno cartella: il gate le salta e lo dice,
snapshoterollbackle rifiutano. Per tornare indietro su una pipeline pronta si fa il rollback del deploy o il downgrade del core (runbook). - La mappa dei branch è del core, uguale in ogni repo:
dev → staging → main, il gate controorigin/dev(controorigin/mainsumaine suihotfix/*versomain), il rollback daorigin/main. Non si configura. - Gli script
pipeline:*delpackage.jsonsono alias di una riga dietl versions …, nel core come nel boilerplate del cliente.
Il changelog
changelog.ts esporta CHANGELOG: PipelineChangelog, e il RunnablePipeline lo dichiara in changelog. È obbligatorio per tipo: una pipeline senza non compila. Ogni voce ha il numero, il giorno del bump (AAAA-MM-GG), uno o più ticket Jira e tre elenchi: Aggiunte (added), Modifiche (changed) e Rimozioni (removed). Una voce nata da un rollback porta anche rollbackTo, con la versione a cui si torna e il motivo.
Ogni voce scritta da etl versions (il rollback e --realign) registra anche core, la versione del core installata in quel momento, letta dal package.json del pacchetto. Nelle voci scritte a mano è facoltativa; se c'è, dev'essere una versione semver.
La versione corrente non si dichiara a parte: è l'ultima voce. etl catalog check (regola changelog, in ogni repo) rifiuta numeri non consecutivi da 1, date che tornano indietro e voci con i tre elenchi vuoti. La versione corrente finisce nel report di ogni run, in pipelineVersion (run-report).
Lo snapshot del piano
Ogni versione congela il suo piano, il PlanSpec che getSpec() restituisce, in versions/v<N>.plan.json. Lo snapshot della versione corrente deve coincidere con il getSpec() di oggi, a meno degli scopi di nodi e istanze: lo verifica il gate, quindi etl ci (nel core pnpm run check). Lo scopo spiega il nodo e non ne cambia il comportamento, quindi lo snapshot si calcola senza: riscriverlo non chiede un bump.
È questo che fa scattare il controllo anche quando il piano cambia per un file fuori dalla cartella della pipeline: un connettore condiviso che espone una colonna in più, un cambio del core che sposta un arco. Anche quello è un cambio della pipeline, e chiede un bump. Il test elenca in una volta tutte le pipeline toccate, ognuna col comando da lanciare.
Lo snapshot si calcola senza .env, come la build della doc: con un .env completo, alcune getSpec() mostrano host e cartelle dell'installazione, e uno snapshot versionato non deve dipendere dalla macchina su cui è stato generato, né portare nel repo i dati di un cliente. Il comando sostituisce process.env con un ambiente vuoto per la durata del calcolo, quindi il risultato è lo stesso con e senza .env, e anche con variabili esportate dalla shell.
Il file è generato: non si scrive né si ritocca a mano. Le chiavi sono in ordine alfabetico e l'indentazione è di due spazi, così rigenerare uno snapshot senza modifiche non produce diff; gli array restano nell'ordine del piano. Prettier non li formatta (.prettierignore), perché il test li confronta byte per byte con l'uscita del comando.
Quando il test è rosso
| Messaggio | Cosa fare |
|---|---|
manca lo snapshot del piano della vN | Lancia il comando che il messaggio indica. Capita a una pipeline nuova o subito dopo un bump. |
il piano non coincide con lo snapshot della vN | Il piano è cambiato. Se la vN è già rilasciata, aggiungi la voce vN+1 al changelog e lancia il comando. Se la vN è la versione che stai scrivendo, rilancialo con --force. |
etl versions snapshot
Scrive lo snapshot del piano della versione di pipeline corrente.
bash
pnpm run pipeline:snapshot <pipeline> # alias di: pnpm run etl versions snapshot <pipeline>
pnpm run pipeline:snapshot <pipeline> --force # sovrascrive uno snapshot diverso| Situazione | Esito | Exit code |
|---|---|---|
| Lo snapshot della versione corrente manca | Lo scrive in versions/v<N>.plan.json, creando la cartella | 0 |
| Lo snapshot c'è ed è uguale al piano | Non tocca nulla e lo dice | 0 |
Lo snapshot c'è ed è diverso, senza --force | Non lo sovrascrive: spiega che il piano di una versione rilasciata è congelato e serve un bump | 1 |
Lo snapshot c'è ed è diverso, con --force | Lo sovrascrive: è la versione ancora in lavorazione, il cui piano cambia finché la PR non è chiusa | 0 |
| Nome che non è nel catalogo | Errore che elenca le pipeline del catalogo; nessun file scritto | 2 |
| Nome mancante, sottocomando o flag sconosciuto | Errore d'uso | 2 |
Cartella della pipeline non trovata (nessun run.ts in src/pipelines/ che esporta la pipeline) | Errore che dice perché; nessun file scritto | 2 |
| Pipeline pronta in un repo cliente | Errore: la versiona il core; nessun file scritto | 2 |
| Lettura o scrittura su disco fallita | Errore col messaggio del sistema | 1 |
Il rifiuto senza --force esiste perché sovrascrivere in silenzio lo snapshot di una versione già rilasciata farebbe tornare verde il test del catalogo senza bump, e la versione dichiarata non direbbe più con quale piano è girata una run. --force da solo non basta a impedirlo, quindi lo impone il gate: lo snapshot di ogni versione che è già sulla base deve restare uguale, e uno riscritto o tolto è una violazione. --force serve solo alla versione che non è ancora sulla base. L'unica eccezione è una versione la cui voce di changelog cambia insieme allo snapshot: il numero è passato a un'altra versione, come nel back-merge di un rollback quando dev aveva già preso quel numero (runbook, § 5).
Il comando trova la cartella di una pipeline importando i run.ts sotto src/pipelines/ del repo e cercando quello che esporta il suo RunnablePipeline (vedi In quale repo).
etl versions snapshot --realign
Un bump del core può spostare il piano di una pipeline senza che la sua cartella cambi: un connettore che espone una colonna in più, un nodo che descrive diversamente la sua configurazione. Anche quella deriva chiede una versione di pipeline nuova, e la PR che alza il core la porta. Il comando la scrive al posto dell'autore:
bash
pnpm etl versions snapshot --realign --ticket <chiave>Per ogni pipeline versionata dal repo (le pronte no) il cui snapshot della versione corrente non coincide col piano di oggi, aggiunge al changelog.ts la versione successiva con la voce «Piano riallineato al core x.y.z» fra le Modifiche, il ticket, la data di oggi e core, e ne scrive lo snapshot. Il gate poi la tratta come ogni bump: base + 1, con la sua voce.
Prima di committare, rivedi gli snapshot nel diff: ogni spostamento dev'essere spiegato dal CHANGELOG.md del core. Il segnale che serve arriva dalle release marcate «Cambia il piano».
| Situazione | Esito | Exit code |
|---|---|---|
| Uno o più piani spostati | Voce e snapshot della versione nuova per ognuno, ed elenco | 0 |
| Nessun piano spostato | Non tocca nulla e lo dice | 0 |
| Lo snapshot della versione corrente di una pipeline manca | Errore che indica il comando per scriverlo; nessun file scritto | 2 |
--ticket mancante o non una chiave Jira, un nome di pipeline, --force | Errore d'uso; nessun file scritto | 2 |
--realign serve alle versioni già rilasciate, il cui piano è congelato. Se la versione corrente è quella che il branch sta scrivendo, il suo snapshot si aggiorna con snapshot <nome> --force, non con una versione in più: il gate vorrebbe la base + 1, non + 2.
Quando serve un bump
Il comportamento di una pipeline non cambia senza che cambi la sua versione. Lo verifica il gate delle versioni, etl versions check, in CI e in etl ci (nel core pnpm run check).
File di comportamento sono tutti i file della cartella della pipeline, compreso changelog.ts, esclusi i test (*.test.ts) e gli snapshot (versions/). Toccare solo test o doc (le schede in docs/pipelines/ stanno fuori dalla cartella) non chiede un bump. Un cambio fuori dalla cartella che sposta il piano (un connettore, il core) lo intercetta lo snapshot: anche quello è un bump.
Nemmeno lo scopo dei nodi chiede un bump: un file di comportamento che differisce dal merge-base solo nelle proprietà purpose: "<stringa letterale>" delle opzioni di un metodo del builder (.source(, .transform(, …, .pipeline( per lo scopo di un'istanza) non conta come cambiato, e lo snapshot del piano si calcola senza gli scopi, di nodi e istanze. Il confronto è a token, quindi ignora anche gli spazi e le virgole finali che Prettier sposta quando aggiunge l'opzione; un { purpose } rimasto vuoto vale come opzione tolta. Per dare uno scopo a un nodo con opzioni calcolate, stendile in un oggetto: { ...opzioni, purpose: "…" } vale opzioni, ma solo se accanto allo spread c'è lo scopo e nient'altro. Uno scopo scritto con una costante, una concatenazione o un template con interpolazione invece non si riconosce, e conta come modifica: scrivilo letterale. Un purpose fuori dalle opzioni del builder (un campo di un record, il parametro di una query) è un dato, e il suo cambio chiede il bump. Ne segue anche che un cambio di soli spazi fuori dalle stringhe non chiede un bump, tranne gli a capo che decidono il punto e virgola automatico.
Se un file di comportamento differisce dalla base, la pipeline deve dichiarare la versione della base più uno, con la sua voce nel changelog e almeno una riga fra Aggiunte, Modifiche e Rimozioni. Una pipeline che sulla base non c'è, non è ancora versionata o è stata rinominata (un altro nome nello snapshot della base), parte dalla v1.
Senza base restano solo gli snapshot. Se il riferimento che la mappa qui sotto chiede non c'è (un repo cliente appena creato con etl init e mai pubblicato, il bootstrap di un repo migrato, un clone senza origin/dev), non c'è niente da confrontare: il gate lo dice con un avviso, nessuna base (origin/dev): niente da confrontare, e salta i controlli che leggono la base (il bump richiesto, la versione uguale a base + 1, gli snapshot già sulla base). Applicare lì «nuova ⇒ v1» farebbe fallire il bootstrap di una pipeline che arriva dal core già alla v2 o oltre. Lo snapshot della versione corrente invece si confronta col piano anche senza base, perché la base non serve per saperlo: in un repo appena migrato uno snapshot che non coincide, o che manca, è rosso subito e non al primo push. Dove il gate deve valere tutto la base va scaricata: in CI con il checkout a fetch-depth: 0, in locale con git fetch origin dev main. Una base indicata a mano con --base che non c'è resta un errore.
| Branch | Base |
|---|---|
hotfix/* diretto a main (o senza PR: in locale, sui push), e main stesso | origin/main |
tutti gli altri, compreso il back-merge di un hotfix/* verso dev o staging | origin/dev |
La base è origin/dev anche quando la PR va verso un branch di feature: così tutti i ticket di una feature che toccano la stessa pipeline condividono un solo bump, il primo che lo scrive, e gli altri lo trovano già fatto. Su un hotfix/* il numero si conta da main, perché è lì che il codice torna; la sua PR di back-merge verso dev invece si confronta con dev, ed è lì che una collisione col numero già preso da dev chiede di rinumerare. Su main il codice è già in produzione: il confronto con sé stesso lascia solo il controllo dello snapshot.
I file cambiati si contano dal merge-base con la base, la versione della base si legge in cima alla base. Ne segue che:
- un branch rimasto indietro non risponde delle pipeline che
devha cambiato dopo: non le ha toccate, e il gate non le guarda; - due PR in parallelo che rivendicano lo stesso numero si scontrano: quando la prima entra in
dev, la seconda va in rosso finché non allinea il branch e rinumera la sua voce. È lo stesso fenomeno dei numeri degli ADR, reso rosso invece che silenzioso.
Una cartella spostata non perde la sua storia. Se sulla base changelog.ts non c'è nel percorso nuovo, il gate chiede a git da quale cartella vengono i file spostati (basta che ne riconosca uno, anche se il changelog è stato riscritto dal bump) e legge la versione lì. In locale sposta con git mv, o fai git add -A prima del gate: git riconosce uno spostamento solo fra file in indice, mentre in CI è tutto già committato. Lo spostamento muove file di comportamento, quindi chiede un bump, con una voce che lo dice.
Una pipeline rinominata invece è una pipeline nuova e riparte dalla v1, anche se la cartella resta la stessa: la dichiarazione della base conta solo se è della stessa pipeline, e lo dice il nome nello snapshot della base, che il test del catalogo vuole uguale al nome nel catalogo.
Il gate non vede un cambio del core che altera il comportamento senza cambiare il piano: per scelta, lo rende visibile il report di run, che porta sia la versione sia la release.
Il changelog contiene solo dati
Il gate legge la versione della base importando il changelog.ts di quel commit, da solo, fuori dal repo. Per questo changelog.ts contiene solo dati: sono ammessi gli import di tipo (import type { PipelineChangelog } …), che spariscono; un import di valore non si risolverebbe e il gate si fermerebbe col percorso del file.
etl versions check
bash
pnpm run pipeline:check # alias di: pnpm run etl versions check
pnpm run pipeline:check --base origin/staging # confronta con un'altra basePer ogni pipeline del catalogo raccoglie i file cambiati dal merge-base (committati, modificati e nuovi non ancora aggiunti), la dichiarazione sulla base e lo snapshot, e li passa alla decisione pura in src/versions/version-gate.ts. Prima toglie dai file cambiati quelli di comportamento che differiscono dal merge-base solo negli scopi dei nodi (vedi Quando serve un bump). Elenca tutte le violazioni in una volta, ognuna con la pipeline e cosa fare. Le pipeline pronte senza cartella le salta, e l'intestazione le elenca. Il branch lo legge da GITHUB_HEAD_REF (le PR, dove il checkout è il merge commit a HEAD staccato), poi da GITHUB_REF_NAME (i push), poi da git; il branch di destinazione di una PR da GITHUB_BASE_REF.
| Messaggio | Cosa fare |
|---|---|
i file di comportamento sono cambiati … ancora la vN | Aggiungi la voce vN+1 al changelog e lancia pnpm run pipeline:snapshot <nome>. |
dichiara la vX, ma su origin/dev è alla vB | Il numero è sbagliato: è vB+1. Se la base è andata avanti, git merge origin/dev e rinumera la voce. |
su origin/dev è nuova (…), quindi parte dalla v1 | Una pipeline nuova, o rinominata, ha la sola voce v1. |
la vN è già presa su origin/dev da un'altra modifica | Un'altra PR è entrata prima con lo stesso numero: git merge origin/dev, porta la tua voce alla vN+1, snapshot. |
il changelog non ha la voce vN | La versione dichiarata deve avere la sua voce. |
la voce vN ha Aggiunte, Modifiche e Rimozioni tutte vuote | Scrivi cosa cambia e perché. |
manca lo snapshot / il piano non coincide con lo snapshot | Come per il test del catalogo: vedi Quando il test è rosso. |
lo snapshot della vN è già su origin/dev e qui è stato riscritto o tolto | Ripristinalo dalla base (git checkout origin/dev -- <cartella>/versions/vN.plan.json). Se il piano è cambiato, serve un bump. |
| Situazione | Exit code |
|---|---|
| Nessuna violazione | 0 |
| Nessuna base della mappa dei branch: avviso «nessuna base», snapshot della versione corrente a posto | 0 |
| Almeno una violazione, anche senza base (solo snapshot mancante o diverso dal piano) | 1 |
Base data con --base che non c'è, storia insufficiente, git che fallisce, flag sbagliato | 2 |
changelog.ts o snapshot del piano della base che non si importa, non è JSON o non ha la forma attesa | 2 |
In CI il gate è un passo del workflow ci.yml, dopo i test; in un repo cliente è una fase di etl ci, che il ci.yml del boilerplate lancia dopo il setup. Il checkout usa fetch-depth: 0: senza, mancherebbero origin/dev, origin/main e la storia per il merge-base. La CI del core gira solo sulle PR verso dev; le PR verso un branch di feature non hanno CI, quindi lì il gate si lancia in locale con etl ci (nel core pnpm run check), e in CI scatta sulla PR finale verso dev.
etl versions rollback
Riporta il codice di una sola pipeline a una sua versione andata in produzione. La procedura completa, dalla PR di hotfix al back-merge, è nel runbook Rollback di una pipeline.
bash
pnpm run pipeline:rollback <pipeline> <N> --motivo "<testo>" --ticket <chiave> # alias di: pnpm run etl versions rollback …
pnpm run pipeline:rollback <pipeline> <N> --motivo "<testo>" --ticket <chiave> --workspace <id>Il piano lo decide una funzione pura, src/versions/rollback-plan.ts; il comando raccoglie i fatti da git e lo esegue:
- la vN è quella dell'ultimo commit di
origin/mainche la dichiarava, cercato solo fra i commit chemainha avuto in cima (git log --first-parent): una versione entrata con un merge e già superata nel suo branch non è mai stata in produzione. Risalendo la storia una versione può solo scendere; se sale, la cartella ospitava una pipeline poi rinominata, e lì la ricerca si ferma; - il numero nuovo è la versione corrente su
mainpiù uno, non la vN: la voce harollbackTocon la vN e il motivo, il ticket di--ticket, la data di oggi, la versione del core installata (core), e una riga fra le Modifiche che dice quali versioni si ritirano; - crea
hotfix/rollback-<pipeline>-v<N>daorigin/maine riporta la cartella della pipeline com'era su quel commit, esclusichangelog.tseversions/, che restano quelli dimainperché portano la storia arrivata dopo. Aggiunge la voce in fondo achangelog.ts, nella forma che Prettier lascerebbe; - rigenera lo snapshot della versione nuova con
etl versions snapshotin un processo a parte, lanciato con la CLI dello stesso pacchetto e lo stessoetl.config.ts: il codice ripristinato si carica solo lì; - confronta il core della voce della vN, se la registra, con quello installato: la cartella ripristinata gira sul core di oggi, e se i due differiscono il piano può non essere quello della vN. La riga Core del resoconto lo dice;
- legge lo stato di pipeline (locale, o del workspace con
--workspace) e confronta le source degli snapshot della versione ritirata e della vN: elenca lo stato salvato, le source che la versione ritirata aggiungeva o toglieva, e lo stato di nodi che nella vN non sono source. Si guardano le source perché solo loro hanno stato; il piano non dice quali dichiarano un watermark, quindi una source toccata compare anche senza stato salvato.
Se lo stato non si legge (l'API del workspace non risponde) il rollback va avanti lo stesso, e il resoconto dice il comando etl state show per leggerlo prima di aprire la PR: lo stato serve a decidere cosa dichiarare, non a preparare il branch.
Non committa, non pusha, e non scrive né dati né stato.
| Situazione | Esito | Exit code |
|---|---|---|
| Rollback preparato | Branch creato, cartella ripristinata, voce e snapshot scritti; stampa stato e prossimi passi | 0 |
| Lo snapshot non si genera (la cartella vecchia non gira contro il core di oggi) | Branch, cartella e voce restano: l'errore del processo figlio e cosa fare | 1 |
| Pipeline fuori dal catalogo, nome o versione mancanti, versione non intera, flag sconosciuto | Errore d'uso | 2 |
| Pipeline pronta in un repo cliente | Errore che indica il rollback del deploy e il downgrade del core | 2 |
--motivo mancante o vuoto, --ticket mancante o non una chiave Jira | Errore d'uso, prima di leggere git | 2 |
vN mai stata in cima a main, uguale alla versione corrente o successiva | Errore che elenca le versioni che main ha avuto | 2 |
Pipeline senza changelog.ts su origin/main, o origin/main assente (git fetch origin) | Errore che dice perché | 2 |
changelog.ts di main senza la forma attesa (l'array chiuso da ]; a inizio riga) | Errore: la voce va scritta a mano | 2 |
Modifiche non committate, o hotfix/rollback-<pipeline>-v<N> che esiste già | Errore, prima di cambiare branch | 2 |
Ogni esito con exit 2 arriva prima di toccare branch e file.
La sezione Versioni della scheda
La scheda di ogni pipeline (docs/pipelines/) chiude con una sezione ## Versioni e il componente <PipelineVersions name="<pipeline>" />. Mostra la storia completa, dalla versione più recente:
- la voce del changelog scritta dall'autore, cioè il perché: Aggiunte, Modifiche e Rimozioni, il ticket, la data e, per una voce nata da un rollback, la versione a cui si torna e il motivo;
- sotto, il diff generato rispetto alla versione precedente, cioè il cosa: nodi, archi e colonne per nodo aggiunti, cambiati e rimossi, calcolato dagli snapshot
v<N-1>ev<N>. La v1 non ha una versione precedente, e la scheda lo dice.
Nodi e archi si confrontano per id, le colonne per id dentro il loro nodo. Un arco che cambia estremi o porta cambia anche id, quindi compare come rimosso più aggiunto; un arco con lo stesso id e un joinType o un tipo diverso è cambiato.
Una versione è marcata «rompe il contratto» quando una colonna di un nodo sink della versione precedente sparisce, cambia nome o cambia dataType: chi legge le tabelle a valle se ne deve accorgere. Il nome conta anche quando l'id della colonna resta lo stesso, perché a valle si legge il nome. Vale anche quando sparisce il sink intero, o quando il nodo smette di essere un sink: le sue colonne non escono più. Una colonna aggiunta, una che cambia solo chiave, o un cambio su un nodo che non è sink non rompono il contratto.
Il diff lo calcola etl docs build a build time, e il loader della doc lo legge dal file del sito. Nella guida del core lo calcola per le schede che la guida pubblica, cioè le pipeline pronte e gli esempi. Uno snapshot non valido ferma la build col suo percorso, uno mancante col comando che lo scrive, invece di mostrare un diff inventato.
Una pipeline nuova
Parte dalla v1: changelog.ts con la sola voce v1, poi pnpm run pipeline:snapshot <nome> dopo averla aggiunta al catalogo in etl.config.ts. Lo scaffold /new-pipeline fa entrambe le cose.
Riferimenti
- Codice:
src/versions/changelog.ts(il changelog),src/versions/plan-snapshot.ts(snapshot e confronto),src/versions/plan-diff.ts(il diff fra due piani e «rompe il contratto»),src/versions/pipeline-dirs.ts(la cartella di una pipeline),src/versions/core-version.ts(la versione del core installata),src/versions/version-gate.ts(la decisione del gate),src/versions/rollback-plan.ts(il piano di rollback),src/versions/changelog-source.ts(la voce aggiunta al testo dichangelog.ts),src/versions/purpose-diff.ts(il confronto a token che ignora gli scopi di nodi e istanze),src/cli-versions.ts(i comandi),src/cli-versions-check.ts(l'adattatore git del gate),src/cli-versions-realign.ts(snapshot --realign),src/cli-versions-rollback.ts(l'adattatore del rollback, con-io.tsper git e il processo figlio e-report.tsper il resoconto),.github/workflows/ci.yml(il passo in CI). - Doc:
docs/.vitepress/lib/pipeline-versions.ts(la storia per la scheda),docs/.vitepress/lib/pipeline-diagrams.ts(grafi e storia, chesrc/docs/sito.tscalcola peretl docs),docs/.vitepress/data/pipelines.data.ts(il loader, che li legge dal file del sito),docs/.vitepress/theme/components/PipelineVersions.vue(il componente). - Test:
src/catalog/catalogo-del-core.test.ts(snapshot di ogni pipeline del catalogo del core),tests/cli/catalog-check.test.ts(il changelog, conetl catalog check),src/versions/version-gate.test.ts(i casi del gate),src/versions/purpose-diff.test.ts(cosa conta come cambio a meno degli scopi),src/cli-versions-check.test.ts(il comando su un git finto),tests/integration/versions-check.integration.test.ts(il comando su un repository git temporaneo, inpnpm run test:integration),src/versions/rollback-plan.test.ts(i casi del piano),src/cli-versions-rollback.test.ts(il comando su un git finto),src/cli-versions-rollback-io.test.ts(git e processo figlio),tests/integration/versions-rollback.integration.test.ts(il comando su un repository git temporaneo),tests/cli/versions-repo-cliente.test.ts(gate, pronte,--realigne rollback attraverso la CLI, su un repo cliente condev,stagingemain),src/versions/plan-diff.test.ts(il diff),docs/.vitepress/lib/pipeline-cards.test.ts(ogni scheda ha<PipelineVersions>).

