Configurer Github Actions

Nous configurons Github Actions pour exécuter des tests avant de mettre à jour notre code source sur les serveurs de production.

12 min read

Comprendre les Github Actions

Les Github Actions nous permettent d'exécuter les scripts du projet automatiquement.

Par scripts, j'entend :

  • tests unitaires
  • exécuter Typescript pour détecter les erreurs de type
  • exécuter ESlint pour détecter les erreurs de code

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.

Pré-requis pour configurer nos actions

Pour configurer nos actions Github, nous avons besoin de :

Créer notre premier workflow

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.

Terminal
1
mkdir .github

À l'intérieur, nous allons créer un fichier dossier nommé workflows.

Terminal
1
cd .github && mkdir workflows

Enfin, nous allons créer un fichier dans ce dossier, nommé ci.yml.

Terminal
1
touch .github/workflows/ci.yml

Ce fichier va contenir la configuration de notre workflow.

Voici son contenu :

.github/workflows/ci.yml
1
name: 🚀 Deploy
2
on:
3
push:
4
branches:
5
- main
6
- dev
7
pull_request: {}
8
9
permissions:
10
actions: write
11
contents: read
12
13
jobs:
14
lint:
15
name: ⬣ ESLint
16
runs-on: ubuntu-latest
17
env:
18
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
19
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
20
TURBO_REMOTE_ONLY: true
21
22
steps:
23
- name: Check out code
24
uses: actions/checkout@v4
25
with:
26
fetch-depth: 2
27
28
- name: Setup Node.js environment
29
uses: actions/setup-node@v4
30
with:
31
node-version: 20
32
cache: 'npm'
33
34
- name: Install dependencies
35
run: npm install
36
37
- name: Build Application
38
run: npm run build
39
40
- name: 🔬 Lint
41
run: npm run lint
42
43
typecheck:
44
name: ʦ TypeScript
45
runs-on: ubuntu-latest
46
env:
47
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
48
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
49
TURBO_REMOTE_ONLY: true
50
51
steps:
52
- name: Check out code
53
uses: actions/checkout@v4
54
with:
55
fetch-depth: 2
56
57
- name: Setup Node.js environment
58
uses: actions/setup-node@v4
59
with:
60
node-version: 20
61
cache: 'npm'
62
63
- name: Install dependencies
64
run: npm install
65
66
- name: Build Application
67
run: npm run build
68
69
- name: 🔎 Type check
70
run: npm run typecheck --if-present
71
72
build:
73
name: 🐳 build
74
uses: ./.github/workflows/build.yml
75
secrets: inherit
76
77
deploy:
78
name: 🚀 Deploy
79
runs-on: [self-hosted]
80
needs: [build, lint, typecheck]
81
if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') && github.event_name == 'push' }}
82
env:
83
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
84
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
85
TURBO_REMOTE_ONLY: true
86
87
steps:
88
- name: Cache node modules
89
uses: actions/cache@v4.0.0
90
with:
91
path: ~/.npm
92
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
93
restore-keys: |
94
${{ runner.os }}-node-
95
96
- name: ⬇️ Checkout repo
97
uses: actions/checkout@v4.1.1
98
99
- name: Login to Docker Hub
100
uses: docker/login-action@v2
101
with:
102
username: ${{ secrets.DOCKERHUB_USERNAME }}
103
password: ${{ secrets.DOCKERHUB_TOKEN }}
104
- name: 🚀 Run Docker Compose on Staging
105
if: ${{ github.ref == 'refs/heads/dev' }}
106
env:
107
NODE_ENV: staging
108
run: |
109
docker pull varkoff/nestjs-remix-monorepo:latest
110
docker compose -f docker-compose.staging.yaml up -d
111
docker system prune --all --volumes --force
112
113
- name: 🚀 Run Docker Compose on Production
114
if: ${{ github.ref == 'refs/heads/main' }}
115
env:
116
NODE_ENV: production
117
DATABASE_URL: ${{ secrets.DATABASE_URL }}
118
run: |
119
docker pull varkoff/nestjs-remix-monorepo:production
120
docker compose -f docker-compose.prod.yml up -d
121
docker system prune --all --volumes --force

Analysons ce fichier.

Anatomie d'une Action Github

Les déclarations en début de fichier ont la signification suivante :

.github/workflows/ci.yml
1
name: 🚀 Deploy
2
on:
3
push:
4
branches:
5
- main
6
- dev
7
pull_request: {}
8
9
permissions:
10
actions: write
11
contents: read
  • "name": Le nom de notre workflow.
  • "on": Les événements qui vont déclencher notre workflow. Ici, un 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.
  • "permissions": Les permissions que notre workflow va utiliser. 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.

Comment utiliser ESlint et Typescript dans une Github Actions

Pour utiliser ESlint dans une Github Actions, nous allons définir un job nommé lint.

.github/workflows/ci.yml
1
lint:
2
name: ⬣ ESLint
3
runs-on: ubuntu-latest
4
env:
5
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
6
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
7
TURBO_REMOTE_ONLY: true
8
9
steps:
10
- name: Check out code
11
uses: actions/checkout@v4
12
with:
13
fetch-depth: 2
14
15
- name: Setup Node.js environment
16
uses: actions/setup-node@v4
17
with:
18
node-version: 20
19
cache: 'npm'
20
21
- name: Install dependencies
22
run: npm install
23
24
- name: Build Application
25
run: npm run build
26
27
- name: 🔬 Lint
28
run: 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.

