2 points par GN⁺ 2025-03-21 | Aucun commentaire pour le moment. | Partager sur WhatsApp
  • Lors du retour d’une CI complexe vers GitHub Actions, la merge queue, plusieurs runners, les builds Rust, les images Docker et les tests d’intégration se sont entremêlés, révélant un coût de débogage supérieur à celui de la configuration
  • Toute modification intégrée à main doit réussir tous les tests, les petites erreurs sont corrigées automatiquement — formatage, dépendances inutilisées, lint — et les artefacts de CI doivent être identiques aux artefacts de release
  • Pour imposer la validation à la fois avant et après la merge queue, il a fallu donner le même nom de job aux deux étapes ; sinon, les checks pouvaient rester bloqués ou des changements en échec pouvaient être fusionnés
  • Entre GITHUB_TOKEN, les permissions des workflows, les tokens personnalisés, les forks et les exceptions liées aux self-hosted runners, le modèle de sécurité devient difficile à comprendre et propice aux erreurs
  • L’exécution de conteneurs Docker, les workflows YAML et les tests locaux limités ont ralenti le développement, mais les nouveaux scripts de CI réduisent fortement le temps de fusion

Le retour d’une CI complexe à GitHub Actions

  • Ces deux dernières semaines, les scripts de CI ont été réécrits pour GitHub Actions
  • C’est la troisième refonte de la configuration CI
    • La première utilisait GitHub Actions
    • Elle est ensuite passée à Earthly
    • Earthly ayant été abandonné, elle revient à GitHub Actions
  • La CI actuelle gère ensemble la merge queue, plusieurs runners, les builds Rust, les images Docker et de lourds tests d’intégration
    • Les runners utilisés sont un mélange de self-hosted, de blacksmith.sh et de GitHub-hosted
    • Pour fusionner une seule PR, environ une heure de temps CI est consommée sur plusieurs runners parallèles

Les conditions que la CI doit satisfaire

  • Toute modification entrant dans main doit réussir tous les tests
  • Les petites erreurs, comme les problèmes de formatage, de dépendances inutilisées ou de lint, doivent être corrigées automatiquement plutôt que se solder par un échec
  • Les artefacts testés dans la CI doivent être les mêmes que les artefacts réellement publiés
  • Pour une bonne expérience développeur, la CI doit se terminer rapidement
  • GitHub Actions prend techniquement en charge ces conditions, mais le processus de configuration s’accompagne de pièges cachés, de comportements incohérents et d’un débogage difficile

Merge queue et checks d’état

  • La clé pour garder une branche main propre est la merge queue de GitHub
    • La merge queue rebase la PR sur main avant d’exécuter la CI
  • Les exécutions CI nécessaires se divisent en deux étapes
    • Exécuter la CI avant l’entrée dans la queue afin de corriger automatiquement les petits problèmes
    • Réexécuter la CI dans la queue afin de valider l’état final de fusion
  • Pour rendre les deux exécutions obligatoires dans GitHub Actions, il a fallu donner le même nom de job aux deux étapes
    • GitHub traite alors les deux exécutions comme le même check, et les deux doivent réussir pour permettre la fusion
    • Cette méthode a été trouvée après plusieurs heures de débogage grâce à une réponse Stack Overflow
  • Les autres approches peuvent conduire à un check d’état qui reste en attente avant l’entrée dans la queue sans que le job ne démarre, ou à la fusion d’un job qui aurait dû échouer dans la merge queue

Le modèle de sécurité difficile à comprendre de GitHub Actions

  • Après la récente compromission d’une GitHub Action populaire, la recommandation a été de « fixer les dépendances à un hash », mais les commentaires indiquaient que presque personne ne le fait
  • GitHub Actions dispose d’un token par défaut, GITHUB_TOKEN
    • Ce token est initialisé avec des permissions par défaut
    • Ces permissions par défaut se configurent dans Actions → General → Workflow Permissions dans les paramètres du dépôt
  • Si les permissions par défaut de GITHUB_TOKEN sont restrictives, il faut les élever pour exécuter les actions et commandes nécessaires ; si elles sont permissives, certaines permissions peuvent être retirées dans le fichier de workflow
  • Un meilleur réglage par défaut serait de partir de zéro permission et de laisser l’utilisateur ajouter uniquement celles qui sont nécessaires
  • Les types de permissions sont nombreux, et sans être expert GitHub, il est difficile de comprendre ce que chacun protège

Exceptions liées aux tokens et aux permissions

  • Pour créer automatiquement des releases GitHub avec softprops/action-gh-release, un token personnalisé CI_RELEASE est utilisé
- name: Release on GitHub
  if: env.version_exists == 'false'
  uses: softprops/action-gh-release@v2
  with:
    tag_name: v${{ env.CURRENT_VERSION }}
    generate_release_notes: true
    make_latest: true
    token: ${{ secrets.CI_RELEASE }}
  • Avec le token par défaut, la release elle-même se termine bien, mais les workflows post-release ne se déclenchent pas
    • Comme rien ne l’indique clairement, il faut trouver une issue laissée par quelqu’un ayant rencontré le même problème pour en comprendre la cause
  • Il est aussi possible d’élever les permissions dans le YAML du workflow
    • La structure consistant à élever les permissions dans le code que l’on cherche à protéger paraît étrange
  • D’après la documentation GitHub, la clé permissions permet d’ajouter ou de retirer des droits de lecture aux dépôts forkés, mais en général pas d’accorder des droits d’écriture
    • L’exception est le cas où un administrateur a sélectionné l’option Send write tokens to workflows from pull requests dans les paramètres de GitHub Actions
  • Avec autant d’exceptions et de pièges, le modèle de sécurité de GitHub Actions est puissant, mais il augmente aussi la surface d’attaque et le risque d’erreur

