Build reproductible localement
Quand les tests sont verts en local et rouge en CI, on se retrouve dans une situation délicate ... ce serait dommage de commiter n fois en espérant mettre le doigt sur le problème, ou pire, dire que "ça marche sur mon poste !". 😅
Il peut être intéressant d'utiliser un outil pour envelopper le processus de build afin de se donner la capacité de le lancer en local, ainsi une erreur serait reproductible. Pour les projets construisant des artefacts comme des binaires, l'outil permettra en plus aux développeurs de les construire de manière iso sur leur poste !
* ba3f522 fix: debug ci
* 21a5524 fix: debug ci
* 4546123 fix: debug ci
* 2377c0a fix: debug ci
* 30af397 fix: debug ci
* 86ea9b2 fix: debug ci
* b18f30f fix: debug ci
* ... 😰
Le cahier des charges
L'outil devrait généralement être capable de :
- Lancer des commandes dans un environnement isolé
- Gérer du cache
- Gérer les dépendances privées facilement
- Lancer des services dépendants, avec une configuration indépendante
- Récupérer des artéfacts
Quelles solutions ?
J'ai pu rencontrer les solutions suivantes :
- docker compose + script bash
- Wercker
- Gitlab runner
- Earthly
Le script bash prend du temps à écrire, à maintenir et est spécifique. 🤦♂️
Wercker est un bon outil mais il est lent et plus maintenu. 🤦♂️
Comme Wercker, le runner gitlab est intéressant, mais il ne prend en compte que les changements commités. 🤦♂️
En ce moment, j'utilise Earthly
pour lequel je n'ai pas encore trouvé de défaut. Je vous propose de regarder un exemple, sans trop paraphraser la documentation. ✨
Earthly
Les usages courants nécessitent simplement l'écriture d'un Earthfile
, avec une syntaxe à mi-chemin entre le Dockerfile
et le Makefile
, un exemple très simple recoupant notre cahier des charges :
FROM organization/php:8.0
WORKDIR /code
deps:
ENV XDG_CACHE_HOME=/code/build/cache
COPY --dir . .
RUN --ssh make install
SAVE ARTIFACT ./build/cache AS LOCAL ./build/cache
lint:
FROM +deps
RUN make lint
test:
FROM +deps
WITH DOCKER \
--compose docker-compose.yml \
--service postgres \
RUN while ! pg_isready --host=localhost --port=5432 --dbname=request --username=idea; do sleep 1; done ;\
make test
END
SAVE ARTIFACT ./build/coverage AS LOCAL ./build/coverage
SAVE ARTIFACT ./testreport.xml AS LOCAL ./testreport.xml
Environnement isolé
D'entrée de jeu, on remarque l'instruction FROM
qui définit l'image docker de base dans laquelle seront lancé les commandes.
Dépendances privées
Les dépendances privées nécessitent un moyen de s'authentifier, Earthly
propose de se binder directement sur votre ssh local, via la variable d'environnement SSH_AUTH_SOCK
. Il s'agit du flag --ssh
dans la cible deps:
.
Cache
Pour cacher les dépendances installées, on va aller surcharger la configuration du gestionnaire de dépendances. Ici, composer
suit la XDG Base Directory Specification on peut alors exploiter la variable d'environnement XDG_CACHE_HOME
. Le cache est alors traité comme un artefact et sera copié lors d'un lancement n+1
.
Lancer des services dépendants
On remarque que la cible test:
utilise un fichier docker-compose.yml: WITH DOCKER
! On lance dans le cas présent uniquement un service postgres
, qui sera rendu accessible depuis notre image spécifiée lors du FROM
.
Artefacts
Comme déjà abordé pour le cache, Earthly nous permet de sauvegarder des artefacts: SAVE ARTEFACT AS LOCAL
, comme notre couverture de code ou un rapport de test. Ces fichiers seront donc accessibles en local, et potentiellement exploitable par votre service de build.
Le local OK, mais le serveur de build maintenant ?
La documentation détaille plusieurs exemples d'intégration avec des services de build : https://docs.earthly.dev/ci-integration/vendor-specific-guides.
Je vous rajoute une proposition pour gitlab-ci
ci-dessous :
image: organization/image:tag
services:
- docker:dind
cache:
key: $CI_PROJECT_ID
paths:
- ./build/cache
before_script:
- docker info
lint:
script:
- ./build.sh "+lint"
test:
script:
- ./build.sh "+test"
artifacts:
paths:
- ./build/coverage
- ./build/testreport.xml
expire_in: 30 days
reports:
junit: ./build/testreport.xml
Et je vous mets le script ./build.sh
qui facilite l'utilisation d'Earthly
aussi bien en local qu'en ci 🙃.
#!/usr/bin/env bash
set -e
pushd $(dirname $0) 1> /dev/null
EARTHLY='./build/bin/earthly'
mkdir -p $(dirname $EARTHLY)
if [ ! -e $EARTHLY ] && [ $(uname) = "Darwin" ]
then
curl -L https://github.com/earthly/earthly/releases/download/v0.6.6/earthly-darwin-amd64 -o $EARTHLY
fi
if [ ! -e $EARTHLY ] && [ $(uname) = "Linux" ]
then
curl -L https://github.com/earthly/earthly/releases/download/v0.6.6/earthly-linux-amd64 -o $EARTHLY
fi
if [ ! -f "$EARTHLY" ]; then
echo "Your system may not be supported"
popd
exit 1
fi
chmod +x $EARTHLY
TIMESTAMP=$(date +%s)
export FORCE_COLOR=1
$EARTHLY --allow-privileged --verbose $@ 2>&1 | tee ./build/$TIMESTAMP.log
echo "Log written in ./build/$TIMESTAMP.log"
popd 1> /dev/null
La chaine d'outils
L'exemple présenté respecte la chaine d'outillage résumé ci-dessus. Afin de garder une consistance et un interêt dans la mise en place d'un outil comme Earthly
, il faut bien entendu s'assurer qu'elle soit respectée.
Conclusion
Je pense qu'utiliser un outil de ce genre à un réel intérêt méthodologique. En plus de les rendre reproductibles, ils apportent un point de découplage vous permettant de changer votre solution de build (oui oui, on est d'accord que c'est pas tous les jours 😅) mais surtout vous permets de concevoir, tester, debugger vos pipelines localement, en vous affranchissant de votre gestion des sources, fini les commits debug ci
par dizaine !