.github/workflows/ci.yml
1
typecheck:
2
name: ʦ TypeScript
3
runs-on: ubuntu-latest
4
env:
5
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
6
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
7
TURBO_REMOTE_ONLY: true
8
9
steps:
10
- name: Check out code
11
uses: actions/checkout@v4
12
with:
13
fetch-depth: 2
14
15
- name: Setup Node.js environment
16
uses: actions/setup-node@v4
17
with:
18
node-version: 20
19
cache: 'npm'
20
21
- name: Install dependencies
22
run: npm install
23
24
- name: Build Application
25
run: npm run build
26
27
- name: 🔎 Type check
28
run: npm run typecheck --if-present

Comment build une image Docker dans une Github Actions

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 :

.github/workflows/build.yml
1
name: 🐳 Build And Push Docker Image
2
on:
3
workflow_call:
4
inputs:
5
tag:
6
type: string
7
description: The tag to push to the Docker registry.
8
9
jobs:
10
build:
11
name: 🐳 Build
12
if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') && github.event_name == 'push' }}
13
runs-on: ubuntu-latest
14
steps:
15
- name: ⬇️ Checkout repo
16
uses: actions/checkout@v4.1.1
17
18
- name: 🧑‍💻 Login to Docker Hub
19
uses: docker/login-action@v3.0.0
20
with:
21
username: ${{ secrets.DOCKERHUB_USERNAME }}
22
password: ${{ secrets.DOCKERHUB_TOKEN }}
23
logout: true
24
25
- name: 🐳 Set up Docker Buildx
26
uses: docker/setup-buildx-action@v3.0.0
27
28
# Setup cache
29
- name: ⚡️ Cache Docker layers
30
uses: actions/cache@v4.0.0
31
with:
32
path: /tmp/.buildx-cache
33
key: ${{ runner.os }}-buildx-${{ github.sha }}-${{ github.ref_name }}
34
restore-keys: |
35
${{ runner.os }}-buildx-
36
- name: 🐳 Build Production Image
37
if: ${{ github.ref == 'refs/heads/main' }}
38
uses: docker/build-push-action@v5.1.0
39
with:
40
context: .
41
push: true
42
tags: varkoff/nestjs-remix-monorepo:production
43
cache-from: type=local,src=/tmp/.buildx-cache
44
cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new
45
46
- name: 🐳 Build Staging Image
47
if: ${{ github.ref == 'refs/heads/dev' }}
48
uses: docker/build-push-action@v5.1.0
49
with:
50
context: .
51
push: true
52
tags: varkoff/nestjs-remix-monorepo:latest
53
cache-from: type=local,src=/tmp/.buildx-cache
54
cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new
55
56
# This ugly bit is necessary if you don't want your cache to grow forever
57
# till it hits GitHub's limit of 5GB.
58
# Temp fix
59
# https://github.com/docker/build-push-action/issues/252
60
# https://github.com/moby/buildkit/issues/1896
61
- name: 🚚 Move cache
62
run: |
63
rm -rf /tmp/.buildx-cache
64
mv /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 :

.github/workflows/build.yml
1
- name: 🐳 Build Production Image
2
if: ${{ github.ref == 'refs/heads/main' }}
3
uses: docker/build-push-action@v5.1.0
4
with:
5
context: .
6
push: true
7
tags: varkoff/nestjs-remix-monorepo:production
8
cache-from: type=local,src=/tmp/.buildx-cache
9
cache-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).

Comment déployer notre application

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 :

.github/workflows/ci.yml
1
deploy:
2
name: 🚀 Deploy
3
runs-on: [self-hosted]
4
needs: [build, lint, typecheck]
5
if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') && github.event_name == 'push' }}
6
env:
7
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
8
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
9
TURBO_REMOTE_ONLY: true
10
11
steps:
12
- name: Cache node modules
13
uses: actions/cache@v4.0.0
14
with:
15
path: ~/.npm
16
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
17
restore-keys: |
18
${{ runner.os }}-node-
19
20
- name: ⬇️ Checkout repo
21
uses: actions/checkout@v4.1.1
22
23
- name: Login to Docker Hub
24
uses: docker/login-action@v2
25
with:
26
username: ${{ secrets.DOCKERHUB_USERNAME }}
27
password: ${{ secrets.DOCKERHUB_TOKEN }}
28
- name: 🚀 Run Docker Compose on Staging
29
if: ${{ github.ref == 'refs/heads/dev' }}
30
env:
31
NODE_ENV: staging
32
run: |
33
docker pull varkoff/nestjs-remix-monorepo:latest
34
docker compose -f docker-compose.staging.yaml up -d
35
docker system prune --all --volumes --force
36
37
- name: 🚀 Run Docker Compose on Production
38
if: ${{ github.ref == 'refs/heads/main' }}
39
env:
40
NODE_ENV: production
41
DATABASE_URL: ${{ secrets.DATABASE_URL }}
42
run: |
43
docker pull varkoff/nestjs-remix-monorepo:production
44
docker compose -f docker-compose.prod.yml up -d
45
docker 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.

Comment déclencher une Github Action

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.

Terminal
1
git add .
2
git commit -m "Deploy"
3
git 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.

Voir nos actions Github

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.

Github Actions ayant échoué

On va le configurer tout de suite.