Structurer les dépôts et promouvoir
Pourquoi
Avec une application et un environnement, la structure du dépôt importe peu. Elle devient la question principale dès qu'il y en a plusieurs : chez Lyneko, une dizaine d'applications, des environnements de recette et de production, des composants de plateforme, et plusieurs personnes qui modifient tout cela. Une mauvaise structure se paie en duplication (le même manifeste copié par environnement, qui dérive), en promotions risquées (un copier-coller entre deux fichiers qui emporte une modification de trop), ou en boucles absurdes (un commit de déploiement qui relance le pipeline qui crée un commit de déploiement).
Cette leçon répond à quatre questions. Où ranger la configuration ? Comment décrire plusieurs environnements à partir d'une seule définition ? Comment faire passer une version de la recette à la production de façon relue et traçable ? Et qui crée les Applications elles-mêmes, puisqu'appliquer chacune à la main n'est plus du GitOps ?
Les concepts
Configuration avec le code, ou dépôt de déploiement séparé
| Chart dans le dépôt de l'application | Dépôt de déploiement séparé | |
|---|---|---|
| Exemple | les applications de Lyneko : deploy/chart/ à côté du code | le laboratoire : signalements-deploiement |
| Un changement de code et de configuration ensemble | un seul commit, une seule relecture | deux dépôts à coordonner |
| Historique du dépôt de code | mêlé de commits de déploiement | propre |
| Droits | qui modifie le code peut modifier le déploiement | droits distincts (l'équipe d'exploitation relit la production) |
| Pipeline | un commit de déploiement déclenche le pipeline du code, sauf filtre | aucune interaction |
La documentation d'Argo CD recommande le dépôt séparé, pour séparer les droits et éviter qu'un commit de déploiement déclenche une construction. Chez Lyneko, le choix inverse a été fait pour la simplicité (un dépôt par application, un développeur peut tout comprendre), et il a un coût observé : modifier une valeur du chart, par exemple le nom d'hôte, déclenche le workflow de l'application, qui reconstruit l'image, écrit une nouvelle étiquette et provoque un second déploiement quelques minutes après le premier. Inoffensif, mais symptomatique. Pour une petite équipe, le chart dans le dépôt est raisonnable ; quand plusieurs équipes et des exigences de contrôle de la production apparaissent, le dépôt séparé devient le bon choix.
Un chart, plusieurs environnements
Deux techniques dominent pour décrire plusieurs environnements sans dupliquer :
- Helm et un fichier de valeurs par environnement. Le chart décrit la forme commune ; chaque environnement ne fixe que ce qui diffère (étiquette, répliques, nom d'hôte). C'est la technique des applications de Lyneko.
- Kustomize, une base et des surcouches (overlays). La base est un ensemble de manifestes ordinaires ; chaque surcouche y applique des modifications déclaratives : changement d'image, nombre de répliques, correctifs. Pas de langage de modèles.
Les deux se valent pour ce besoin ; on choisit selon l'outillage déjà en place, et on évite de mélanger les deux pour une même application.
Promouvoir
Promouvoir, c'est faire passer dans l'environnement suivant une version qui a fait ses preuves dans le précédent. En GitOps, une promotion est un commit qui change la version de l'environnement cible, et ce commit passe par une demande de fusion, relue par les personnes responsables de cet environnement. On y gagne une trace (qui a promu quoi, quand, avec quelle approbation) et un retour arrière évident (le revert de ce commit).
Le principe du cours précédent s'applique : on promeut le même artefact, pas une reconstruction. L'étiquette écrite en production est celle qui a été testée en recette.
L'application racine
Une Application est une ressource Kubernetes : on peut donc la décrire dans Git, et la faire déployer... par une autre Application. C'est le motif de l'application racine (App of Apps) : une seule Application, appliquée à la main une fois, suit un répertoire qui contient les définitions de toutes les autres. Ajouter une application devient un commit dans ce répertoire ; la supprimer, un autre.
flowchart TB
R["racine<br/>(appliquée une fois à la main)"] --> A["Dépôt gitops : applications/"]
A --> S1["signalements-recette"]
A --> S2["signalements-production"]
S1 --> D["Dépôt signalements-deploiement<br/>chart + environnements/recette"]
S2 --> D2["Dépôt signalements-deploiement<br/>chart + environnements/production"]
En pratique
Un répertoire par environnement
Le dépôt de déploiement est réorganisé : le chart reste commun, et chaque environnement a son répertoire avec ses valeurs.
signalements-deploiement/
├── chart/
│ ├── Chart.yaml
│ ├── values.yaml valeurs communes
│ └── templates/
└── environnements/
├── recette/values.yaml écrit par la CI à chaque fusion
└── production/values.yaml modifié par demande de fusion# environnements/production/values.yaml
image:
tag: "1.0.0" # promu depuis la recette par demande de fusion
replicas: 3
environnement: production$ mkdir -p environnements/recette environnements/production
$ git mv chart/values-recette.yaml environnements/recette/values.yaml
$ git add environnements && git commit -qm "Un répertoire par environnement" && git push -q origin main
$ helm template signalements chart -f environnements/production/values.yaml | grep -E "replicas|image:|value:"
replicas: 3
image: "registre.lyneko.example/signalements:1.0.0"
value: "production"
Ce commit casse l'application de recette, qui référence encore values-recette.yaml :
$ argocd app get signalements-recette --refresh
Sync Status: Unknown
Health Status: Healthy
CONDITION MESSAGE
ComparisonError Failed to load target state: failed to generate manifest for source 1 of 1: rpc error: code = Unknown desc = failed to execute helm template command: failed running helm: `helm template . --name-template signalements --namespace signalements-r...
$ argocd app get signalements-recette -o json | jq -r '.status.conditions[0].message' | grep -o "Error: .*"
Error: open <path to cached source>/chart/values-recette.yaml: no such file or directory
$ kubectl -n signalements-recette get deploy signalements -o jsonpath='en place : {.spec.template.spec.containers[0].image}{"\n"}'
en place : registre.lyneko.example/signalements:1.1.0
Deux leçons. La santé reste Healthy et l'application continue de tourner : une source qui ne se rend plus ne démonte jamais ce qui est en place, la synchronisation passe simplement à Unknown. Et une réorganisation du dépôt de déploiement doit être faite en même temps que la mise à jour des Applications qui le lisent, ce que l'application racine va rendre naturel : les deux vivent dans Git.
L'application racine
Un second dépôt, plateforme/gitops, contient la définition de toutes les Applications :
gitops/
├── racine.yaml appliquée une fois, à la main
└── applications/
├── signalements-recette.yaml
└── signalements-production.yamlChaque application d'environnement pointe vers le même chart, avec le fichier de valeurs de son environnement. Un fichier de valeurs hors du répertoire du chart se désigne par un chemin relatif, à condition de rester dans le même dépôt :
# applications/signalements-production.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: signalements-production
namespace: argocd
spec:
project: default
source:
repoURL: http://forgejo.forge.svc.cluster.local:3000/plateforme/signalements-deploiement.git
targetRevision: main
path: chart
helm:
releaseName: signalements
valueFiles:
- ../environnements/production/values.yaml
destination:
server: https://kubernetes.default.svc
namespace: signalements-production
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- RespectIgnoreDifferences=true(signalements-recette.yaml est identique, au nom et à l'environnement près.) La racine suit le répertoire applications/ et crée ses objets dans le namespace argocd :
# racine.yaml
# L'application racine : la seule appliquée à la main. Elle déploie toutes les autres.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: racine
namespace: argocd
spec:
project: default
source:
repoURL: http://forgejo.forge.svc.cluster.local:3000/plateforme/gitops.git
targetRevision: main
path: applications
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true$ git add . && git commit -qm "Applications de Signalements, application racine" && git push -q origin main
$ kubectl apply -f racine.yaml
application.argoproj.io/racine created
$ argocd app list
NAME CLUSTER NAMESPACE PROJECT STATUS HEALTH SYNCPOLICY CONDITIONS
argocd/racine https://kubernetes.default.svc argocd default Synced Healthy Auto-Prune <none>
argocd/signalements-production https://kubernetes.default.svc signalements-production default Synced Healthy Auto-Prune <none>
argocd/signalements-recette https://kubernetes.default.svc signalements-recette default Synced Healthy Auto-Prune <none>
En vingt secondes, la racine a créé l'application de production, et adopté l'application de recette, qui existait déjà (appliquée à la main dans les leçons précédentes) : elle a remplacé sa définition par celle du dépôt, ce qui a corrigé au passage le chemin du fichier de valeurs. Les deux environnements tournent, chacun avec sa version :
$ ./version.sh recette
{"environnement":"recette","version":"1.1.0"}
$ ./version.sh production
{"environnement":"production","version":"1.0.0"}
(version.sh interroge Signalements depuis un pod temporaire du namespace de l'environnement.) Désormais, plus aucune Application ne s'applique à la main : on modifie le dépôt gitops.
Le pipeline écrit la recette
Le pipeline de construction (le cours GitHub Actions l'a écrit) construit l'image, puis écrit son étiquette dans environnements/recette/values.yaml. Simulons-le pour une nouvelle version 1.2.0, construite et chargée dans le nœud du laboratoire :
$ sed -i 's/tag: "1.1.0" # écrit par la CI/tag: "1.2.0" # écrit par la CI/' environnements/recette/values.yaml
$ git -c user.name="github-actions[bot]" commit -qam "Deploy recette 1.2.0" && git push -q origin main
$ argocd app wait signalements-recette --sync --health --timeout 180
$ ./version.sh recette
{"environnement":"recette","version":"1.2.0"}
La recette suit main en continu : c'est le déploiement continu du cours précédent, appliqué à un environnement.
Promouvoir par demande de fusion
La production, elle, ne change que par une demande de fusion relue. Le script promouvoir.sh la prépare : il lit la version de la recette, crée une branche qui ne modifie que l'étiquette de production, et ouvre la demande de fusion par l'API de la forge.
#!/usr/bin/env bash
# Ouvre une demande de fusion qui promeut en production la version de la recette.
# Usage : ./promouvoir.sh (la forge est jointe par le port-forward de preparer-depot.sh)
set -euo pipefail
cd "$(dirname "$0")"
mdp=$PWD/forge.mdp
api() { curl -sf -u "lyneko:$(cat "$mdp")" -H 'Content-Type: application/json' "$@"; }
travail=$(mktemp -d); trap 'rm -rf "$travail"' EXIT
git clone -q http://127.0.0.1:3000/plateforme/signalements-deploiement.git "$travail"
cd "$travail"
version=$(sed -n 's/^ tag: "\([^"]*\)".*/\1/p' environnements/recette/values.yaml)
actuelle=$(sed -n 's/^ tag: "\([^"]*\)".*/\1/p' environnements/production/values.yaml)
[ "$version" != "$actuelle" ] || { echo "la production est déjà en $version"; exit 0; }
git switch -q -c "promotion/$version"
sed -i "s/^ tag: \"$actuelle\"/ tag: \"$version\"/" environnements/production/values.yaml
git -c user.name="Équipe plateforme" -c user.email=plateforme@lyneko.example \
commit -qam "Production : $actuelle vers $version"
git push -q origin "promotion/$version"
corps=$(printf 'Promotion en production de la version validée en recette.\n\n```diff\n%s\n```' "$(git diff HEAD~1 -U0 | tail -n +5)")
api -X POST http://127.0.0.1:3000/api/v1/repos/plateforme/signalements-deploiement/pulls \
-d "$(jq -n --arg t "Production : $actuelle vers $version" --arg b "$corps" --arg h "promotion/$version" \
'{title: $t, body: $b, head: $h, base: "main"}')" | jq -r '"demande de fusion n° \(.number) : \(.title)"'La première exécution du script, pendant la préparation du cours, a promu la 1.1.0 (demande de fusion n° 1, fusionnée : la production est passée de 1.0.0 à 1.1.0 ; voir aussi le piège de la demande fermée aussitôt créée, plus bas). La recette étant depuis passée en 1.2.0, voici le cycle suivant, complet :
$ ./promouvoir.sh
demande de fusion n° 2 : Production : 1.1.0 vers 1.2.0
$ curl -s http://127.0.0.1:3000/api/v1/repos/plateforme/signalements-deploiement/pulls/2 | jq -r '"#\(.number) \(.state) : \(.title)\n\(.body)"'
#2 open : Production : 1.1.0 vers 1.2.0
Promotion en production de la version validée en recette.
```diff
@@ -2 +2 @@ image:
- tag: "1.1.0" # promu depuis la recette par demande de fusion
+ tag: "1.2.0" # promu depuis la recette par demande de fusion
```
La demande de fusion ne contient qu'une ligne : la relecture porte sur une seule question, « cette version est-elle prête pour la production ? ». Une personne responsable de la production relit et fusionne, dans l'interface de la forge (ici par l'API) :
$ curl -sf -u "lyneko:..." -X POST .../pulls/2/merge -d '{"Do":"merge","delete_branch_after_merge":true}' -o /dev/null -w "fusion : HTTP %{http_code}\n"
fusion : HTTP 200
$ argocd app get signalements-production --refresh | grep "^Sync Status"
Sync Status: OutOfSync from main (6c72a0c)
$ argocd app wait signalements-production --sync --health --timeout 180
Sync Status: Synced to main (6c72a0c)
Health Status: Healthy
# production à jour 14 s après la fusion
$ ./version.sh production
{"environnement":"production","version":"1.2.0"}
La production est passée en 1.2.0 quatorze secondes après la fusion. (La ligne qui commence par # résume ce qu'a affiché une boucle de relevé, omise.) L'historique de la forge dit qui a demandé la promotion, qui l'a acceptée, et quand ; le retour arrière est le revert du commit de fusion.
L'alternative Kustomize
La même description, avec une base et une surcouche de production :
# kustomize/base/kustomization.yaml
resources:
- deployment.yaml
- service.yaml# kustomize/overlays/production/kustomization.yaml
resources:
- ../../base
namespace: signalements-production
images:
- name: registre.lyneko.example/signalements
newTag: "1.2.0"
replicas:
- name: signalements
count: 3
patches:
- target: {kind: Deployment, name: signalements}
patch: |
- op: replace
path: /spec/template/spec/containers/0/env/0/value
value: production$ kubectl version --client | grep -i kustomize
Kustomize Version: v5.8.1
$ kubectl kustomize kustomize/overlays/production | grep -E "namespace:|replicas:|image:|value:"
namespace: signalements-production
namespace: signalements-production
replicas: 3
value: production
image: registre.lyneko.example/signalements:1.2.0
La base contient des manifestes ordinaires, lisibles et applicables tels quels ; la surcouche ne dit que ce qui change. Le champ images se modifie par une commande (kustomize edit set image), ce qui le rend facile à écrire depuis un pipeline. Argo CD reconnaît le fichier kustomization.yaml et rend la surcouche sans configuration particulière.
Sous le capot
Le motif de l'application racine fonctionne parce qu'une Application est une ressource comme une autre : la racine la rend depuis Git, l'applique dans le namespace argocd, et le contrôleur, qui surveille les ressources Application, prend en charge chaque nouvelle venue. Quand la racine a « adopté » l'application de recette créée à la main, elle a simplement appliqué par-dessus sa définition, et posé son annotation de suivi : l'Application de recette appartient désormais à la racine, et sa suppression du dépôt gitops la supprimerait (avec ses ressources si elle porte le finalizer de cascade).
Un fichier de valeurs désigné par ../environnements/... est lu dans le clone du dépôt fait par le repo-server, à condition de rester dans ce clone : Argo CD refuse un chemin qui en sortirait. Pour prendre des valeurs dans un autre dépôt, on utilise une Application à plusieurs sources, dont l'une est référencée (ref: valeurs) et désignée dans valueFiles par $valeurs/chemin/fichier.yaml.
argocd app wait --sync vérifie que l'application est synchronisée à la révision qu'elle connaît. Juste après une fusion, si Argo CD n'a pas encore rafraîchi le dépôt, l'application est « synchronisée » à l'ancienne révision et la commande rend la main aussitôt. Dans un script, on fait précéder l'attente d'un rafraîchissement, comme ci-dessus.
Pièges courants
Une branche par environnement. Une branche recette et une branche production qu'on fusionne l'une dans l'autre semblent naturelles, et fonctionnent mal : les fusions emportent tout ce qui diffère, y compris ce qui devait rester propre à un environnement ; les branches divergent ; une correction urgente faite en production doit être reportée à la main. Un répertoire par environnement sur une seule branche, avec des promotions qui ne touchent qu'un fichier, évite tous ces problèmes.
Une étiquette mobile. tag: latest ou tag: main dans les valeurs rend la promotion invisible : le même commit désigne des images différentes selon le jour, et Argo CD ne voit aucun changement à appliquer quand l'image change derrière l'étiquette. Écrivez toujours une étiquette immuable, ou une empreinte.
Une réorganisation qui casse les applications. Déplacer un fichier référencé par une Application la fait passer en ComparisonError. Modifiez le dépôt de déploiement et les Applications dans la même série de changements.
La demande de fusion de promotion fermée aussitôt créée. Observé avec Forgejo pendant la préparation de ce cours : une branche supprimée puis recréée sous le même nom juste avant d'ouvrir la demande de fusion ; le traitement différé de la suppression a fermé la nouvelle demande dans la seconde. Utilisez des noms de branche uniques (incluant la version, comme ici), et ne recréez pas une branche que vous venez de supprimer.
argocd app wait rend la main avant le déploiement. Le dépôt n'a pas encore été rafraîchi. Rafraîchissez d'abord (argocd app get --refresh), ou configurez un webhook.
La racine supprime une application par erreur. Avec prune: true sur la racine, retirer un fichier de applications/ supprime l'Application, et, si elle porte le finalizer, toutes ses ressources. Protégez les applications de production : pas de finalizer de cascade, ou relecture obligatoire du dépôt gitops.
Sécurité
Les droits suivent la structure. Avec un dépôt de déploiement séparé, on peut donner aux développeurs le droit d'écrire la recette (ou laisser le pipeline le faire) et réserver la fusion vers la production à une équipe, par une règle de protection du fichier ou du répertoire (propriétaires de code). Avec le chart dans le dépôt de l'application, quiconque peut fusionner du code peut changer la production.
Le dépôt gitops est le plus sensible de tous. Il décrit quelles applications existent, d'où elles viennent, et dans quel projet. Une modification malveillante peut ajouter une application qui déploie n'importe quoi n'importe où (dans les limites du projet, leçon 9). Il mérite la protection la plus stricte : relecture par l'équipe plateforme, aucun accès d'écriture pour les pipelines.
La relecture de la promotion est une vraie barrière si elle est obligatoire et faite par une personne différente de l'auteur. Sur une forge où les règles de protection ne sont pas disponibles (le plan gratuit de GitHub pour un dépôt privé, par exemple), elle n'est qu'une convention.
En production
Chez Lyneko, chaque application porte son chart dans deploy/chart/ et une Application par application, créée au moment de l'adoption ; les applications tierces sans dépôt propre (un outil comme Stirling-PDF) ont leur chart et leur Application dans le dépôt d'infrastructure, suivis par Argo CD depuis main. Le motif de l'application racine est la prochaine étape naturelle : une seule Application appliquée à la main, toutes les autres dans un dépôt relu.
L'écriture des étiquettes. Trois approches existent : le pipeline écrit et pousse (la méthode de ce cours et de Lyneko) ; un outil dédié, Argo CD Image Updater, surveille le registre et écrit lui-même les nouvelles étiquettes (version 1.3 en août 2026, configuré par une ressource ImageUpdater depuis sa version 1.0, et dont la documentation déconseille encore l'usage pour les charges critiques) ; ou le Source Hydrator d'Argo CD (bêta en 3.5), qui rend les manifestes finaux et les pousse dans une branche dédiée, pour que Git contienne exactement ce qui est appliqué.
Les manifestes rendus. Avec Helm ou Kustomize, ce qui est appliqué n'est écrit nulle part : il faut le rendre pour le voir. Certaines équipes poussent les manifestes rendus dans Git (à la main, par le pipeline ou avec le Source Hydrator) pour que chaque demande de fusion montre le diff réel des objets Kubernetes. C'est plus verbeux, et beaucoup plus facile à relire.
Exercices
1. Ajoutez un environnement preproduction entre la recette et la production : quels fichiers créez-vous, et comment adaptez-vous promouvoir.sh ?
Solution
Un fichier environnements/preproduction/values.yaml dans le dépôt de déploiement, et une Application applications/signalements-preproduction.yaml dans le dépôt gitops (la racine la créera). Le script prend en paramètres l'environnement source et l'environnement cible (./promouvoir.sh recette preproduction, puis ./promouvoir.sh preproduction production) au lieu de les écrire en dur.
2. Un collègue propose de remplacer les répertoires par environnement par deux branches, recette et production, avec une promotion par fusion de recette dans production. Donnez deux problèmes concrets.
Solution
La fusion emporte toutes les différences de la branche recette, y compris celles qui doivent rester propres à la recette (le nombre de répliques, un réglage de débogage) : il faut les annuler à chaque promotion. Et une correction urgente faite directement en production doit être reportée à la main dans recette, sous peine d'être écrasée à la promotion suivante ou de faire diverger les branches. Avec des répertoires, une promotion ne touche qu'une ligne.
3. Après la fusion d'une promotion, argocd app wait signalements-production --sync --health rend la main immédiatement et le script affiche l'ancienne version. Pourquoi, et comment corriger le script ?
Solution
Argo CD n'a pas encore rafraîchi le dépôt : l'application est synchronisée à la révision précédente, donc l'attente est satisfaite. Il faut rafraîchir avant d'attendre (argocd app get --refresh), ou attendre que la révision synchronisée soit celle du commit de fusion (comparer status.sync.revision au commit attendu), ou configurer un webhook.
4. Modifiez la surcouche Kustomize de production pour qu'elle désigne l'image par son empreinte au lieu de son étiquette.
Solution
images:
- name: registre.lyneko.example/signalements
digest: sha256:<empreinte de l'image 1.2.0>Kustomize remplace alors la référence par registre.lyneko.example/signalements@sha256:.... L'empreinte se lit dans le registre (docker buildx imagetools inspect) ou dans la sortie du pipeline qui a publié l'image.
5. La racine est en prune: true. Une personne supprime par erreur applications/signalements-production.yaml et fusionne. Que se passe-t-il, selon que l'Application de production porte ou non le finalizer de cascade ? Que proposez-vous ?
Solution
La racine élague l'Application de production. Sans finalizer, seule l'Application disparaît : la production continue de tourner, sans être gérée ; on rétablit le fichier et la racine la recrée, qui reprend les ressources existantes. Avec le finalizer, la suppression emporte toutes les ressources : la production est coupée. Proposition : pas de finalizer de cascade sur les applications de production, relecture obligatoire du dépôt gitops, et, pour la racine, l'option Prune=confirm (ou prune: false) qui exige une confirmation explicite avant d'élaguer.
Récapitulatif
- La configuration peut vivre avec le code (simple, un seul dépôt) ou dans un dépôt de déploiement séparé (droits distincts, pas de boucle avec le pipeline) ; le second s'impose avec plusieurs équipes.
- Plusieurs environnements : un chart et un fichier de valeurs par environnement, ou une base Kustomize et des surcouches. Jamais une branche par environnement.
- La recette suit
main, écrite par le pipeline ; la production change par une demande de fusion d'une ligne, relue, qui promeut le même artefact. - L'application racine est la seule appliquée à la main ; toutes les Applications vivent dans un dépôt relu.
- Une source qui ne se rend plus ne démonte rien : l'application passe en
Unknownet continue de tourner. - Rafraîchissez avant d'attendre, et protégez le dépôt des Applications comme le plus sensible de tous.
Pour aller plus loin
- Argo CD, Cluster Bootstrapping : l'application racine et ses variantes.
- Stop Using Branches for Deploying to Different GitOps Environments : l'argumentaire détaillé contre les branches par environnement.
- Leçon suivante : Ordonner un déploiement : vagues, hooks et options.
Sources
- Argo CD, Cluster Bootstrapping (App of Apps)
- Argo CD, Best Practices (séparer configuration et code)
- Argo CD, Helm : fichiers de valeurs hors du chart
- Kubernetes, Declarative Management of Kubernetes Objects Using Kustomize
- Kostis Kapelonis (Codefresh), Stop Using Branches for Deploying to Different GitOps Environments
- Forgejo, API (demandes de fusion)
- Argo CD, Source Hydrator (bêta en 3.5)
- argoproj-labs/argocd-image-updater, documentation