Comment créer une GitHub Action pour lancer des tests avec des services ?
Je creuse GitHub Actions depuis que j'ai eu accès à la bêta. GitHub Actions est disponible pour tout le monde depuis le 11 novembre 2019, et je peux vous l'expliquer. 🎉
Une autre version française de l'article est disponible ici.
Photo de Yancy Min sur Unsplash
Qu'est-ce que c'est ?
GitHub Actions est une solution d'intégration continue et de déploiement continu (CI/CD). C'est une nouvelle fonctionnalité de GitHub, qui s'affiche sous forme d'onglet dans votre dépôt. Ses opérations sont déclenchées par des événements Git, comme le push d'un commit. Pour créer une nouvelle séquence d'événements à exécuter lors d'un push, il faut créer un ensemble de commandes qui seront exécutées selon les spécificités du projet. Une fois mis en place, ce service est conçu pour exécuter des processus automatiquement et en continu pour le travail collaboratif sur un dépôt Git. Il faut noter que GitHub Actions repose sur Docker, il est donc recommandé d'avoir quelques connaissances à ce sujet pour l'utiliser.
Écrire son action
Une action contient plusieurs éléments nécessaires à l'exécution d'un processus : l'événement on, qui correspond à l'événement GitHub : push, pull request, etc. Pendant la bêta, seuls les déclencheurs push et pull request sont disponibles.
on: [push, pull_request]
Il est bien sûr possible de définir la portée de votre action, pour qu'elle effectue des tâches précises sur certaines branches de votre dépôt, mais pas sur d'autres. Par exemple, un déploiement automatique sur un serveur depuis la branche staging, mais pas depuis la branche dev.
on: push: branches: - staging
runs-on: ubuntu-latest
Il existe plusieurs environnements virtuels possibles pour l'exécution de votre action, ce qui peut être utile pour correspondre aux spécificités de chacun. D'Ubuntu 19.10 à Windows 2016, vous avez le choix 😉
Il faut noter que chaque job est exécuté dans une nouvelle instance de l'environnement virtuel.
Donc, si vous voulez effectuer une série d'actions liées entre elles, il vaut peut-être mieux le faire dans le même job. Mais il est possible de créer des dépendances entre jobs avec le paramètre need :
jobs: job1: job2: need: job1
Dans ce cas, job2 ne s'exécutera qu'une fois job1 terminé.
Ensuite, il y a toutes les étapes à réaliser pendant le processus. Dans une optique d'intégration continue, le but est de lancer les tests automatiquement. Mais un certain nombre d'éléments doivent être définis avant de pouvoir lancer les tests !
Cet ensemble est défini par le mot-clé steps. Chaque étape nécessite au moins un élément à exécuter, c'est-à-dire l'un des éléments suivants :
le mot-clé uses, pour utiliser un élément précis :
- une action publique
- une action dans le même dépôt
- une action sur le Docker Hub ou sur le registre public Docker
ou
le mot-clé run, pour lancer une commande en bash (sachant qu'il existe plusieurs façons d'exécuter une commande si nécessaire)
Enfin, on associe le paramètre name pour savoir quelle étape il représente. Un exemple étant toujours plus parlant, voici un job pour tester un projet Django :
jobs: tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Set up Python 3.5.7 - uses: actions/setup-python@v1 with: python-version: 3.5.7 - name: Install dependencies run: pip install -r requirements.txt - name: Run tests run: python manage.py test
Ici, le job tests consiste à :
- installer Python 3.5.7 sur l'environnement Ubuntu
- installer les dépendances
- lancer les tests
Pour tester en conditions réelles, il faut une base de données, d'où la mise en place de services.
Ajouter un service à son action
Comment fait-on ? C'est très simple, c'est similaire à la définition d'un service dans un docker-compose.yml, mais avec beaucoup moins de paramètres.
On utilise le mot-clé services avec :
- la définition de l'image Docker à utiliser
- la liste des ports du service à exposer
- une liste de variables d'environnement
- une liste de volumes
services: image: postgres env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_BD: postgres ports: - 5432/tcp options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
Il nous faut l'image Docker à lancer, le port à exposer et le nom du service. Il est possible de transmettre des variables d'environnement ou des options Docker, comme dans l'exemple ci-dessus. Les valeurs sensibles peuvent être définies avec une clé secrète.
De plus, il est parfois nécessaire d'attendre un retour sur l'état du service pour établir une connexion. On définit donc un élément qui vérifie l'état de santé du service pour savoir quand il est opérationnel (on parle aussi de health check). J'ai repris l'exemple réalisé par Chris Patterson et Mike Coutermarsh pour PostgreSQL.
Si vous n'avez pas de health check disponible, on met en pause l'exécution du processus avec la commande sleep. Oui, ce n'est pas terrible, je vous l'accorde...
Tout ça, c'est très bien, mais il faut faire le lien entre le service et notre application. On le fera via le port, et pour l'hôte, ce sera localhost, puisque le service est exécuté directement dans la machine Ubuntu dans notre exemple. Sinon, ça aurait été le nom du service.
Dans l'exemple précédent, un port libre de la machine est attribué aléatoirement au port 5432, et on y accède comme ceci :
${{ job.services.postgres.ports[5432] }}
Néanmoins, on peut aussi l'attribuer de manière fixe :
ports: - 5432:5432
Ici, le port 5432 du service est relié au port 5432 de la machine définie dans l'action.
Le workflow suivant est mis en place :
name: Test Workflow on: push jobs: tests: runs-on: ubuntu-latest services: image: postgres env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_BD: postgres ports: - 5432:5432 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v1 - name: Set up Python 3.5.7 - uses: actions/setup-python@v1 with: python-version: 3.5.7 - name: Install dependencies run: pip install -r requirements.txt - name: Run tests run: python manage.py test
Dans notre exemple, il faut quand même surcharger les valeurs du service dans le settings.py de Django pour la connexion à la base de données PostgreSQL.
Et le tour est joué, il ne reste plus qu'à pousser du code et espérer que ça marche. 😉