Pourquoi GitHub Actions : guide pratique ?
Contexte réel : pourquoi un dev a besoin de ca au quotidien
GitHub Actions est une outil puissant et flexible qui permet aux développeurs d'automatiser leurs workflows, des tests automatisés jusqu'à la mise en production. C'est essentiel pour garantir que le code fonctionne correctement à chaque étape du processus de développement. Un cas d'utilisation concret serait : vous avez un projet open source et vous voulez s'assurer que toutes les nouvelles contributions passent par des tests automatisés avant d'être fusionnées dans la branche principale.
Prerequis
- Connaissances en gestion de projets avec Git
- Familiarité avec le langage de programmation du projet (ex: JavaScript, Python)
- Compréhension de l'environnement de développement local et cloud
Outils à installer (versions)
- Git : 2.34.1 ou ultérieur
- Node.js : 16.14.0 ou ultérieur (pour les projets JavaScript)
- Python : 3.9.7 ou ultérieur (pour les projets Python)
Concepts fondamentaux
Workflow
Un workflow est une série d'étapes définies par un développeur qui sont exécutées dans le cloud à chaque événement spécifique déclenché, comme la création d'une pull request ou le push d'un commit.
## workflows/hello-world.yml
name: Hello World
on:
push:
branches: [ main ]
Job
Un job est un ensemble de tâches qui sont exécutées en parallèle et sont associés à un événement spécifique. Par exemple, vous pouvez avoir deux jobs : l'un pour les tests locaux et l'autre pour la mise en production.
## workflows/hello-world.yml
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
Step
Une étape est une tâche spécifique qui peut être exécutée dans un job. Par exemple, vous pouvez installer des dépendances avec npm install ou exécuter des tests avec npm test.
## workflows/hello-world.yml
- name: Install dependencies
run: npm install
Action
Une action est une composante réutilisable qui peut être ajoutée à un workflow. Par exemple, l'action actions/checkout permet de récupérer le code du dépôt.
## workflows/hello-world.yml
- name: Checkout code
uses: actions/checkout@v2
Mise en pratique : projet fil rouge
Mini-projet complet et réaliste : un gestionnaire de tâches basé sur Node.js
Initialisation du projet
mkdir task-manager cd task-manager npm init -y npm install express body-parser dotenvCréation des fichiers et dossiers
/task-manager ├── .env ├── server.js └── routes/ └── tasks.jsConfiguration de l'environnement (
/.env)PORT=3000 DB_URL=mongodb://localhost:27017/task-managerCréation du serveur (
/server.js)// server.js const express = require('express'); const bodyParser = require('body-parser'); const tasksRouter = require('./routes/tasks'); const app = express(); const port = process.env.PORT || 3000; app.use(bodyParser.json()); app.use('/api/tasks', tasksRouter); app.listen(port, () => { console.log(`Server is running on port ${port}`); });Création des routes pour les tâches (
/routes/tasks.js)// routes/tasks.js const express = require('express'); const router = express.Router(); const Task = require('../models/task'); router.get('/', async (req, res) => { try { const tasks = await Task.find(); res.json(tasks); } catch (error) { res.status(500).json({ message: error.message }); } }); router.post('/', async (req, res) => { const task = new Task({ title: req.body.title, description: req.body.description }); try { const newTask = await task.save(); res.status(201).json(newTask); } catch (error) { res.status(400).json({ message: error.message }); } }); module.exports = router;Création du modèle de tâche (
/models/task.js)// models/task.js const mongoose = require('mongoose'); const taskSchema = new mongoose.Schema({ title: { type: String, required: true, trim: true }, description: { type: String, trim: true } }); const Task = mongoose.model('Task', taskSchema); module.exports = Task;Création du workflow (
/.github/workflows/nodejs.yml)# .github/workflows/nodejs.yml name: Node.js CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [14.x, 16.x] steps: - uses: actions/checkout@v2 - name: Use Node.js $matrix.node-version uses: actions/setup-node@v2 with: node-version: $matrix.node-version - run: npm install - run: npm test
Mise en pratique : workflow pour une API de blog
Initialisation du projet
mkdir blog-api cd blog-api npm init -y npm install express body-parser mongooseCréation des fichiers et dossiers
/blog-api ├── .env ├── server.js └── routes/ └── posts.jsConfiguration de l'environnement (
/.env)PORT=3000 DB_URL=mongodb://localhost:27017/blog-apiCréation du serveur (
/server.js)// server.js const express = require('express'); const bodyParser = require('body-parser'); const postsRouter = require('./routes/posts'); const app = express(); const port = process.env.PORT || 3000; app.use(bodyParser.json()); app.use('/api/posts', postsRouter); app.listen(port, () => { console.log(`Server is running on port ${port}`); });Création des routes pour les articles (
/routes/posts.js)// routes/posts.js const express = require('express'); const router = express.Router(); const Post = require('../models/post'); router.get('/', async (req, res) => { try { const posts = await Post.find(); res.json(posts); } catch (error) { res.status(500).json({ message: error.message }); } }); router.post('/', async (req, res) => { const post = new Post({ title: req.body.title, content: req.body.content }); try { const newPost = await post.save(); res.status(201).json(newPost); } catch (error) { res.status(400).json({ message: error.message }); } }); module.exports = router;Création du modèle d'article (
/models/post.js)// models/post.js const mongoose = require('mongoose'); const postSchema = new mongoose.Schema({ title: { type: String, required: true, trim: true }, content: { type: String, required: true, trim: true } }); const Post = mongoose.model('Post', postSchema); module.exports = Post;Création du workflow (
/.github/workflows/nodejs.yml)# .github/workflows/nodejs.yml name: Node.js CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [14.x, 16.x] steps: - uses: actions/checkout@v2 - name: Use Node.js $matrix.node-version uses: actions/setup-node@v2 with: node-version: $matrix.node-version - run: npm install - run: npm test
Erreurs fréquentes et debugging
Erreur 1 : Error: ENOENT: no such file or directory, open '/path/to/file.js'
Code incorrect :
// server.js
const express = require('express');
const bodyParser = require('body-parser');
const tasksRouter = require('./routes/tasks');
const app = express();
const port = process.env.PORT || 3000;
app.use(bodyParser.json());
app.use('/api/tasks', tasksRouter);
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
Code correct :
// server.js
const express = require('express');
const bodyParser = require('body-parser');
const tasksRouter = require('./routes/tasks');
const app = express();
const port = process.env.PORT || 3000;
app.use(bodyParser.json());
app.use('/api/tasks', tasksRouter);
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
Erreur 2 : Error: EACCES: permission denied, open '/path/to/file.js'
Code incorrect :
// server.js
const express = require('express');
const bodyParser = require('body-parser');
const tasksRouter = require('./routes/tasks');
const app = express();
const port = process.env.PORT || 3000;
app.use(bodyParser.json());
app.use('/api/tasks', tasksRouter);
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
Code correct :
// server.js
const express = require('express');
const bodyParser = require('body-parser');
const tasksRouter = require('./routes/tasks');
const app = express();
const port = process.env.PORT || 3000;
app.use(bodyParser.json());
app.use('/api/tasks', tasksRouter);
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
Erreur 3 : Error: Cannot find module 'express'
Code incorrect :
// server.js
const express = require('express');
const bodyParser = require('body-parser');
const tasksRouter = require('./routes/tasks');
const app = express();
const port = process.env.PORT || 3000;
app.use(bodyParser.json());
app.use('/api/tasks', tasksRouter);
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
Code correct :
// server.js
const express = require('express');
const bodyParser = require('body-parser');
const tasksRouter = require('./routes/tasks');
const app = express();
const port = process.env.PORT || 3000;
app.use(bodyParser.json());
app.use('/api/tasks', tasksRouter);
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
Pour aller plus loin
Travailler avec des environnements de développement cloud : Vous pouvez utiliser GitHub Actions pour exécuter vos workflows sur des environnements de développement cloud tels que AWS, Azure ou Google Cloud.
Utiliser des actions personnalisées : Créez vos propres actions pour répondre à des besoins spécifiques de votre projet et les partager avec la communauté GitHub.
Intégrer des tests d'intégration et de bout en bout : Ajoutez des workflows pour exécuter des tests d'intégration et de bout en bout sur chaque push ou pull request.
Défi pratique : Créer un scraper simple avec GitHub Actions
Initialisation du projet
mkdir web-scraper cd web-scraper npm init -y npm install axios cheerioCréation des fichiers et dossiers
/web-scraper ├── index.jsScraping avec Axios et Cheerio (
/index.js)// index.js const axios = require('axios'); const cheerio = require('cheerio'); async function scrape() { try { const { data } = await axios.get('https://example.com'); const $ = cheerio.load(data); const title = $('title').text(); console.log(`Title: ${title}`); } catch (error) { console.error(error); } } scrape();Création du workflow (
/.github/workflows/nodejs.yml)# .github/workflows/nodejs.yml name: Node.js CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [14.x, 16.x] steps: - uses: actions/checkout@v2 - name: Use Node.js $matrix.node-version uses: actions/setup-node@v2 with: node-version: $matrix.node-version - run: npm install - run: node index.js
Explication des concepts :
- Workflow : Un workflow est une série d'étapes définies par un développeur qui sont exécutées dans le cloud à chaque événement spécifique déclenché, comme la création d'une pull request ou le push d'un commit.
- Job : Un job est un ensemble de tâches qui sont exécutées en parallèle et sont associés à un événement spécifique. Par exemple, vous pouvez avoir deux jobs : l'un pour les tests locaux et l'autre pour la mise en production.
- Step : Une étape est une tâche spécifique qui peut être exécutée dans un job. Par exemple, vous pouvez installer des dépendances avec
npm installou exécuter des tests avecnpm test. - Action : Une action est une composante réutilisable qui peut être ajoutée à un workflow. Par exemple, l'action
actions/checkoutpermet de récupérer le code du dépôt.
Utilisation des actions personnalisées
Création d'une action personnalisée (
/.github/actions/hello-world-action) :/web-scraper ├── index.js └── .github/ └── actions/ └── hello-world-action ├── entrypoint.sh └── DockerfileContenu de
entrypoint.sh:#!/bin/bash echo "Hello, GitHub Actions!"Contenu de
Dockerfile:FROM ubuntu:latest RUN apt-get update && \ apt-get install -y curl COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"]Utilisation de l'action personnalisée dans un workflow (
/.github/workflows/nodejs.yml) :name: Node.js CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Use Node.js $matrix.node-version uses: actions/setup-node@v2 with: node-version: $matrix.node-version - run: npm install - run: node index.js - name: Run Hello World Action uses: ./.github/actions/hello-world-action
Intégration des tests d'intégration et de bout en bout
Création des fichiers pour les tests (
/tests) :/web-scraper ├── index.js └── tests/ ├── integration.test.js └── e2e.test.jsContenu de
integration.test.js:const axios = require('axios'); describe('Integration Tests', () => { it('should get a valid response from the server', async () => { const { status } = await axios.get('http://localhost:3000/api/posts'); expect(status).toBe(200); }); });Contenu de
e2e.test.js:const axios = require('axios'); describe('End-to-End Tests', () => { it('should create a new post and get it back', async () => { const response = await axios.post('http://localhost:3000/api/posts', { title: 'Test Post', content: 'This is a test post.' }); expect(response.status).toBe(201); const { data } = response; const getResponse = await axios.get(`http://localhost:3000/api/posts/${data._id}`); expect(getResponse.status).toBe(200); }); });Ajout des tests au workflow (
/.github/workflows/nodejs.yml) :name: Node.js CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [14.x, 16.x] steps: - uses: actions/checkout@v2 - name: Use Node.js $matrix.node-version uses: actions/setup-node@v2 with: node-version: $matrix.node-version - run: npm install - run: npm test
Explication des concepts :
- Workflow : Un workflow est une série d'étapes définies par un développeur qui sont exécutées dans le cloud à chaque événement spécifique déclenché, comme la création d'une pull request ou le push d'un commit.
- Job : Un job est un ensemble de tâches qui sont exécutées en parallèle et sont associés à un événement spécifique. Par exemple, vous pouvez avoir deux jobs : l'un pour les tests locaux et l'autre pour la mise en production.
- Step : Une étape est une tâche spécifique qui peut être exécutée dans un job. Par exemple, vous pouvez installer des dépendances avec
npm installou exécuter des tests avecnpm test. - Action : Une action est une composante réutilisable qui peut être ajoutée à un workflow. Par exemple, l'action
actions/checkoutpermet de récupérer le code du dépôt.
Utilisation des actions personnalisées
Création d'une action personnalisée (
/.github/actions/hello-world-action) :/web-scraper ├── index.js └── .github/ └── actions/ └── hello-world-action ├── entrypoint.sh └── DockerfileContenu de
entrypoint.sh:#!/bin/bash echo "Hello, GitHub Actions!"Contenu de
Dockerfile:FROM ubuntu:latest RUN apt-get update && \ apt-get install -y curl COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"]Utilisation de l'action personnalisée dans un workflow (
/.github/workflows/nodejs.yml) :name: Node.js CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Use Node.js $matrix.node-version uses: actions/setup-node@v2 with: node-version: $matrix.node-version - run: npm install - run: node index.js - name: Run Hello World Action uses: ./.github/actions/hello-world-action
Intégration des tests d'intégration et de bout en bout
Création des fichiers pour les tests (
/tests) :/web-scraper ├── index.js └── tests/ ├── integration.test.js └── e2e.test.jsContenu de
integration.test.js:const axios = require('axios'); describe('Integration Tests', () => { it('should get a valid response from the server', async () => { const { status }