Naar de hoofdinhoud

Release-treinen

Een release-trein levert meerdere repository’s samen. Als de trein wordt gesneden, legt fremforge de commit van elke repository erin (de componenten) vast in een onveranderlijk, gehasht manifest. Daarna brengt het die release door zijn fasen, bijvoorbeeld eerst tst en dan prd, door de eigen deploy-workflows van elke repository te starten met de vastgelegde commits.

  • Eén release over meerdere repository’s. Een platform, de infrastructuur ervan en een ingestiedienst gaan mee als één trein met één manifest, niet als drie deploys die toevallig kort na elkaar draaien.
  • Gesneden volgens een regel. fremforge snijdt de trein als de oudste merge die nog niet in een trein zit oud genoeg is, bijvoorbeeld 60 minuten. Je kunt ook met de hand snijden.
  • Er glippen geen merges tussendoor. Bij het snijden houdt een merge-hold merges naar de componenten tegen tot de fase die je noemt geslaagd is. De mergeknop, de API en de merge queue wachten allemaal.
  • De deploy-gates gelden ongewijzigd. Een trein start alleen workflow-runs. Elke run komt de gates van zijn omgeving tegen (goedkeurders, toegestane branches, tijdvensters, verplichte checks, gelijktijdigheid, de pauzevlag), precies zoals een run die je zelf start.

Release-treinen staan uit totdat een eigenaar van de organisatie ze aanzet. Als ze aan worden gezet, beginnen ze in alleen schaduw: ze leggen vast wat ze zouden uitrollen en rollen niets uit totdat een eigenaar dat uitzet.

Een trein definiëren

