
Pour industrialiser un projet Pulumi local, commencez par trois contrôles
simples : compiler le code Python, lancer un petit test unitaire, puis
rejouer pulumi preview --diff --non-interactive. Sur un projet
KVM/libvirt, la CI qui execute vraiment le preview ne peut pas tourner sur
un runner GitHub heberge, car il ne voit pas qemu:///system. Elle doit
tourner sur un runner self-hosted Linux qui a accès a l'hyperviseur.
Le flux de ce guide a été rejoue le 1 avril 2026 sur la stack dev du lab
local avec python -m compileall ., python -m unittest discover,
pulumi preview --stack dev --diff --non-interactive, puis validation du
workflow GitHub Actions avec actionlint et yamllint.
Ce que vous allez ajouter
Section intitulée « Ce que vous allez ajouter »L'objectif n'est pas encore de construire un pipeline gigantesque. Vous allez mettre en place une chaîne courte, mais utile :
- une verification de syntaxe Python ;
- un test unitaire minimal sur le chargement de la configuration ;
- un
previewPulumi non interactif ; - un workflow GitHub Actions qui rejoue ces étapes sur un runner adapte ;
- un lint du YAML du workflow avant envoi.
Pourquoi preview doit rester dans la chaîne de qualité
Section intitulée « Pourquoi preview doit rester dans la chaîne de qualité »Dans un projet Pulumi, le preview n'est pas un simple confort visuel. Il
sert a vérifier l'intention du changement avant l'application réelle.
Sur le lab restructure de cette section, il doit encore annoncer :
- le composant
lab:kvm:LibvirtVmStack; - le provider libvirt ;
- le réseau
pulumi-config-net; - le disque clone ;
- le disque cloud-init avec
userData: [secret]; - la VM
pulumi-config-vm.
Si cette lecture disparaît du workflow de qualité, vous perdez très vite la
vision du changement avant up.
Etape 1 - Ajouter un test unitaire minimal
Section intitulée « Etape 1 - Ajouter un test unitaire minimal »Le premier test utile ici ne porte pas sur libvirt lui-même. Il porte sur
le contrat de configuration de la stack. Créez un fichier tests/test_config.py
avec un faux objet de configuration, puis vérifiez les valeurs par défaut et les
valeurs surchargees.
from __future__ import annotations
import unittestfrom unittest.mock import patch
import pulumi
from pulumi_kvm_local.config import load_stack_settings
class FakeConfig: def __init__(self, values: dict[str, object] | None = None): self._values = values or {}
def get(self, key: str): value = self._values.get(key) if isinstance(value, int): return str(value) return value
def get_int(self, key: str): value = self._values.get(key) if value is None: return None return int(value)
def require_secret(self, key: str): value = self._values[key] return pulumi.Output.secret(str(value))
class LoadStackSettingsTests(unittest.TestCase): def test_load_stack_settings_uses_defaults(self): with patch("pulumi_kvm_local.config.pulumi.Config", return_value=FakeConfig({"adminPassword": "secret"})): settings = load_stack_settings()
self.assertEqual(settings.network_name, "pulumi-lab-net") self.assertEqual(settings.vm_name, "pulumi-lab-vm") self.assertEqual(settings.vm_memory_mib, 2048) self.assertEqual(settings.vm_vcpu, 2) self.assertEqual(settings.admin_user, "devops") self.assertIsInstance(settings.admin_password, pulumi.Output)
def test_load_stack_settings_reads_stack_values(self): fake_config = FakeConfig( { "networkName": "pulumi-config-net", "vmName": "pulumi-config-vm", "vmMemoryMiB": 3072, "vmVcpu": 2, "adminUser": "devops", "adminPassword": "Pulumi-Devops-2026!", } )
with patch("pulumi_kvm_local.config.pulumi.Config", return_value=fake_config): settings = load_stack_settings()
self.assertEqual(settings.network_name, "pulumi-config-net") self.assertEqual(settings.vm_name, "pulumi-config-vm") self.assertEqual(settings.vm_memory_mib, 3072) self.assertEqual(settings.vm_vcpu, 2) self.assertEqual(settings.admin_user, "devops") self.assertIsInstance(settings.admin_password, pulumi.Output)
if __name__ == "__main__": unittest.main()Ce test reste volontairement simple : il verifie que votre projet lit bien la configuration attendue sans avoir besoin de lancer une vraie ressource.
Etape 2 - Rejouer les contrôles locaux
Section intitulée « Etape 2 - Rejouer les contrôles locaux »Avant d'écrire la CI, rejouez la chaîne localement sur le projet :
source venv/bin/activatepython -m compileall .python -m unittest discover -s tests -p 'test_*.py'pulumi stack select devpulumi preview --stack dev --diff --non-interactiveDans le lab valide, unittest retourne :
Ran 2 tests;OK.
Le preview non interactif annonce encore 7 ressources a créer et masque
correctement userData comme secret. C'est exactement le signal que vous
voulez conserver avant une fusion.
Etape 3 - Lire un preview non interactif
Section intitulée « Etape 3 - Lire un preview non interactif »Le point important n'est pas seulement que la commande passe. Il faut savoir la lire vite.
Dans le lab rejoue, le preview montre notamment :
lab:kvm:LibvirtVmStack;pulumi:providers:libvirt;name: "pulumi-config-net"pour le réseau ;name: "pulumi-config-vm"pour la VM ;memory: 3072etvcpu: 2;userData: [secret]pour le disque cloud-init.
Cette lecture donne déjà une très bonne revue automatique du changement, sans
exécuter pulumi up.
Etape 4 - Écrire un workflow GitHub Actions self-hosted
Section intitulée « Etape 4 - Écrire un workflow GitHub Actions self-hosted »Le workflow suivant est volontairement sobre. Il rejoue la même chaîne que vos
vérifications locales et garde le preview sur un runner self-hosted Linux.
---name: pulumi-preview
"on": pull_request: paths: - "**/*.py" - "Pulumi.yaml" - "Pulumi.dev.yaml" - "requirements.txt" - ".github/workflows/pulumi-preview.yml" workflow_dispatch:
permissions: contents: read
jobs: validate-preview: runs-on: - self-hosted - linux steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12"
- name: Install dependencies run: | python -m venv venv . venv/bin/activate pip install -r requirements.txt
- name: Compile Python sources run: | . venv/bin/activate python -m compileall .
- name: Run unit tests run: | . venv/bin/activate python -m unittest discover -s tests -p 'test_*.py'
- name: Pulumi preview env: PULUMI_CONFIG_PASSPHRASE: "" run: | . venv/bin/activate pulumi login --local pulumi stack select dev pulumi preview --stack dev --diff --non-interactiveTrois points méritent votre attention :
runs-on: [self-hosted, linux]parce que le projet vise un hyperviseur local ;PULUMI_CONFIG_PASSPHRASE: ""pour un backend local sans passphrase ;- l'absence de
updans la pull request, car le but ici est de relire le changement, pas d'appliquer la stack.
Etape 5 - Linter le workflow avant de pousser
Section intitulée « Etape 5 - Linter le workflow avant de pousser »Un fichier GitHub Actions peut être invalide alors même que vos commandes Pulumi sont bonnes. Linter le YAML localement évite ce faux signal.
actionlint .github/workflows/pulumi-preview.ymlyamllint .github/workflows/pulumi-preview.ymlDans le lab rejoue, ces deux commandes passent sans erreur.
Ce qu'apporte cette chaîne de qualité
Section intitulée « Ce qu'apporte cette chaîne de qualité »Chaque couche répond a une question différente :
compileallverifie que le code Python reste syntaxiquement acceptable ;unittestverifie un petit comportement local et portable ;pulumi previewrelit l'intention IaC avant application ;actionlintetyamllintvérifient que la CI elle-même n'est pas casse.
Vous obtenez ainsi une chaîne progressive, légère et bien adaptée a un projet Pulumi encore local.
Pieges fréquents
Section intitulée « Pieges fréquents »| Symptôme | Cause probable | Solution |
|---|---|---|
Le workflow tourne sur ubuntu-latest mais le preview echoue | Le runner ne voit pas qemu:///system | Gardez la partie preview sur un runner self-hosted Linux avec accès libvirt |
pulumi preview demande une interaction | La commande reste dans son mode interactif | Ajoutez --non-interactive --diff |
| Les tests essaient de lire une vraie stack | Le test importe pulumi.Config() sans isolation | Mockez pulumi.Config() comme dans l'exemple |
| Le YAML est refuse avant même l'exécution des jobs | Le workflow est mal forme | Rejouez actionlint et yamllint avant le push |
A retenir
Section intitulée « A retenir »- Une bonne CI Pulumi commence souvent par
compileall, un test unitaire et unpreviewnon interactif. - Sur libvirt, le
previewréel doit tourner sur un runner self-hosted qui voit l'hyperviseur. - Le
previewreste une lecture de qualité essentielle avantup. - Linter le workflow lui-même évite de casser la chaîne de qualité pour une simple erreur YAML.