Retour au blog

Comment passer des déploiements locaux à un CI/CD rapide avec GitHub Actions

2026-01-198 min read

Je construis une application web de coaching de prononciation pour m'aider à améliorer ma prosodie en anglais. Je suis de langue maternelle espagnole et j'ai pensé l'ouvrir au public au cas où cela aiderait d'autres personnes apprenant l'anglais comme langue seconde. Le backend est en Python avec FastAPI, utilisant Parselmouth (un wrapper Python pour Praat) pour l'analyse audio et Google Gemini pour générer des retours de coaching. Le frontend est en React avec Vite et TypeScript.

Pour l'infrastructure, j'ai opté pour Google Cloud. Le backend s'exécute sur Cloud Run, le frontend est hébergé sur Firebase Hosting, et les données utilisateur résident dans Firestore. Les images Docker sont poussées vers Artifact Registry. Le code se trouve sur GitHub. C'est une configuration assez standard pour un projet personnel que je voulais maintenir dans les limites du niveau gratuit.

Le problème

Je déployais à la fois le frontend et le backend depuis ma machine locale. Le frontend était assez rapide, mais le backend était pénible. Chaque déploiement impliquait de construire l'image Docker à partir de zéro, ce qui prenait 5 à 10 minutes pendant que pip téléchargeait toutes les dépendances Python.

Je voulais passer au CI/CD afin que les déploiements se fassent automatiquement lorsque je poussais sur main. Mais je ne voulais pas non plus attendre 10 minutes pour chaque push. J'ai donc décidé de rendre GitHub Actions intelligent quant à ce qui changeait réellement.

La structure monorepo

Tout se trouve dans un seul dépôt avec un dossier frontend/ et un dossier backend/. Le workflow se déclenche à chaque push sur main et je voulais qu'il détermine ce qui a réellement changé et ne déploie que cette partie. Fichiers frontend modifiés ? Déployer uniquement sur Firebase. Code backend modifié ? Déployer uniquement sur Cloud Run. Et si requirements.txt changeait, reconstruire l'image de base Docker. Mais seulement dans ce cas.

Trouver un moyen de détecter les changements

J'ai cherché et trouvé une action appelée dorny/paths-filter. Elle vérifie quels fichiers ont changé lors d'un push et produit des booléens que d'autres jobs peuvent utiliser. Je l'ai configurée comme premier job de mon workflow :

jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
requirements: ${{ steps.filter.outputs.requirements }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
backend:
- 'backend/**'
frontend:
- 'frontend/**'
requirements:
- 'backend/requirements.txt'
- 'backend/Dockerfile.base'

Cela m'a donné trois sorties que je pouvais référencer dans d'autres jobs pour décider s'ils devaient s'exécuter.

Gérer les builds Docker lents

Même avec la détection des changements fonctionnelle, les déploiements backend restaient lents. Chaque build exécutait pip install à partir de zéro. Mon requirements.txt contient des bibliothèques ML, des trucs de traitement audio, tout quoi. Cela représente beaucoup de téléchargements.

J'ai réalisé que je ne changeais les dépendances qu'une fois par mois, peut-être. Le reste du temps, je ne faisais que modifier le code de l'application. J'ai donc essayé de diviser le Dockerfile en deux.

Dockerfile.base

J'y ai mis tout ce qui était lent : paquets système, dépendances pip. L'idée était de construire ceci une fois et de le pousser vers Artifact Registry, puis de le réutiliser.

FROM python:3.11-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
ffmpeg \
espeak-ng \
libsndfile1 \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY requirements.txt .
RUN pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txt

Dockerfile.fast

Celui-ci démarre à partir de l'image de base et copie uniquement le code. Pas de apt-get. Pas de pip. Juste une commande COPY.

FROM us-central1-docker.pkg.dev/lescoach/prosody-repo/prosody-api-base:latest

WORKDIR /app
COPY app/ ./app/

ENV PYTHONUNBUFFERED=1
ENV PORT=8080
EXPOSE 8080

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]

Cela a réduit le temps de build de 5 à 10 minutes à environ 30 secondes, ce qui était une belle amélioration.

Assembler le workflow

J'ai configuré le job de l'image de base pour qu'il ne s'exécute que lorsque les dépendances changent réellement :