Een trein is één bestand, trains/<name>.yaml, in een repository van je organisatie die je zelf kiest (een kleine release-repository werkt goed). De bestandsnaam is de naam van de trein: 1 tot 40 kleine letters, cijfers en koppeltekens. Een wijziging van een trein is een pull request als elke andere, dus ze wordt gereviewd, en de geschiedenis van de repository is de geschiedenis van de trein.

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
VeldVerplichtOpmerkingen
versionja1.
ownerjaDe Forgejo-login van de persoon voor wie de trein handelt. Die moet lid van de organisatie zijn. De merge-holds van de trein staan op zijn naam.
descriptionneeEén regel, maximaal 200 tekens. Zet hem tussen aanhalingstekens als hij # bevat, wat YAML leest als het begin van een opmerking.
componentsja1 tot 20, elk een naam die je zelf kiest, gekoppeld aan repo en branch (standaard main). Een repository met branch kan maar één keer in een trein zitten.
cutja{ after_oldest_unreleased_minutes: N } (0 tot 10080), of manual om alleen met de hand te snijden.
holdneerelease_after: een fasenaam, cut (alleen tegenhouden zolang het manifest wordt geschreven) of end (tot de trein klaar is). De standaard is de eerste fase. enabled: false plaatst geen hold.
dispatchneeworkflows (de standaard) start de workflow-runs van de fasen. none is de schaduwmodus: de trein snijdt, houdt tegen en schrijft zijn manifest, maar start niets.
plan_filesneeEen glob van releaseplanbestanden, standaard .release/*.md. Planbestanden die sinds de vorige trein zijn gewijzigd, staan in het manifest met hun sha256. null vermeldt er geen.
stagesja1 tot 5, op volgorde. Elke fase heeft een name, een optionele timeout_minutes (5 tot 2880, standaard 120) en 1 tot 20 steps.

Elke stap start één workflow-run:

VeldVerplichtOpmerkingen
componentjaDe repository van welke component de workflow draait, op de branch van die component.
workflowjaHet workflowbestand, bijvoorbeeld code-deploy.yaml. Het moet een workflow_dispatch-trigger hebben.
inputsneeDe workflow_dispatch-inputs, als strings.
environmentneeDe omgeving waar de run doorheen moet. De stap slaagt pas als de deployment van de run naar deze omgeving door de gates is gekomen. Noem de omgeving altijd als de workflow uitrolt, zodat een run die de gates nooit heeft bereikt niet als deployment kan meetellen.

Inputwaarden kunnen deze placeholders gebruiken:

PlaceholderWaarde
{{ sha }}De vastgelegde commit van de component van de stap.
{{ sha.<component> }}De vastgelegde commit van een andere component.
{{ train.name }}, {{ train.number }}, {{ train.id }}De naam van de trein, zijn nummer (1, 2, 3, …) en zijn id.

Een workflow die de commit als input krijgt, moet precies die commit bouwen of uitrollen, niet de kop van de branch. De branch kan verder gaan zodra de hold van de trein is opgeheven.

Een definitie controleren voordat ze wordt gemerged

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}')"

Het antwoord bevat valid, errors en warnings. Naast het bestand zelf controleert fremforge het tegen je organisatie: de eigenaar is lid, elke repository en branch bestaat, elke workflow bestaat op zijn branch, accepteert workflow_dispatch en de inputs die de stap meestuurt (en krijgt elke input die hij vereist), en elke omgeving is ingesteld. Een waarschuwing laat je weten als een workflow de omgeving van de stap nergens noemt, of als de hold een branch dekt waar fremforge/hold geen verplichte check is. Voer het uit vanuit de pull request die de trein wijzigt. Het token heeft deployments:read nodig.

Treinen aanzetten

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"}'

Het token heeft policy:write nodig. Zolang treinen aanstaan, leest fremforge elke twee minuten trains/*.yaml, allemaal op één commit van de branch.

Het antwoord, en GET .../trains/settings, bevat ook shadow_only. Dat is true totdat een eigenaar het in de console uitzet; zie Alleen schaduw.

Wil je dat de hold de mergeknop tegenhoudt, zet dan handhaving van merge-holds aan voor de branches van de componenten. Zonder handhaving wordt de hold vastgelegd en door tooling gelezen, maar wacht de mergeknop niet.

Wat snijden doet

  1. fremforge controleert de definitie tegen de organisatie, zoals validate dat doet. Een trein die niet kan draaien, wordt niet gesneden.
  2. Het plaatst de merge-hold: één per branch die de componenten gebruiken, op naam van de eigenaar van de trein, met als reden Release train <name> #<n>: merges held until stage <stage> has passed.
  3. Het leest de kopcommit van elke component en vergelijkt die met de commit die de vorige trein vastlegde.
  4. Het schrijft het manifest en de trein, samen.

Mislukt er iets na de hold, dan heft fremforge de hold weer op. Een mislukte snede laat nooit een branch tegengehouden achter.

Per treindefinitie loopt er één trein tegelijk. De volgende snede wacht tot de huidige trein is afgerond, is mislukt of is geannuleerd.

De regel. Per component zijn de niet-uitgebrachte commits die sinds de commit van de vorige trein. Is de oudste daarvan, bij welke component ook, after_oldest_unreleased_minutes oud, dan wordt de trein gesneden. De leeftijd is de committerdatum van de commit, en dat is bij een squash merge het moment van de merge. De eerste trein van een definitie telt vanaf de kopcommit van elke branch.

Met de hand. Een eigenaar van de organisatie, of een token dat voor een persoon handelt, kan nu snijden:

curl -X POST https://frem.sh/_app/api/v1/orgs/<org>/trains/<name>/cut \
  -H "Authorization: Bearer $FREMFORGE_TOKEN"

Het token heeft deployments:write nodig en moet voor een persoon handelen: een snede met de hand wordt op diens naam vastgelegd.

Het manifest

Het manifest is wat de trein heeft geleverd, en het verandert nooit: de database weigert elke wijziging ervan. Het bevat:

  • de repository, branch en vastgelegde commit van elke component, de commit die de vorige trein vastlegde en hoeveel commits nieuw zijn;
  • de planbestanden die sinds de vorige trein zijn gewijzigd, elk met de sha256 van de inhoud op de vastgelegde commit;
  • de treindefinitie zoals die bij het snijden was, en de commit van de definitie-repository waaruit ze is gelezen, zodat een latere wijziging van het bestand nooit een trein verandert die al loopt.

manifest_sha256 is de sha256 van het manifest, geschreven als canonieke JSON: objectsleutels op elk niveau gesorteerd, geen witruimte. Je kunt hem uit het opgeslagen manifest opnieuw berekenen:

curl -s https://frem.sh/_app/api/v1/orgs/<org>/trains/runs/<id> \
  -H "Authorization: Bearer $FREMFORGE_TOKEN" \
  | jq -cS '.manifest' | tr -d '\n' | sha256sum

Fasen

Een gesneden trein start zijn eerste fase bij de volgende uitvoering van de taak, binnen twee minuten. Elke fase voert haar stappen na elkaar uit:

  1. fremforge start de workflow van de stap met workflow_dispatch op de branch van de component, met de inputs ingevuld.
  2. De run komt de gates van zijn omgeving tegen. Zolang hij op een goedkeuring wacht, meldt de trein dat (waiting for approval in environment tst). Goedkeurders keuren de deployment zoals gewoonlijk goed, onder Bouwen en uitrollen → Deployments.
  3. Als de run slaagt, en (als de stap een environment noemt) zijn deployment naar die omgeving door de gates is gekomen, is de stap geslaagd en start de volgende.

Zijn alle stappen van een fase geslaagd, dan gaat de trein naar de volgende fase. De hold wordt opgeheven na de fase die in hold.release_after staat, en uiterlijk als de trein eindigt.

fremforge start de runs met het Forgejo-account van het platform, zoals een schema dat zou doen. Daardoor kan de eigenaar van de trein de deployments van zijn eigen trein goedkeuren: een goedkeuring door de persoon die een run startte, wordt geweigerd.

Een trein stopt (failed, met de reden) als:

  • een run mislukt of wordt geannuleerd, of de gates van een omgeving hem weigeren (de reden noemt de gate);
  • een run slaagt zonder door de omgeving van de stap te gaan;
  • een fase langer duurt dan haar timeout_minutes;
  • een dispatch mislukt of de uitkomst ervan onbekend is. fremforge start een stap nooit twee keer: kan het niet vaststellen of een run is gestart, dan stopt het de trein en vraagt het je in de Actions van de repository te kijken.

Als een trein stopt, worden zijn holds opgeheven. Commits die al zijn gemerged, blijven gemerged; de volgende snede neemt de oplossing mee.

Een trein annuleren die nog niet klaar is:

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"}'

De holds worden opgeheven. Workflow-runs die de trein al heeft gestart, blijven draaien.

In de console zien eigenaren van de organisatie hetzelfde onder Bouwen en uitrollen → Release-treinen (https://frem.sh/<org>/_admin/release-trains): elke trein, de trein die loopt met waar hij op wacht, recente treinen met hun vastgelegde commits, en knoppen om nu te snijden of te annuleren.

Schaduwmodus

Zet dispatch: none om een trein uit te proberen voordat hij iets uitrolt, bijvoorbeeld naast het releaseproces dat je nu gebruikt:

version: 1
owner: hk
dispatch: none
# components, cut, hold and stages as usual

Een trein in schaduwmodus:

  • wordt volgens zijn regel gesneden, plaatst zijn merge-hold en schrijft zijn manifest, precies zoals een trein die uitrolt;
  • legt bij het snijden voor elke stap vast wat hij zou starten: de repository, branch, workflow en inputs met de vastgelegde commits (would_dispatch bij elke stap, met status shadow);
  • doorloopt daarna zijn fasen zonder ook maar één workflow-run te starten, heft zijn hold op na de fase die in hold.release_after staat, en wordt afgerond;
  • is gemarkeerd met shadow: true in de API, met een badge Schaduw in de console, en in elke gebeurtenis en elk auditrecord.

Vergelijk wat hij zou hebben gestart met wat je hebt geleverd. Komt dat overeen, verwijder dan dispatch: none. De volgende snede rolt uit zodra de organisatie niet meer in alleen schaduw staat.

Alleen schaduw

dispatch: none is een regel in een bestand, en iedereen die naar de definitie-repository kan mergen, kan hem verwijderen. Alleen schaduw is de eigen instelling van de organisatie, en fremforge handhaaft die, wat een definitie ook zegt. Zolang ze aanstaat:

  • is elke trein een schaduwtrein: elke stap legt vast wat hij zou starten, en er wordt geen workflow-run gestart;
  • wordt er geen merge-hold geplaatst, ook niet als een definitie erom vraagt;
  • vermeldt een snede waarvan de definitie vroeg om uit te rollen of tegen te houden dat in het manifest (definition.org_shadow_only) en in het auditlog (release_train.shadow_only_enforced);
  • stopt een trein die is gesneden terwijl alleen schaduw uitstond, vóór zijn volgende dispatch, als alleen schaduw weer wordt aangezet. Zijn holds worden opgeheven.

Alleen schaduw staat voor elke organisatie aan totdat een eigenaar het uitzet. Zet het uit in de console, onder Bouwen en uitrollen → Release-treinen: open Treinen laten uitrollen, typ precies de naam van de organisatie en bevestig.

  • Daarvoor is een eigenaar van de organisatie nodig, aangemeld in de console.
  • Heeft je organisatie een eigen extra aanmeldstap ingesteld, dan word je eerst gevraagd je opnieuw aan te melden, tenzij je dat in de afgelopen 10 minuten al hebt gedaan.
  • Elke eigenaar van de organisatie krijgt een e-mail met wie het heeft uitgezet, en er komt een gebeurtenis train.shadow_only_cleared in de gebeurtenissenfeed.
  • De API kan alleen schaduw aanzetten ("shadow_only": true in PUT .../trains/settings), maar nooit uitzetten. "shadow_only": false wordt voor elk token geweigerd.

Alleen schaduw weer aanzetten is één knop op dezelfde pagina. Het auditlog legt release_train.shadow_only_cleared en release_train.shadow_only_set vast, met wie het heeft gewijzigd.

Treinen lezen

AanroepAntwoord
GET /orgs/<org>/trainsElke definitie, de trein die ervan loopt en de laatste trein, en de bestanden die niet te lezen zijn met hun fouten. MCP: list_release_trains.
GET /orgs/<org>/trains/runs?train=<name>Gesneden treinen, de nieuwste eerst, met status, fase, stappen en holds. MCP: list_release_train_runs.
GET /orgs/<org>/trains/runs/<id>Eén trein met zijn manifest en manifest_sha256. MCP: get_release_train.
GET /orgs/<org>/trains/settingsOf treinen aanstaan, en waar ze zijn gedefinieerd.

Ze hebben allemaal deployments:read nodig. De status van een trein is cut, running, completed, failed of cancelled, en shadow is true voor een trein in schaduwmodus.

Gebeurtenissen en audit

De gebeurtenissenfeed heeft voor elk van deze één gebeurtenis per componentrepository, met als sha de commit die de trein daar heeft vastgelegd:

SoortWanneer
train.cutEr is een trein gesneden. data bevat train, number, train_id, owner, component en manifest_sha256.
train.stage_promotedEen fase is geslaagd. data.stage, en data.next_stage (null na de laatste fase).
train.completedAlle fasen zijn geslaagd.
train.failedDe trein is gestopt. data.stage en data.reason; data.outcome is cancelled als iemand hem heeft geannuleerd.

Gebeurtenissen van een trein in schaduwmodus hebben data.shadow: true.

Het auditlog legt 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 en release_train.cancelled vast, samen met de eigen gebeurtenissen van de hold, merge_hold.created en merge_hold.released. Een snede volgens de regel en elke stap worden vastgelegd als system:release-trains, handelend voor de eigenaar van de trein; een snede of annulering met de hand op naam van de persoon die haar uitvoerde.

Wat een trein niet doet

  • Hij keurt niets goed. Goedkeuringen worden door goedkeurders op de deployments gegeven, net als zonder trein.
  • Hij verandert de gates niet. Een omgeving met een gate-bypass, een pauzevlag of verplichte reviewers gedraagt zich precies zoals bij elke andere run.
  • Hij draait niets terug. Een mislukte fase stopt de trein. Gemergede commits blijven gemerged; lever de oplossing in de volgende trein.
  • Hij annuleert geen runs. Het annuleren van een trein laat de runs die hij heeft gestart met rust.