Nous configurons Github Actions pour exécuter des tests avant de mettre à jour notre code source sur les serveurs de production.
Les Github Actions nous permettent d'exécuter les scripts du projet automatiquement.
Par scripts, j'entend :
Si ces tests passent, nous pouvons être sûrs que l'application fonctionnera correctement sur le serveur de production. Du moins, ça nous évitera quelques surprises.
Nous allons donc configurer Github pour d'abord exécuter ces tests. S'ils passent, nous allons compiler le code source de notre application (avec un build) et envoyer l'image sur les serveurs de Docker.
Et enfin, nous téléchargerons cette image Docker sur le serveur pour exécuter notre application.
Pour configurer nos actions Github, nous avons besoin de :
Nous allons créer le fichier qui va contenir les instructions que Github Actions va exécuter.
Pour ce faire, nous allons à la racine de notre projet créer un dossier nommé .github
.
1mkdir .github
À l'intérieur, nous allons créer un fichier dossier nommé workflows
.
1cd .github && mkdir workflows
Enfin, nous allons créer un fichier dans ce dossier, nommé ci.yml
.
1touch .github/workflows/ci.yml
Ce fichier va contenir la configuration de notre workflow.
Voici son contenu :
1name: 🚀 Deploy2on:3push:4branches:5- main6- dev7pull_request: {}89permissions:10actions: write11contents: read1213jobs:14lint:15name: ⬣ ESLint16runs-on: ubuntu-latest17env:18TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}19TURBO_TEAM: ${{ secrets.TURBO_TEAM }}20TURBO_REMOTE_ONLY: true2122steps:23- name: Check out code24uses: actions/checkout@v425with:26fetch-depth: 22728- name: Setup Node.js environment29uses: actions/setup-node@v430with:31node-version: 2032cache: 'npm'3334- name: Install dependencies35run: npm install3637- name: Build Application38run: npm run build3940- name: 🔬 Lint41run: npm run lint4243typecheck:44name: ʦ TypeScript45runs-on: ubuntu-latest46env:47TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}48TURBO_TEAM: ${{ secrets.TURBO_TEAM }}49TURBO_REMOTE_ONLY: true5051steps:52- name: Check out code53uses: actions/checkout@v454with:55fetch-depth: 25657- name: Setup Node.js environment58uses: actions/setup-node@v459with:60node-version: 2061cache: 'npm'6263- name: Install dependencies64run: npm install6566- name: Build Application67run: npm run build6869- name: 🔎 Type check70run: npm run typecheck --if-present7172build:73name: 🐳 build74uses: ./.github/workflows/build.yml75secrets: inherit7677deploy:78name: 🚀 Deploy79runs-on: [self-hosted]80needs: [build, lint, typecheck]81if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') && github.event_name == 'push' }}82env:83TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}84TURBO_TEAM: ${{ secrets.TURBO_TEAM }}85TURBO_REMOTE_ONLY: true8687steps:88- name: Cache node modules89uses: actions/cache@v4.0.090with:91path: ~/.npm92key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}93restore-keys: |94${{ runner.os }}-node-9596- name: ⬇️ Checkout repo97uses: actions/checkout@v4.1.19899- name: Login to Docker Hub100uses: docker/login-action@v2101with:102username: ${{ secrets.DOCKERHUB_USERNAME }}103password: ${{ secrets.DOCKERHUB_TOKEN }}104- name: 🚀 Run Docker Compose on Staging105if: ${{ github.ref == 'refs/heads/dev' }}106env:107NODE_ENV: staging108run: |109docker pull varkoff/nestjs-remix-monorepo:latest110docker compose -f docker-compose.staging.yaml up -d111docker system prune --all --volumes --force112113- name: 🚀 Run Docker Compose on Production114if: ${{ github.ref == 'refs/heads/main' }}115env:116NODE_ENV: production117DATABASE_URL: ${{ secrets.DATABASE_URL }}118run: |119docker pull varkoff/nestjs-remix-monorepo:production120docker compose -f docker-compose.prod.yml up -d121docker system prune --all --volumes --force
Analysons ce fichier.
Les déclarations en début de fichier ont la signification suivante :
1name: 🚀 Deploy2on:3push:4branches:5- main6- dev7pull_request: {}89permissions:10actions: write11contents: read
push
sur la branche main ou dev. (Les pull request validées sont aussi des push). pull-request: {}
signifie que notre workflow sera déclenché à chaque ouverture de pull request.actions: write
signifie que notre workflow va pouvoir utiliser les actions Github. contents: read
signifie que notre workflow va pouvoir lire le code source.Ensuite, nous définissons les jobs (donc les actions) que notre workflow va exécuter.
Chaque job possède plusieurs instructions :
name
: Le nom de la tâche.runs-on
: Le runner que va utiliser Github pour exécuter la tâche. Nous pouvons configurer un runner sur notre serveur (self-hosted) ou sur Github. ubuntu-latest
est le runner par défaut de Github pour les environnements Linux.env
: Les variables d'environnement que notre job va utiliser.steps
: Les étapes que notre job va exécuter. Il faut au moins définir une étape. Certaines étapes seront redondantes d'un job à l'autre, mais c'est plus simple de les définir dans chaque job.Regardons-les de plus près.
Pour utiliser ESlint dans une Github Actions, nous allons définir un job nommé lint
.
1lint:2name: ⬣ ESLint3runs-on: ubuntu-latest4env:5TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}6TURBO_TEAM: ${{ secrets.TURBO_TEAM }}7TURBO_REMOTE_ONLY: true89steps:10- name: Check out code11uses: actions/checkout@v412with:13fetch-depth: 21415- name: Setup Node.js environment16uses: actions/setup-node@v417with:18node-version: 2019cache: 'npm'2021- name: Install dependencies22run: npm install2324- name: Build Application25run: npm run build2627- name: 🔬 Lint28run: npm run lint
Ici, nous exécutons successivement plusieurs commandes (déclarées sous l'instruction steps
) :
actions/checkout@v4
: Cette action permet de récupérer le code source de notre projet. Sans elle, on exécuterait les commandes d'après dans le vide.actions/setup-node@v4
: Cette action permet de configurer l'environnement Node.js sur notre job et de l'installer. Sans elle, on ne peut pas exécuter Javascript.npm install
: Cette commande permet d'installer les dépendances de notre projet. (Comme en local)npm run build
: Cette commande permet de lancer le build de notre projet. (Comme en local)npm run lint
: Cette commande permet d'exécuter ESlint sur notre projet. (Comme en local)Pour l'action Typescript, ce sera exactement le même esprit. Au lieu de lancer la commande npm run lint
, nous allons lancer la commande npm run typecheck
.
1typecheck:2name: ʦ TypeScript3runs-on: ubuntu-latest4env:5TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}6TURBO_TEAM: ${{ secrets.TURBO_TEAM }}7TURBO_REMOTE_ONLY: true89steps:10- name: Check out code11uses: actions/checkout@v412with:13fetch-depth: 21415- name: Setup Node.js environment16uses: actions/setup-node@v417with:18node-version: 2019cache: 'npm'2021- name: Install dependencies22run: npm install2324- name: Build Application25run: npm run build2627- name: 🔎 Type check28run: npm run typecheck --if-present
Pour build l'image Docker de notre application, nous allons exécuter la commande docker build
sur les serveurs de Github. Notez que c'est la même commande que nous avions utilisée précédemment pour vérifier que notre Dockerfile fonctionnait.
Après avoir build notre image, nous allons l'héberger sur Docker Hub (dans le repository que nous venons de créer).
Notez que l'action déclarée dans notre fichier ci.yml
appelle un autre fichier build.yml
du fichier .github/workflows
.
Voici le fichier build.yml
:
1name: 🐳 Build And Push Docker Image2on:3workflow_call:4inputs:5tag:6type: string7description: The tag to push to the Docker registry.89jobs:10build:11name: 🐳 Build12if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') && github.event_name == 'push' }}13runs-on: ubuntu-latest14steps:15- name: ⬇️ Checkout repo16uses: actions/checkout@v4.1.11718- name: 🧑💻 Login to Docker Hub19uses: docker/login-action@v3.0.020with:21username: ${{ secrets.DOCKERHUB_USERNAME }}22password: ${{ secrets.DOCKERHUB_TOKEN }}23logout: true2425- name: 🐳 Set up Docker Buildx26uses: docker/setup-buildx-action@v3.0.02728# Setup cache29- name: ⚡️ Cache Docker layers30uses: actions/cache@v4.0.031with:32path: /tmp/.buildx-cache33key: ${{ runner.os }}-buildx-${{ github.sha }}-${{ github.ref_name }}34restore-keys: |35${{ runner.os }}-buildx-36- name: 🐳 Build Production Image37if: ${{ github.ref == 'refs/heads/main' }}38uses: docker/build-push-action@v5.1.039with:40context: .41push: true42tags: varkoff/nestjs-remix-monorepo:production43cache-from: type=local,src=/tmp/.buildx-cache44cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new4546- name: 🐳 Build Staging Image47if: ${{ github.ref == 'refs/heads/dev' }}48uses: docker/build-push-action@v5.1.049with:50context: .51push: true52tags: varkoff/nestjs-remix-monorepo:latest53cache-from: type=local,src=/tmp/.buildx-cache54cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new5556# This ugly bit is necessary if you don't want your cache to grow forever57# till it hits GitHub's limit of 5GB.58# Temp fix59# https://github.com/docker/build-push-action/issues/25260# https://github.com/moby/buildkit/issues/189661- name: 🚚 Move cache62run: |63rm -rf /tmp/.buildx-cache64mv /tmp/.buildx-cache-new /tmp/.buildx-cache
Cette action possède des instructions en plus :
if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') && github.event_name == 'push' }}
: Cette instruction permet de définir que notre job ne sera exécuté que si le push est sur la branche main ou dev. (Pour ce faire, il faut bien sûr avoir nommer vos branches Github de cette manière). Pour tout push sur une autre branche, ce code ne sera pas exécuté.actions/checkout@v4.1.1
: Cette instruction permet de récupérer le code source de notre projet. (comme pour les commandes précédentes).uses: docker/login-action@v3.0.0
: Cette instruction permet de se connecter à Docker Hub. Pour ce faire, il faut générer un token ici https://app.docker.com/settings/personal-access-tokens.uses: docker/setup-buildx-action@v3.0.0
: Cette instruction permet de configurer le buildx de Docker. (Le client Docker multi-arch).actions/cache@v4.0.0
: Cette instruction permet de mettre en cache les layers de Docker. (Pour accélérer le processus de build, qui est parfois très long...).uses: docker/build-push-action@v5.1.0
: Cette instruction permet de build et de push notre image Docker sur Docker Hub.Ensuite, nous dupliquons la dernière étape de notre job pour build l'image en production et en staging.
Voici l'instruction pour build l'image en production :
1- name: 🐳 Build Production Image2if: ${{ github.ref == 'refs/heads/main' }}3uses: docker/build-push-action@v5.1.04with:5context: .6push: true7tags: varkoff/nestjs-remix-monorepo:production8cache-from: type=local,src=/tmp/.buildx-cache9cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new
Notez l'instruction if: ${{ github.ref == 'refs/heads/main' }}
signifiant que cette étape sera ignorée lors d'un push sur la branche dev
.
Ensuite, nous définissons le contexte (similaire à la commande docker build -t varkoff/nestjs-remix-monorepo:production .
) avec context: .
et tags: varkoff/nestjs-remix-monorepo:production
. L'élément push: true
permet de push notre image sur Docker Hub.
Cette action va échouer si vos identifiants Docker Hub ne sont pas bons, ou si une erreur se produisait dans le build (par exemple, une erreur de syntaxe dans notre Dockerfile).
Maintenant, à chaque nouveau push sur les branches main
ou dev
, Github va exécuter notre workflow. Il va donc valider le code avec Typescript et ESlint, et il va build et push notre image Docker sur Docker Hub.
Une fois ces trois étapes passées sans erreur, notre image sera disponible sur Docker Hub.
La dernière action sera de télécharger cette image depuis Docker Hub et l'exécuter sur notre serveur VPS. Nous configurons un serveur VPS hébergé chez Hostinger dans la prochaîne leçon.
Voici l'action qui va permettre de déployer notre application :
1deploy:2name: 🚀 Deploy3runs-on: [self-hosted]4needs: [build, lint, typecheck]5if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') && github.event_name == 'push' }}6env:7TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}8TURBO_TEAM: ${{ secrets.TURBO_TEAM }}9TURBO_REMOTE_ONLY: true1011steps:12- name: Cache node modules13uses: actions/cache@v4.0.014with:15path: ~/.npm16key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}17restore-keys: |18${{ runner.os }}-node-1920- name: ⬇️ Checkout repo21uses: actions/checkout@v4.1.12223- name: Login to Docker Hub24uses: docker/login-action@v225with:26username: ${{ secrets.DOCKERHUB_USERNAME }}27password: ${{ secrets.DOCKERHUB_TOKEN }}28- name: 🚀 Run Docker Compose on Staging29if: ${{ github.ref == 'refs/heads/dev' }}30env:31NODE_ENV: staging32run: |33docker pull varkoff/nestjs-remix-monorepo:latest34docker compose -f docker-compose.staging.yaml up -d35docker system prune --all --volumes --force3637- name: 🚀 Run Docker Compose on Production38if: ${{ github.ref == 'refs/heads/main' }}39env:40NODE_ENV: production41DATABASE_URL: ${{ secrets.DATABASE_URL }}42run: |43docker pull varkoff/nestjs-remix-monorepo:production44docker compose -f docker-compose.prod.yml up -d45docker system prune --all --volumes --force
Analysons ensemble les différences.
Nous retrouvons deux instructions supplémentaires :
needs: [build, lint, typecheck]
: Cette instruction permet de définir que notre job ne sera exécuté que si les jobs build
, lint
et typecheck
ont tous réussi. Le cas échéant, le code source ne sera pas mis à jour sur la prod.
runs-on: [self-hosted]
: Cette instruction permet de définir que notre job va être exécuté sur notre serveur VPS. Les serveurs de Github sont temporaires pour vérifier la qualité du code source, mais nous devons louer un serveur VPS qui exécutera en permanence notre application.
Voici les étapes :
action/cache@v4.0.0
: Cette instruction permet de mettre en cache les dépendances de notre projet. (Comme en local)actions/checkout@v4.1.1
: Cette instruction permet de récupérer le code source de notre projet, comme pour les actions précédentes.docker/login-action@v2
: Cette instruction permet de se connecter à Docker Hub (pour être autorisé à télécharger notre image privée)docker compose -f docker-compose.prod.yml up -d
: Cette instruction permet de démarrer notre application en production, en utilisant le fichier docker-compose.prod.yml
que nous avons défini précédemment.docker system prune --all --volumes --force
: Cette instruction permet de nettoyer notre serveur VPS. (Pour éviter d'utiliser toute la capacité de stockage de notre serveur)Une fois notre workflow entièrement terminé, notre application sera disponible sur notre serveur VPS. Cependant, nous devons maintenant configurer un serveur VPS (pour exécuter le code source) et configurer notre domaine.
Pour déclencher nos actions, nous devons faire un commit et un push sur notre repository Github. Il faut au préalable avoir créé un repository sur Github.
1git add .2git commit -m "Deploy"3git push origin HEAD # sur la branche main ou dev
Une fois ces commandes exécutées, notre workflow sera déclenché.
Pour consulter l'avancement de notre workflow, il faut aller sur la page du repository Github, dans l'onglet Actions
.
Si l'on clique sur l'action que nous venons de déclencher, la plupart de nos actions devraient avoir réussies. À l'exception de l'action de déploiement, qui nécessite un accès à notre serveur.
On va le configurer tout de suite.