Trains de release
Un train de release livre plusieurs dépôts ensemble. Lorsque le train est coupé, fremforge fige le commit de chaque dépôt qui en fait partie (ses composants) dans un manifeste immuable et haché. Il fait ensuite passer cette release par ses étapes, par exemple tst puis prd, en lançant les workflows de déploiement propres à chaque dépôt avec les commits figés.
- Une seule release pour plusieurs dépôts. Une plateforme, son infrastructure et un service d’ingestion partent dans un seul train avec un seul manifeste, et non en trois déploiements qui se trouvent tourner à peu près en même temps.
- Coupé selon une règle. fremforge coupe le train lorsque la plus ancienne fusion qui n’est encore dans aucun train est assez ancienne, par exemple 60 minutes. Vous pouvez aussi couper à la main.
- Aucune fusion ne s’y glisse. Au moment de la coupe, un gel des fusions bloque les fusions vers les composants jusqu’à ce que l’étape que vous désignez soit passée. Le bouton de fusion, l’API et la file de fusion attendent tous.
- Les contrôles de déploiement s’appliquent sans changement. Un train ne fait que lancer des exécutions de workflow. Chaque exécution passe par les contrôles de son environnement (approbateurs, branches autorisées, fenêtres horaires, vérifications requises, concurrence, indicateur de pause) exactement comme une exécution que vous lancez vous-même.
Les trains de release sont désactivés tant qu’un propriétaire de l’organisation ne les active pas. Une fois activés, ils démarrent en observation seule : ils enregistrent ce qu’ils déploieraient et ne déploient rien tant qu’un propriétaire n’a pas désactivé ce mode.
Définir un train
Un train est un fichier, trains/<name>.yaml, dans un dépôt de votre organisation que vous choisissez (un petit dépôt release convient bien). Le nom du fichier est le nom du train : 1 à 40 lettres minuscules, chiffres et tirets. Une modification d’un train est une pull request comme une autre ; elle est donc relue, et l’historique du dépôt est l’historique du train.
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| Champ | Obligatoire | Remarques |
|---|---|---|
version | oui | 1. |
owner | oui | Le login Forgejo de la personne pour qui le train agit. Elle doit être membre de l’organisation. Les gels des fusions du train sont les siens. |
description | non | Une ligne, jusqu’à 200 caractères. Mettez-la entre guillemets si elle contient #, que YAML lit comme le début d’un commentaire. |
components | oui | 1 à 20, chacun étant un nom de votre choix associé à repo et branch (par défaut main). Un même dépôt et une même branche ne peuvent figurer qu’une fois dans un train. |
cut | oui | { after_oldest_unreleased_minutes: N } (0 à 10080), ou manual pour ne couper qu’à la main. |
hold | non | release_after : un nom d’étape, cut (gel uniquement pendant l’écriture du manifeste) ou end (jusqu’à la fin du train). La valeur par défaut est la première étape. enabled: false ne pose aucun gel. |
dispatch | non | workflows (par défaut) lance les exécutions de workflow des étapes. none correspond au mode observation : le train coupe, gèle et écrit son manifeste, mais ne lance rien. |
plan_files | non | Un motif glob de fichiers de plan de release, par défaut .release/*.md. Les fichiers de plan modifiés depuis le train précédent sont listés dans le manifeste avec leur sha256. null n’en liste aucun. |
stages | oui | 1 à 5, dans l’ordre. Chacune a un name, un timeout_minutes facultatif (5 à 2880, par défaut 120) et 1 à 20 steps. |
Chaque pas (step) lance une exécution de workflow :
| Champ | Obligatoire | Remarques |
|---|---|---|
component | oui | Le composant dont le dépôt exécute le workflow, sur la branche de ce composant. |
workflow | oui | Le fichier de workflow, par exemple code-deploy.yaml. Il doit avoir un déclencheur workflow_dispatch. |
inputs | non | Les entrées workflow_dispatch, sous forme de chaînes. |
environment | non | L’environnement par lequel l’exécution doit passer. Le pas n’est validé que lorsque le déploiement de l’exécution vers cet environnement a franchi ses contrôles. Indiquez-le dès que le workflow déploie, pour qu’une exécution qui n’a jamais atteint les contrôles ne puisse pas compter comme un déploiement. |
Les valeurs d’entrée peuvent utiliser ces variables :
| Variable | Valeur |
|---|---|
{{ sha }} | Le commit figé du composant du pas. |
{{ sha.<component> }} | Le commit figé d’un autre composant. |
{{ train.name }}, {{ train.number }}, {{ train.id }} | Le nom du train, son numéro (1, 2, 3, …) et son identifiant. |
Un workflow qui reçoit le commit en entrée doit construire ou déployer exactement ce commit, et non la tête de la branche. La branche peut avancer une fois le gel du train levé.
Vérifier une définition avant sa fusion
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}')"La réponse contient valid, errors et warnings. Outre le fichier lui-même, fremforge le vérifie par rapport à votre organisation : le propriétaire est membre, chaque dépôt et chaque branche existent, chaque workflow existe sur sa branche, accepte workflow_dispatch et les entrées envoyées par le pas (et reçoit toutes les entrées qu’il exige), et chaque environnement est configuré. Un avertissement vous signale lorsqu’un workflow ne nomme jamais l’environnement du pas, ou lorsque le gel couvre une branche où fremforge/hold n’est pas une vérification requise. Lancez cet appel depuis la pull request qui modifie le train. Le jeton doit avoir deployments:read.
Activer les trains
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"}'Le jeton doit avoir policy:write. Tant que les trains sont activés, fremforge lit trains/*.yaml toutes les deux minutes, tous au même commit de la branche.
La réponse, ainsi que GET .../trains/settings, contient aussi shadow_only. Il vaut true jusqu’à ce qu’un propriétaire le désactive dans la console ; voir Observation seule.
Pour que le gel bloque le bouton de fusion, activez l’application des gels des fusions sur les branches des composants. Sans cela, le gel est enregistré et lu par les outils, mais le bouton de fusion n’attend pas.
Ce que fait une coupe
- fremforge vérifie la définition par rapport à l’organisation, comme le fait
validate. Un train qui ne peut pas s’exécuter n’est pas coupé. - Il pose le gel des fusions : un par branche utilisée par les composants, appartenant au propriétaire du train, avec le motif
Release train <name> #<n>: merges held until stage <stage> has passed. - Il lit le commit de tête de chaque composant et le compare au commit figé par le train précédent.
- Il écrit le manifeste et le train, ensemble.
Si une étape échoue après la pose du gel, fremforge lève de nouveau le gel. Une coupe ratée ne laisse jamais une branche gelée.
Une définition de train n’a qu’un seul train en cours à la fois. La coupe suivante attend que le train en cours soit terminé, en échec ou annulé.
La règle. Pour chaque composant, les commits non publiés sont ceux postérieurs au commit du train précédent. Lorsque le plus ancien d’entre eux, sur n’importe quel composant, atteint l’âge after_oldest_unreleased_minutes, le train est coupé. L’âge est la date de committer du commit, qui correspond au moment de la fusion pour une fusion squash. Le premier train d’une définition compte à partir du commit de tête de chaque branche.
À la main. Un propriétaire de l’organisation, ou un jeton qui agit pour une personne, peut couper immédiatement :
curl -X POST https://frem.sh/_app/api/v1/orgs/<org>/trains/<name>/cut \
-H "Authorization: Bearer $FREMFORGE_TOKEN"Le jeton doit avoir deployments:write et agir pour une personne : une coupe à la main est enregistrée à son nom.
Le manifeste
Le manifeste est ce que le train a livré, et il ne change jamais : la base de données refuse toute modification. Il contient :
- pour chaque composant, son dépôt, sa branche et son commit figé, le commit figé par le train précédent et le nombre de nouveaux commits ;
- les fichiers de plan modifiés depuis le train précédent, chacun avec le sha256 de son contenu au commit figé ;
- la définition du train telle qu’elle était au moment de la coupe, et le commit du dépôt de définitions d’où elle a été lue, pour qu’une modification ultérieure du fichier ne change jamais un train déjà en cours.
manifest_sha256 est le sha256 du manifeste écrit en JSON canonique : clés d’objet triées à tous les niveaux, sans espaces. Vous pouvez le recalculer à partir du manifeste stocké :
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Étapes
Un train coupé démarre sa première étape à l’exécution suivante du job, dans les deux minutes. Chaque étape exécute ses pas l’un après l’autre :
- fremforge lance le workflow du pas avec
workflow_dispatchsur la branche du composant, avec les entrées renseignées. - L’exécution passe par les contrôles de son environnement. Tant qu’elle attend une approbation, le train l’indique (
waiting for approval in environment tst). Les approbateurs approuvent le déploiement comme d’habitude, dans Build et déploiement → Déploiements. - Lorsque l’exécution réussit et (si le pas nomme un
environment) que son déploiement vers cet environnement a franchi les contrôles, le pas est validé et le suivant démarre.
Lorsque tous les pas d’une étape sont validés, le train passe à l’étape suivante. Le gel est levé après l’étape nommée dans hold.release_after, et au plus tard à la fin du train.
fremforge lance les exécutions avec le compte Forgejo de la plateforme, comme le ferait une planification. Le propriétaire du train peut donc approuver les déploiements de son propre train : une approbation par la personne qui a lancé une exécution est refusée.
Un train s’arrête (failed, avec le motif) lorsque :
- une exécution échoue ou est annulée, ou les contrôles d’un environnement la refusent (le motif nomme le contrôle) ;
- une exécution réussit sans passer par l’environnement du pas ;
- une étape dépasse son
timeout_minutes; - un lancement échoue ou son résultat est inconnu. fremforge ne lance jamais un pas deux fois : s’il ne peut pas savoir si une exécution a démarré, il arrête le train et vous demande de consulter les Actions du dépôt.
Lorsqu’un train s’arrête, ses gels sont levés. Les commits déjà fusionnés restent fusionnés ; la coupe suivante emporte le correctif.
Annuler un train qui n’est pas terminé :
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"}'Les gels sont levés. Les exécutions de workflow déjà lancées par le train continuent.
Dans la console, les propriétaires de l’organisation voient la même chose dans Build et déploiement → Trains de release (https://frem.sh/<org>/_admin/release-trains) : chaque train, celui en cours avec ce qu’il attend, les trains récents avec leurs commits figés, et des boutons pour couper maintenant ou annuler.
Mode observation
Indiquez dispatch: none pour essayer un train avant qu’il ne déploie quoi que ce soit, par exemple en parallèle du processus de release que vous utilisez aujourd’hui :
version: 1
owner: hk
dispatch: none
# components, cut, hold and stages as usualUn train en mode observation :
- est coupé selon sa règle, pose son gel des fusions et écrit son manifeste, exactement comme un train qui déploie ;
- enregistre au moment de la coupe, pour chaque pas, ce qu’il lancerait : le dépôt, la branche, le workflow et les entrées avec les commits figés (
would_dispatchsur chaque pas, avec le statutshadow) ; - parcourt ensuite ses étapes sans lancer une seule exécution de workflow, lève son gel après l’étape nommée dans
hold.release_after, puis se termine ; - est marqué
shadow: truedans l’API, avec un badge Observation dans la console, ainsi que dans chaque événement et chaque entrée d’audit.
Comparez ce qu’il aurait lancé avec ce que vous avez livré. Quand tout concorde, retirez dispatch: none. La coupe suivante déploie dès que l’organisation n’est plus en observation seule.
Observation seule
dispatch: none est une ligne dans un fichier, et toute personne qui peut fusionner dans le dépôt de définitions peut la retirer. L’observation seule est un paramètre propre à l’organisation, et fremforge l’applique quoi que dise une définition. Tant qu’elle est active :
- chaque train est un train en observation : chaque pas enregistre ce qu’il lancerait, et aucune exécution de workflow n’est lancée ;
- aucun gel des fusions n’est posé, même lorsqu’une définition en demande un ;
- une coupe dont la définition demandait de déployer ou de geler l’indique dans le manifeste (
definition.org_shadow_only) et dans le journal d’audit (release_train.shadow_only_enforced) ; - un train coupé alors que l’observation seule était désactivée s’arrête, avant son prochain lancement, si l’observation seule est réactivée. Ses gels sont levés.
L’observation seule est active pour chaque organisation jusqu’à ce qu’un propriétaire la désactive. Désactivez-la dans la console, dans Build et déploiement → Trains de release : ouvrez Laisser les trains déployer, saisissez exactement le nom de l’organisation et confirmez.
- Il faut être propriétaire de l’organisation et connecté à la console.
- Si votre organisation a configuré sa propre réauthentification renforcée, il vous est d’abord demandé de vous reconnecter, sauf si vous l’avez fait au cours des 10 dernières minutes.
- Chaque propriétaire de l’organisation reçoit un e-mail indiquant qui l’a désactivée, et un événement
train.shadow_only_clearedest publié dans le flux d’événements. - L’API peut activer l’observation seule (
"shadow_only": truedansPUT .../trains/settings) mais jamais la désactiver."shadow_only": falseest refusé pour tous les jetons.
La réactiver se fait d’un seul bouton sur la même page. Le journal d’audit enregistre release_train.shadow_only_cleared et release_train.shadow_only_set, avec l’auteur de la modification.
Lire les trains
| Appel | Réponse |
|---|---|
GET /orgs/<org>/trains | Chaque définition, son train en cours et son dernier train, ainsi que les fichiers qui ne s’analysent pas, avec leurs erreurs. MCP : list_release_trains. |
GET /orgs/<org>/trains/runs?train=<name> | Les trains coupés, du plus récent au plus ancien, avec statut, étape, pas et gels. MCP : list_release_train_runs. |
GET /orgs/<org>/trains/runs/<id> | Un train avec son manifeste et manifest_sha256. MCP : get_release_train. |
GET /orgs/<org>/trains/settings | Si les trains sont activés, et où ils sont définis. |
Tous nécessitent deployments:read. Le status d’un train vaut cut, running, completed, failed ou cancelled, et shadow vaut true pour un train en mode observation.
Événements et audit
Le flux d’événements publie un événement par dépôt composant pour chacun des cas suivants, avec sha le commit que le train y a figé :
| Type | Quand |
|---|---|
train.cut | Un train a été coupé. data contient train, number, train_id, owner, component et manifest_sha256. |
train.stage_promoted | Une étape est passée. data.stage, et data.next_stage (null après la dernière étape). |
train.completed | Toutes les étapes sont passées. |
train.failed | Le train s’est arrêté. data.stage et data.reason ; data.outcome vaut cancelled lorsque quelqu’un l’a annulé. |
Les événements d’un train en mode observation portent data.shadow: true.
Le journal d’audit enregistre 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 et release_train.cancelled, ainsi que les entrées propres au gel, merge_hold.created et merge_hold.released. Une coupe selon la règle et chaque pas sont enregistrés au nom de system:release-trains, agissant pour le propriétaire du train ; une coupe ou une annulation à la main, au nom de la personne qui l’a faite.
Ce qu’un train ne fait pas
- Il n’approuve rien. Les approbations sont données par les approbateurs sur les déploiements, comme sans train.
- Il ne modifie pas les contrôles. Un environnement avec un contournement de contrôle, un indicateur de pause ou des relecteurs requis se comporte exactement comme pour n’importe quelle exécution.
- Il ne fait pas de retour arrière. Une étape en échec arrête le train. Les commits fusionnés restent fusionnés ; livrez le correctif dans le train suivant.
- Il n’annule pas d’exécutions. Annuler un train laisse intactes les exécutions qu’il a lancées.