build-base-image:
runs-on: ubuntu-latest
needs: detect-changes
if: needs.detect-changes.outputs.requirements == 'true'
steps:
- uses: actions/checkout@v4
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}
- uses: google-github-actions/setup-gcloud@v2
- run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
- name: Build and push base image
working-directory: backend
run: |
docker build -f Dockerfile.base -t ${{ env.ARTIFACT_REGISTRY }}/prosody-api-base:latest .
docker push ${{ env.ARTIFACT_REGISTRY }}/prosody-api-base:latest

Le job de déploiement backend a nécessité quelques essais et erreurs. Il devait attendre la construction de l'image de base si elle s'exécutait, mais fonctionner aussi si elle était ignorée. J'ai fini par ceci :

deploy-backend:
runs-on: ubuntu-latest
needs: [detect-changes, build-base-image]
if: |
always() &&
needs.detect-changes.outputs.backend == 'true' &&
(needs.build-base-image.result == 'success' || needs.build-base-image.result == 'skipped')
steps:
# étapes d'authentification...

- name: Check if base image exists
id: check-base
run: |
if docker manifest inspect ${{ env.ARTIFACT_REGISTRY }}/prosody-api-base:latest > /dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi

- name: Build base image if missing
if: steps.check-base.outputs.exists == 'false'
working-directory: backend
run: |
docker build -f Dockerfile.base -t ${{ env.ARTIFACT_REGISTRY }}/prosody-api-base:latest .
docker push ${{ env.ARTIFACT_REGISTRY }}/prosody-api-base:latest

- name: Build and push app image
working-directory: backend
run: |
docker build -f Dockerfile.fast -t ${{ env.ARTIFACT_REGISTRY }}/prosody-api:${{ github.sha }} .
docker push ${{ env.ARTIFACT_REGISTRY }}/prosody-api:${{ github.sha }}

- uses: google-github-actions/deploy-cloudrun@v2
with:
service: prosody-api
region: us-central1
image: ${{ env.ARTIFACT_REGISTRY }}/prosody-api:${{ github.sha }}
flags: --allow-unauthenticated

J'ai appris que always() était nécessaire ici. Sans cela, le job n'évalue même pas sa condition si un job amont a été ignoré. J'ai également ajouté une vérification docker manifest inspect pour gérer le cas où l'image de base n'existe pas encore, comme lors d'un premier déploiement ou si quelqu'un l'a supprimée accidentellement.

La partie frontend

Le job frontend s'est avéré plus simple car il ne dépend pas du backend :

deploy-frontend:
runs-on: ubuntu-latest
needs: detect-changes
if: needs.detect-changes.outputs.frontend == 'true'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- run: npm ci
working-directory: frontend
- run: npm run build
working-directory: frontend
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: ${{ secrets.GITHUB_TOKEN }}
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_LESCOACH }}
channelId: live
projectId: lescoach
entryPoint: frontend

Si seul le frontend a changé, c'est le seul job qui s'exécute.

Configuration de l'authentification

J'ai opté pour la Workload Identity Federation au lieu des clés de compte de service. La configuration initiale était plus longue, mais j'ai apprécié de ne pas avoir à gérer la rotation des clés ou à m'inquiéter des fichiers JSON stockés dans les secrets. GitHub obtient des jetons de courte durée via OIDC, ce qui m'a semblé plus propre.

Où j'en suis

Les changements uniquement sur le frontend se déploient maintenant en moins d'une minute. Les changements de code backend prennent environ 90 secondes. Les changements de dépendances prennent toujours environ 4 minutes, mais ceux-ci sont rares pour moi.

Workflow GitHub Actions montrant les jobs detect-changes, build-base-image, deploy-frontend et deploy-backend se terminant en 1m 33s
Frontend et backend déployés ensemble en 1m 33s.

La plupart du temps, je modifie des éléments de l'interface utilisateur et le déploiement est rapide maintenant. Le workflow complet représente environ 130 lignes de YAML. L'action paths-filter fait la majeure partie du travail pour déterminer ce qui a changé.

Restez Informé

Recevez les derniers articles et insights dans votre boîte mail.

Unsubscribe anytime. No spam, ever.