
Pour industrialiser un projet Pulumi local, commencez par trois controles
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 acces a l’hyperviseur.
Le flux de ce guide a ete 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 chaine 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 etapes sur un runner adapte ;
- un lint du YAML du workflow avant envoi.
Pourquoi preview doit rester dans la chaine de qualite
Section intitulée « Pourquoi preview doit rester dans la chaine de qualite »Dans un projet Pulumi, le preview n’est pas un simple confort visuel. Il
sert a verifier l’intention du changement avant l’application reelle.
Sur le lab restructure de cette section, il doit encore annoncer :
- le composant
lab:kvm:LibvirtVmStack; - le provider libvirt ;
- le reseau
pulumi-config-net; - le disque clone ;
- le disque cloud-init avec
userData: [secret]; - la VM
pulumi-config-vm.
Si cette lecture disparait du workflow de qualite, vous perdez tres 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-meme. Il porte sur
le contrat de configuration de la stack. Creez un fichier tests/test_config.py
avec un faux objet de configuration, puis verifiez les valeurs par defaut 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 controles locaux
Section intitulée « Etape 2 - Rejouer les controles locaux »Avant d’ecrire la CI, rejouez la chaine 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 creer 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 reseau ;name: "pulumi-config-vm"pour la VM ;memory: 3072etvcpu: 2;userData: [secret]pour le disque cloud-init.
Cette lecture donne deja une tres bonne revue automatique du changement, sans
executer pulumi up.
Etape 4 - Ecrire un workflow GitHub Actions self-hosted
Section intitulée « Etape 4 - Ecrire un workflow GitHub Actions self-hosted »Le workflow suivant est volontairement sobre. Il rejoue la meme chaine que vos
verifications 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@v4
- name: Setup Python uses: actions/setup-python@v5 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 meritent 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 etre invalide alors meme que vos commandes Pulumi sont bonnes. Linter le YAML localement evite 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 chaine de qualite
Section intitulée « Ce qu’apporte cette chaine de qualite »Chaque couche repond a une question differente :
compileallverifie que le code Python reste syntaxiquement acceptable ;unittestverifie un petit comportement local et portable ;pulumi previewrelit l’intention IaC avant application ;actionlintetyamllintverifient que la CI elle-meme n’est pas casse.
Vous obtenez ainsi une chaine progressive, legere et bien adaptee a un projet Pulumi encore local.
Pieges frequents
Section intitulée « Pieges frequents »| Symptome | 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 acces 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 meme l’execution 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
previewreel doit tourner sur un runner self-hosted qui voit l’hyperviseur. - Le
previewreste une lecture de qualite essentielle avantup. - Linter le workflow lui-meme evite de casser la chaine de qualite pour une simple erreur YAML.