fix(deploy): ne ramène jamais un environnement sur un commit plus ancien

Les CI de deux push rapprochés peuvent finir dans le désordre. deploy.yml faisait alors
reset --hard sur un GITHUB_SHA plus ancien que celui déjà déployé, et le groupe concurrency
deploy-<branche> ne garde qu'un job en attente : un troisième arrivé annulait le précédent, qui
n'était jamais déployé.

- un commit qui précède celui déjà déployé est ignoré, avec une annotation dans le run ;
- le groupe concurrency cède la place à un flock posé dans le clone de la VM, tenu du fetch
  jusqu'à la sonde de santé : les déploiements passent un par un, aucun n'est annulé ;
- les trois étapes n'en font plus qu'une, le verrou tombant avec le shell qui l'a posé ; les
  journaux restent découpés par ::group::.

ADR 0014 et 50-cicd.md décrivent les deux gardes.
This commit is contained in:
Johan LEROY
2026-09-23 10:44:51 +02:00
parent 0284cc0cd8
commit 3ca4839a02
3 changed files with 33 additions and 13 deletions
+19 -10
View File
@@ -5,6 +5,9 @@
# déploie `GITHUB_SHA`, le commit testé, pas la pointe de branche du moment (ADR 0014). # déploie `GITHUB_SHA`, le commit testé, pas la pointe de branche du moment (ADR 0014).
# Piège : jamais de déclencheur `pull_request` ici. Sur un dépôt public, une PR de fork # Piège : jamais de déclencheur `pull_request` ici. Sur un dépôt public, une PR de fork
# exécuterait son code sur la machine de production (ADR 0009) - job deploy. # exécuterait son code sur la machine de production (ADR 0009) - job deploy.
# Piège : les CI de deux push finissent parfois dans le désordre. Un commit qui précède celui déjà
# déployé est ignoré, et le verrou est un `flock` sur la VM plutôt qu'un groupe `concurrency` :
# GitHub n'y garde qu'un job en attente, et le suivant l'évince sans bruit.
name: Déploiement name: Déploiement
@@ -20,9 +23,6 @@ jobs:
name: Déploie sur la VM name: Déploie sur la VM
runs-on: [self-hosted, linux, eni-g3] runs-on: [self-hosted, linux, eni-g3]
timeout-minutes: 30 timeout-minutes: 30
concurrency:
group: deploy-${{ github.ref_name }}
cancel-in-progress: false
environment: environment:
name: ${{ github.ref_name == 'main' && 'prod' || 'rec' }} name: ${{ github.ref_name == 'main' && 'prod' || 'rec' }}
url: ${{ github.ref_name == 'main' && 'https://enervision.local' || 'https://rec.enervision.local:8443' }} url: ${{ github.ref_name == 'main' && 'https://enervision.local' || 'https://rec.enervision.local:8443' }}
@@ -30,29 +30,38 @@ jobs:
ENVIRONNEMENT: ${{ github.ref_name == 'main' && 'prod' || 'rec' }} ENVIRONNEMENT: ${{ github.ref_name == 'main' && 'prod' || 'rec' }}
PORT_HTTPS: ${{ github.ref_name == 'main' && '443' || '8443' }} PORT_HTTPS: ${{ github.ref_name == 'main' && '443' || '8443' }}
steps: steps:
- name: Aligne le dossier de l'environnement sur le commit testé # Un seul step : le verrou tombe avec le shell qui l'a posé.
- name: Déploie le commit testé, sans jamais reculer
run: | run: |
cd "/srv/enervision/${ENVIRONNEMENT}" cd "/srv/enervision/${ENVIRONNEMENT}"
exec 9>"$(git rev-parse --git-dir)/verrou-deploiement"
flock 9
echo "::group::Aligne le dossier de l'environnement sur le commit testé"
git fetch --quiet origin "${GITHUB_REF_NAME}" git fetch --quiet origin "${GITHUB_REF_NAME}"
deploye="$(git rev-parse HEAD)"
if [ "$deploye" != "$GITHUB_SHA" ] && git merge-base --is-ancestor "$GITHUB_SHA" "$deploye"; then
echo "::notice::${GITHUB_SHA:0:7} précède le commit déjà déployé (${deploye:0:7}) : rien à déployer."
exit 0
fi
git checkout --quiet "${GITHUB_REF_NAME}" git checkout --quiet "${GITHUB_REF_NAME}"
git reset --quiet --hard "${GITHUB_SHA}" git reset --quiet --hard "${GITHUB_SHA}"
git log -1 --format='%h %s' git log -1 --format='%h %s'
echo "::endgroup::"
- name: Reconstruit et redémarre la stack echo "::group::Reconstruit et redémarre la stack"
run: |
cd "/srv/enervision/${ENVIRONNEMENT}"
make stack-up make stack-up
echo "::endgroup::"
- name: Attend que l'API réponde derrière le proxy echo "::group::Attend que l'API réponde derrière le proxy"
run: |
for _ in $(seq 1 36); do for _ in $(seq 1 36); do
if curl --fail --silent --insecure "https://localhost:${PORT_HTTPS}/api/v1/health/ready"; then if curl --fail --silent --insecure "https://localhost:${PORT_HTTPS}/api/v1/health/ready"; then
exit 0 exit 0
fi fi
sleep 5 sleep 5
done done
echo "::endgroup::"
echo "L'API ne répond pas après 3 minutes" >&2 echo "L'API ne répond pas après 3 minutes" >&2
cd "/srv/enervision/${ENVIRONNEMENT}"
compose="docker compose -f docker-compose.yml -f docker-compose.prod.yml" compose="docker compose -f docker-compose.yml -f docker-compose.prod.yml"
$compose ps $compose ps
$compose logs --tail=50 backend proxy $compose logs --tail=50 backend proxy
@@ -44,7 +44,11 @@ passent en `workflow_call` et n'ont plus de déclencheur propre.
exiger dans les règles de branche** : un composant sauté par son filtre ne publie aucun check exiger dans les règles de branche** : un composant sauté par son filtre ne publie aucun check
interne, qui resterait « en attente » s'il était exigé. interne, qui resterait « en attente » s'il était exigé.
4. **`deploy`.** Il appelle `deploy.yml`, sur les seuls push, et seulement si `CI ok` a réussi. 4. **`deploy`.** Il appelle `deploy.yml`, sur les seuls push, et seulement si `CI ok` a réussi.
`deploy.yml` aligne le dossier de l'environnement sur `GITHUB_SHA`, le commit testé. `deploy.yml` aligne le dossier de l'environnement sur `GITHUB_SHA`, le commit testé, sauf
si ce commit précède celui déjà déployé : les CI de deux push peuvent finir dans le désordre,
et un environnement ne recule jamais. Les déploiements d'un même environnement passent un par
un sous un verrou `flock` sur la VM, et non dans un groupe `concurrency`, où GitHub ne garde
qu'un job en attente et annule le précédent quand un troisième arrive.
`deploy.yml` n'a toujours **aucun déclencheur `pull_request`** : il n'accepte que `deploy.yml` n'a toujours **aucun déclencheur `pull_request`** : il n'accepte que
`workflow_call` et `workflow_dispatch`, dans l'esprit de l'ADR 0009. `workflow_call` et `workflow_dispatch`, dans l'esprit de l'ADR 0009.
+9 -2
View File
@@ -111,8 +111,15 @@ et non sur la pointe de branche du moment, qui a pu avancer pendant la CI. Il la
migrations Alembic dans le conteneur backend, et attend jusqu'à trois minutes que migrations Alembic dans le conteneur backend, et attend jusqu'à trois minutes que
`/api/v1/health/ready` réponde derrière le proxy. Cette sonde ne vérifie que la connexion à la `/api/v1/health/ready` réponde derrière le proxy. Cette sonde ne vérifie que la connexion à la
base et la présence de TimescaleDB : sans la migration, le déploiement serait vert sur une base base et la présence de TimescaleDB : sans la migration, le déploiement serait vert sur une base
sans schéma, et c'est pourquoi `make stack-up` la porte. Un groupe de concurrence par branche, sans schéma, et c'est pourquoi `make stack-up` la porte.
sans annulation, empêche deux déploiements simultanés du même environnement.
Les CI de deux push rapprochés peuvent finir dans le désordre. Deux gardes empêchent un
environnement de reculer ou de sauter un commit :
- un commit qui **précède** celui déjà déployé est ignoré, avec une annotation dans le run ;
- les déploiements d'un même environnement passent un par un sous un verrou `flock` posé dans le
clone de la VM. Un groupe `concurrency` ne convenait pas : GitHub n'y garde qu'un job en
attente, et un troisième arrivé l'annule sans erreur.
Le job ne fait pas de `actions/checkout` dans son espace de travail, et c'est voulu : le dossier Le job ne fait pas de `actions/checkout` dans son espace de travail, et c'est voulu : le dossier
de l'environnement est stable, hors du runner, parce que `.env`, certificats et volumes doivent de l'environnement est stable, hors du runner, parce que `.env`, certificats et volumes doivent