Release-Trains
Ein Release-Train liefert mehrere Repositorys gemeinsam aus. Wenn der Train geschnitten wird, legt fremforge den Commit jedes beteiligten Repositorys (seiner Komponenten) in einem unveränderlichen, gehashten Manifest fest. Danach führt es dieses Release durch seine Stufen, zum Beispiel erst tst und dann prd, indem es die eigenen Deploy-Workflows jedes Repositorys mit den festgelegten Commits startet.
- Ein Release über mehrere Repositorys. Eine Plattform, ihre Infrastruktur und ein Ingestion-Dienst gehen als ein Train mit einem Manifest raus, nicht als drei Deployments, die zufällig kurz nacheinander laufen.
- Nach einer Regel geschnitten. fremforge schneidet den Train, wenn der älteste noch nicht in einem Train enthaltene Merge alt genug ist, zum Beispiel 60 Minuten. Du kannst auch von Hand schneiden.
- Kein Merge rutscht dazwischen. Beim Schnitt stoppt ein Merge-Hold die Merges in die Komponenten, bis die von dir genannte Stufe passiert ist. Merge-Button, API und Merge-Warteschlange warten alle.
- Die Deploy-Gates gelten unverändert. Ein Train startet nur Workflow-Läufe. Jeder Lauf trifft auf die Gates seiner Umgebung (Freigebende, erlaubte Branches, Zeitfenster, erforderliche Checks, Nebenläufigkeit, das Pause-Flag), genau wie ein Lauf, den du selbst startest.
Release-Trains sind aus, bis ein Owner der Organisation sie einschaltet. Nach dem Einschalten starten sie im Modus Nur Schatten: Sie halten fest, was sie deployen würden, und deployen nichts, bis ein Owner das ausschaltet.
Einen Train definieren
Ein Train ist eine Datei, trains/<name>.yaml, in einem Repository deiner Organisation, das du auswählst (ein kleines release-Repository eignet sich gut). Der Dateiname ist der Name des Trains: 1 bis 40 Kleinbuchstaben, Ziffern und Bindestriche. Eine Änderung an einem Train ist ein Pull Request wie jeder andere, wird also reviewt, und die Historie des Repositorys ist die Historie des Trains.
version: 1
owner: hk # the person the train acts for
description: Platform, infrastructure and ingestion
components:
platform: { repo: platform }
infra: { repo: infrastructure-azure, branch: main }
ingest: { repo: ingestion }
cut:
after_oldest_unreleased_minutes: 60
hold:
release_after: tst # hold merges until stage tst has passed
stages:
- name: tst
timeout_minutes: 90
steps:
- component: platform
workflow: code-deploy.yaml
inputs: { app: migrate, confirm: migrate, sha: "{{ sha }}" }
environment: tst
- component: platform
workflow: code-deploy.yaml
inputs: { app: web, confirm: web, sha: "{{ sha }}" }
environment: tst
- name: prd
steps:
- component: platform
workflow: code-deploy-prd.yaml
inputs: { app: migrate, confirm: migrate, sha: "{{ sha }}", infra_pin: "{{ sha.infra }}" }
environment: prd| Feld | Pflicht | Hinweise |
|---|---|---|
version | ja | 1. |
owner | ja | Der Forgejo-Login der Person, für die der Train handelt. Sie muss Mitglied der Organisation sein. Die Merge-Holds des Trains gehören ihr. |
description | nein | Eine Zeile, bis zu 200 Zeichen. Setze sie in Anführungszeichen, wenn sie # enthält, was YAML als Beginn eines Kommentars liest. |
components | ja | 1 bis 20, jeweils ein frei gewählter Name, der auf repo und branch (Standard main) abgebildet wird. Ein Repository mit Branch kann einmal in einem Train vorkommen. |
cut | ja | { after_oldest_unreleased_minutes: N } (0 bis 10080) oder manual, um nur von Hand zu schneiden. |
hold | nein | release_after: ein Stufenname, cut (Hold nur, solange das Manifest geschrieben wird) oder end (bis der Train fertig ist). Standard ist die erste Stufe. enabled: false setzt keinen Hold. |
dispatch | nein | workflows (Standard) startet die Workflow-Läufe der Stufen. none ist der Schattenmodus: Der Train schneidet, setzt Holds und schreibt sein Manifest, startet aber nichts. |
plan_files | nein | Ein Glob für Release-Plandateien, Standard .release/*.md. Plandateien, die sich seit dem vorherigen Train geändert haben, stehen mit ihrem sha256 im Manifest. null listet keine auf. |
stages | ja | 1 bis 5, in Reihenfolge. Jede hat einen name, optional timeout_minutes (5 bis 2880, Standard 120) und 1 bis 20 steps. |
Jeder Schritt startet einen Workflow-Lauf:
| Feld | Pflicht | Hinweise |
|---|---|---|
component | ja | In welchem Komponenten-Repository der Workflow läuft, auf dem Branch dieser Komponente. |
workflow | ja | Die Workflow-Datei, zum Beispiel code-deploy.yaml. Sie muss einen workflow_dispatch-Trigger haben. |
inputs | nein | Die workflow_dispatch-Eingaben, als Strings. |
environment | nein | Die Umgebung, die der Lauf durchlaufen muss. Der Schritt gilt nur als bestanden, wenn das Deployment des Laufs in diese Umgebung ihre Gates passiert hat. Gib sie immer an, wenn der Workflow deployt, damit ein Lauf, der die Gates nie erreicht hat, nicht als Deployment zählt. |
Eingabewerte können diese Platzhalter verwenden:
| Platzhalter | Wert |
|---|---|
{{ sha }} | Der festgelegte Commit der Komponente des Schritts. |
{{ sha.<component> }} | Der festgelegte Commit einer anderen Komponente. |
{{ train.name }}, {{ train.number }}, {{ train.id }} | Der Name des Trains, seine Nummer (1, 2, 3, …) und seine ID. |
Ein Workflow, der den Commit als Eingabe erhält, sollte genau diesen Commit bauen oder deployen, nicht den Kopf des Branches. Der Branch kann weiterwandern, sobald der Hold des Trains aufgehoben ist.
Eine Definition vor dem Merge prüfen
curl -X POST https://frem.sh/_app/api/v1/orgs/<org>/trains/validate \
-H "Authorization: Bearer $FREMFORGE_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg s "$(cat trains/platform.yaml)" '{path: "trains/platform.yaml", source: $s}')"Die Antwort enthält valid, errors und warnings. Neben der Datei selbst prüft fremforge sie gegen deine Organisation: Der Owner ist Mitglied, jedes Repository und jeder Branch existiert, jeder Workflow existiert auf seinem Branch, akzeptiert workflow_dispatch und die Eingaben, die der Schritt sendet (und bekommt jede Eingabe, die er verlangt), und jede Umgebung ist konfiguriert. Eine Warnung zeigt dir, wenn ein Workflow die Umgebung des Schritts nie nennt oder wenn der Hold einen Branch abdeckt, auf dem fremforge/hold kein erforderlicher Check ist. Führe die Prüfung aus dem Pull Request aus, der den Train ändert. Das Token braucht deployments:read.
Trains einschalten
curl -X PUT https://frem.sh/_app/api/v1/orgs/<org>/trains/settings \
-H "Authorization: Bearer $FREMFORGE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"enabled": true, "definitions_repo": "release", "definitions_branch": "main"}'Das Token braucht policy:write. Solange Trains eingeschaltet sind, liest fremforge trains/*.yaml alle zwei Minuten, alle Dateien aus demselben Commit des Branches.
Die Antwort und GET .../trains/settings enthalten auch shadow_only. Der Wert ist true, bis ein Owner es in der Konsole ausschaltet; siehe Nur Schatten.
Damit der Hold den Merge-Button stoppt, schalte die Durchsetzung von Merge-Holds für die Branches der Komponenten ein. Ohne sie wird der Hold erfasst und von Werkzeugen gelesen, aber der Merge-Button wartet nicht.
Was ein Schnitt tut
- fremforge prüft die Definition gegen die Organisation, so wie
validatees tut. Ein Train, der nicht laufen kann, wird nicht geschnitten. - Es setzt den Merge-Hold: einen pro Branch, den die Komponenten nutzen, mit dem Owner des Trains als Inhaber und der Begründung
Release train <name> #<n>: merges held until stage <stage> has passed. - Es liest den Kopf-Commit jeder Komponente und vergleicht ihn mit dem Commit, den der vorherige Train festgelegt hat.
- Es schreibt das Manifest und den Train, gemeinsam.
Schlägt nach dem Setzen des Holds etwas fehl, hebt fremforge den Hold wieder auf. Ein fehlgeschlagener Schnitt hinterlässt nie einen gesperrten Branch.
Eine Train-Definition hat jeweils nur einen laufenden Train. Der nächste Schnitt wartet, bis der aktuelle abgeschlossen, fehlgeschlagen oder abgebrochen ist.
Die Regel. Für jede Komponente sind die noch nicht ausgelieferten Commits die seit dem Commit des vorherigen Trains. Wenn der älteste davon, in irgendeiner Komponente, after_oldest_unreleased_minutes alt ist, wird der Train geschnitten. Das Alter ist das Committer-Datum des Commits, bei einem Squash-Merge also der Zeitpunkt des Merges. Der erste Train einer Definition zählt ab dem Kopf-Commit jedes Branches.
Von Hand. Ein Owner der Organisation oder ein Token, das für eine Person handelt, kann sofort schneiden:
curl -X POST https://frem.sh/_app/api/v1/orgs/<org>/trains/<name>/cut \
-H "Authorization: Bearer $FREMFORGE_TOKEN"Das Token braucht deployments:write und muss für eine Person handeln: Ein Schnitt von Hand wird als ihrer erfasst.
Das Manifest
Das Manifest ist das, was der Train ausgeliefert hat, und es ändert sich nie: Die Datenbank lehnt jede Änderung daran ab. Es enthält:
- für jede Komponente Repository, Branch und festgelegten Commit, den Commit, den der vorherige Train festgelegt hat, und wie viele Commits neu sind;
- die Plandateien, die sich seit dem vorherigen Train geändert haben, jeweils mit dem sha256 ihres Inhalts beim festgelegten Commit;
- die Train-Definition so, wie sie beim Schnitt war, und den Commit des Definitions-Repositorys, aus dem sie gelesen wurde, sodass eine spätere Änderung der Datei nie einen bereits laufenden Train verändert.
manifest_sha256 ist der sha256 des Manifests, geschrieben als kanonisches JSON: Objektschlüssel auf jeder Ebene sortiert, kein Leerraum. Du kannst ihn aus dem gespeicherten Manifest nachrechnen:
curl -s https://frem.sh/_app/api/v1/orgs/<org>/trains/runs/<id> \
-H "Authorization: Bearer $FREMFORGE_TOKEN" \
| jq -cS '.manifest' | tr -d '\n' | sha256sumStufen
Ein geschnittener Train startet seine erste Stufe beim nächsten Lauf des Jobs, innerhalb von zwei Minuten. Jede Stufe führt ihre Schritte nacheinander aus:
- fremforge startet den Workflow des Schritts mit
workflow_dispatchauf dem Branch der Komponente, mit ausgefüllten Eingaben. - Der Lauf trifft auf die Gates seiner Umgebung. Während er auf eine Freigabe wartet, zeigt der Train das an (
waiting for approval in environment tst). Freigebende geben das Deployment wie gewohnt frei, unter Build und Deployment → Deployments. - Wenn der Lauf erfolgreich ist und (sofern der Schritt eine
environmentnennt) sein Deployment in diese Umgebung die Gates passiert hat, ist der Schritt bestanden und der nächste beginnt.
Wenn jeder Schritt einer Stufe bestanden ist, geht der Train zur nächsten Stufe über. Der Hold wird nach der in hold.release_after genannten Stufe aufgehoben, spätestens aber, wenn der Train endet.
fremforge startet die Läufe mit dem Forgejo-Konto der Plattform, so wie es ein Zeitplan täte. Deshalb kann der Owner des Trains die Deployments seines eigenen Trains freigeben: Eine Freigabe durch die Person, die einen Lauf gestartet hat, wird abgelehnt.
Ein Train stoppt (failed, mit Begründung), wenn:
- ein Lauf fehlschlägt oder abgebrochen wird oder die Gates einer Umgebung ihn ablehnen (die Begründung nennt das Gate);
- ein Lauf erfolgreich ist, ohne die Umgebung des Schritts zu durchlaufen;
- eine Stufe länger als ihr
timeout_minutesläuft; - ein Dispatch fehlschlägt oder sein Ergebnis unbekannt ist. fremforge startet einen Schritt nie zweimal: Kann es nicht feststellen, ob ein Lauf gestartet wurde, stoppt es den Train und bittet dich, in den Actions des Repositorys nachzusehen.
Wenn ein Train stoppt, werden seine Holds aufgehoben. Bereits gemergte Commits bleiben gemergt; der nächste Schnitt bringt die Korrektur mit.
Brich einen Train ab, der noch nicht fertig ist:
curl -X POST https://frem.sh/_app/api/v1/orgs/<org>/trains/runs/<id>/cancel \
-H "Authorization: Bearer $FREMFORGE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"reason": "Re-cut after the hotfix"}'Die Holds werden aufgehoben. Workflow-Läufe, die der Train bereits gestartet hat, laufen weiter.
In der Konsole sehen Owner der Organisation dasselbe unter Build und Deployment → Release-Trains (https://frem.sh/<org>/_admin/release-trains): jeden Train, den laufenden mit dem, worauf er wartet, die letzten Trains mit ihren festgelegten Commits sowie Buttons, um sofort zu schneiden oder abzubrechen.
Schattenmodus
Setze dispatch: none, um einen Train auszuprobieren, bevor er irgendetwas deployt, zum Beispiel parallel zu deinem heutigen Release-Prozess:
version: 1
owner: hk
dispatch: none
# components, cut, hold and stages as usualEin Train im Schattenmodus:
- wird nach seiner Regel geschnitten, setzt seinen Merge-Hold und schreibt sein Manifest, genau wie ein Train, der deployt;
- hält beim Schnitt für jeden Schritt fest, was er dispatchen würde: Repository, Branch, Workflow und Eingaben mit den festgelegten Commits (
would_dispatchbei jedem Schritt, mit dem Statusshadow); - durchläuft dann seine Stufen, ohne einen einzigen Workflow-Lauf zu starten, hebt seinen Hold nach der in
hold.release_aftergenannten Stufe auf und wird abgeschlossen; - ist in der API mit
shadow: truegekennzeichnet, in der Konsole mit dem Badge Schatten und in jedem Ereignis und Audit-Eintrag.
Vergleiche, was er dispatcht hätte, mit dem, was du ausgeliefert hast. Wenn beides übereinstimmt, entferne dispatch: none. Der nächste Schnitt deployt, sobald die Organisation nicht mehr auf Nur Schatten steht.
Nur Schatten
dispatch: none ist eine Zeile in einer Datei, und jeder, der in das Definitions-Repository mergen kann, kann sie entfernen. Nur Schatten ist eine Einstellung der Organisation selbst, und fremforge setzt sie durch, egal was eine Definition sagt. Solange sie eingeschaltet ist:
- ist jeder Train ein Schatten-Train: Jeder Schritt hält fest, was er dispatchen würde, und es wird kein Workflow-Lauf gestartet;
- wird kein Merge-Hold gesetzt, auch wenn eine Definition einen verlangt;
- vermerkt ein Schnitt, dessen Definition deployen oder einen Hold setzen wollte, das im Manifest (
definition.org_shadow_only) und im Audit-Log (release_train.shadow_only_enforced); - stoppt ein Train, der geschnitten wurde, während Nur Schatten aus war, vor seinem nächsten Dispatch, wenn Nur Schatten wieder eingeschaltet wird. Seine Holds werden aufgehoben.
Nur Schatten ist für jede Organisation eingeschaltet, bis ein Owner es ausschaltet. Schalte es in der Konsole unter Build und Deployment → Release-Trains aus: Öffne Trains ausrollen lassen, gib den Namen der Organisation exakt ein und bestätige.
- Das kann nur ein Owner der Organisation, der in der Konsole angemeldet ist.
- Hat deine Organisation einen eigenen Step-up bei der Anmeldung konfiguriert, wirst du zuerst gebeten, dich erneut anzumelden, es sei denn, du hast das in den letzten 10 Minuten getan.
- Jeder Owner der Organisation erhält eine E-Mail darüber, wer es ausgeschaltet hat, und ein Ereignis
train.shadow_only_clearederscheint im Ereignis-Feed. - Die API kann Nur Schatten einschalten (
"shadow_only": trueinPUT .../trains/settings), aber nie ausschalten."shadow_only": falsewird für jedes Token abgelehnt.
Nur Schatten wieder einzuschalten ist ein einziger Button auf derselben Seite. Das Audit-Log erfasst release_train.shadow_only_cleared und release_train.shadow_only_set, mit der Person, die es geändert hat.
Trains lesen
| Aufruf | Antwort |
|---|---|
GET /orgs/<org>/trains | Jede Definition, ihr laufender und ihr letzter Train sowie die Dateien, die sich nicht parsen lassen, mit ihren Fehlern. MCP: list_release_trains. |
GET /orgs/<org>/trains/runs?train=<name> | Geschnittene Trains, neueste zuerst, mit Status, Stufe, Schritten und Holds. MCP: list_release_train_runs. |
GET /orgs/<org>/trains/runs/<id> | Ein Train mit seinem Manifest und manifest_sha256. MCP: get_release_train. |
GET /orgs/<org>/trains/settings | Ob Trains eingeschaltet sind und wo sie definiert sind. |
Alle brauchen deployments:read. Der status eines Trains ist cut, running, completed, failed oder cancelled, und shadow ist bei einem Train im Schattenmodus true.
Ereignisse und Audit
Der Ereignis-Feed enthält für jedes der folgenden ein Ereignis pro Komponenten-Repository, mit sha als dem Commit, den der Train dort festgelegt hat:
| Art | Wann |
|---|---|
train.cut | Ein Train wurde geschnitten. data enthält train, number, train_id, owner, component und manifest_sha256. |
train.stage_promoted | Eine Stufe wurde passiert. data.stage und data.next_stage (null nach der letzten Stufe). |
train.completed | Jede Stufe wurde passiert. |
train.failed | Der Train wurde gestoppt. data.stage und data.reason; data.outcome ist cancelled, wenn ihn jemand abgebrochen hat. |
Ereignisse eines Trains im Schattenmodus tragen data.shadow: true.
Das Audit-Log erfasst release_train.settings_updated, release_train.shadow_only_set, release_train.shadow_only_cleared, release_train.shadow_only_enforced, release_train.cut, release_train.step_dispatched, release_train.stage_promoted, release_train.completed, release_train.failed und release_train.cancelled, zusammen mit den eigenen Einträgen des Holds, merge_hold.created und merge_hold.released. Ein Schnitt nach der Regel und jeder Schritt werden als system:release-trains erfasst, handelnd für den Owner des Trains; ein Schnitt oder Abbruch von Hand als die Person, die ihn ausgeführt hat.
Was ein Train nicht tut
- Er gibt nichts frei. Freigaben erteilen die Freigebenden bei den Deployments, wie ohne Train.
- Er ändert die Gates nicht. Eine Umgebung mit einer Gate-Umgehung, einem Pause-Flag oder erforderlichen Reviewern verhält sich genauso wie bei jedem anderen Lauf.
- Er rollt nicht zurück. Eine fehlgeschlagene Stufe stoppt den Train. Gemergte Commits bleiben gemergt; liefere die Korrektur im nächsten Train aus.
- Er bricht keine Läufe ab. Wird ein Train abgebrochen, bleiben die von ihm gestarteten Läufe unberührt.