L’incertitude des self-hosted runners

  • La documentation GitHub ne recommande pas l’utilisation de self-hosted runners dans les dépôts publics
    • En effet, le fork d’un dépôt public peut exécuter du code dangereux sur la machine du self-hosted runner via une PR
  • GitHub propose aussi un réglage de self-hosted runner qui exige l’approbation de l’exécution des PR provenant de contributeurs externes
  • La documentation ne donne pas de réponse claire sur la sûreté de cette combinaison avec un self-hosted runner, et il n’existe pas non plus de consensus sur Internet
  • La complexité est telle qu’il reste difficile d’en être sûr à 100 %

Docker et GitHub Actions entrent en conflit

  • GitHub Actions peut exécuter des jobs dans un conteneur
    • L’avantage est de pouvoir préemballer les dépendances dans un dev container au lieu de les installer à chaque fois
  • En pratique, les problèmes de permissions de fichiers reviennent sans cesse
    • Le conteneur build les fichiers avec un utilisateur donné, mais le runner GitHub peut s’exécuter avec un autre uid/gid
    • Résultat : il peut devenir impossible d’accéder aux fichiers à l’intérieur du conteneur, au workspace GitHub ou aux répertoires hôtes temporaires
  • Le répertoire $HOME peut lui aussi ne pas correspondre
    • Le dev container peut installer les outils dans /home/ubuntu
    • Dans GitHub Actions, $HOME peut devenir /github/home
    • Les outils qui dépendent de fichiers sous $HOME peuvent alors ne pas trouver les fichiers nécessaires
  • Les actions qui interagissent avec le système hôte peuvent aussi casser
    • Le cache GitHub étant limité à 10 Go, le sticky disk action de blacksmith est utilisé pour monter un lecteur NVMe destiné au cache
    • Cela ne fonctionnait pas dans le conteneur, puis a été résolu après une correction de blacksmith.sh
  • Le champ container lui-même présente aussi des limites
    • Il est impossible d’override l’entrypoint
    • Il est également impossible d’exécuter seulement certaines steps dans le conteneur et les autres en dehors

Développement et débogage des workflows YAML

  • La logique de GitHub Actions s’écrit en YAML, et plus elle devient complexe, plus il est facile de se tromper
  • La vérification par le linter GitHub YAML de RustRover a aidé, mais une meilleure analyse statique est nécessaire
  • En local, il est difficile de tester suffisamment le comportement réel de la CI
    • act est un outil connu, mais il ne prend en charge qu’une petite partie de ce que la CI doit faire
  • La meilleure méthode de débogage a été de créer un second dépôt identique et de répéter git commit -a -m "wip" && git push test-ci branch jusqu’à ce que la CI se comporte comme prévu

Séparer les workflows et réutiliser les artefacts

  • Pour éviter de relancer tout le pipeline CI à chaque fois, les workflows individuels sont gardés petits
  • À la fin de chaque workflow, les artefacts sont téléversés, puis les workflows suivants les téléchargent afin d’éviter de reconstruire depuis le début
  • Il est possible de tester un workflow de façon isolée en téléchargeant les artefacts d’une exécution précédente
    • Cependant, lors du téléchargement depuis une exécution précédente, il faut fournir un token à l’action download-artifact
    • Ce token peut être le token par défaut, mais la raison pour laquelle il faut encore l’indiquer explicitement reste une question ouverte
  • Le fichier de workflow principal devient une chaîne appelant d’autres fichiers YAML
jobs:
  invoke-build-rust:
    name: Build Rust
    uses: ./.github/workflows/build-rust.yml
  invoke-build-java:
    name: Build Java
    uses: ./.github/workflows/build-java.yml
  invoke-tests-unit:
    name: Unit Tests
    needs: [invoke-build-rust, invoke-build-java]
    uses: ./.github/workflows/test-unit.yml
  invoke-tests-adapter:
    name: Adapter Tests
    needs: [invoke-build-rust]
    uses: ./.github/workflows/test-adapters.yml
    secrets: inherit
  invoke-build-docker:
    name: Build Docker
    needs: [invoke-build-rust, invoke-build-java]
    uses: ./.github/workflows/build-docker.yml
  invoke-tests-integration:
    name: Integration Tests
    needs: [invoke-build-docker]
    uses: ./.github/workflows/test-integration.yml
  invoke-tests-java:
    name: Java Tests
    needs: [invoke-build-java]
    uses: ./.github/workflows/test-java.yml
  • Certains jobs nécessitent secrets: inherit
    • Lorsqu’un workflow en appelle un autre, les secrets ne sont pas partagés par défaut
    • C’était la cause d’un problème où l’ensemble du pipeline CI échouait alors que l’exécution d’une step individuelle fonctionnait

Des fusions plus rapides, mais un débogage toujours coûteux

  • Les nouveaux scripts de CI ont fortement réduit le temps de fusion, et le résultat est satisfaisant
  • Il a toutefois fallu beaucoup trop de temps pour y parvenir, et le débogage doit devenir plus simple lorsque des problèmes surviennent

Aucun commentaire pour le moment.

Aucun commentaire pour le moment.