E-NO
DevOps 11 min de lecture

Automatisation CI/CD PowerShell avec exemples pratiques : guide d'implémentation

calendar_today Publié : 2026-07-28
update Dernière mise à jour : 2026-07-28
analytics Efficacité SEO : 97%
Illustration du guide technique pour « Automatisation CI/CD PowerShell avec exemples pratiques : guide d'implémentation ».

Introduction

Ce guide propose un parcours pratique, de bout en bout, pour les équipes qui veulent automatiser la construction, les tests et le déploiement PowerShell via CI/CD. À l'issue, vous saurez :

  • Recenser les versions et dépendances pour rendre la pipeline reproductible.
  • Mettre en place un pilote sûr qui modifie un fichier de configuration en respectant les sémantiques WhatIf et Confirm.
  • Valider votre code avec PSScriptAnalyzer et Pester avant d'emballer un artefact versionné.
  • Déployer avec pré‑contrôles, dry run et vérification post‑déploiement.
  • Gérer les pannes courantes et effectuer un rollback propre.

Les exemples sont volontairement minimaux et ne nécessitent pas de conteneurs. Adaptez les scripts à votre service CI/CD préféré (GitHub Actions, GitLab CI/CD, Azure Pipelines, etc.) : l'accent est mis sur les chemins PowerShell opérationnels.

Inventaire des versions et de l'environnement

Avant d'automatiser, capturez les versions et paramètres exacts utilisés par vos scripts et vos runners.

Contrôles rapides

# Version de PowerShell et plateforme
$PSVersionTable

# S'assurer que PSGallery est approuvé et que les outils sont disponibles
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
Install-Module Pester -MinimumVersion 5.5.0 -Scope CurrentUser -Force
Install-Module PSScriptAnalyzer -Scope CurrentUser -Force

# Vérifier les outils
Get-Module Pester -ListAvailable | Select-Object Name, Version | Sort-Object Version -Descending | Select-Object -First 1
Get-Module PSScriptAnalyzer -ListAvailable | Select-Object Name, Version | Sort-Object Version -Descending | Select-Object -First 1

# Disponibilité du provider NuGet (requis pour certaines opérations modules)
Get-PackageProvider -Name NuGet -ForceBootstrap | Format-List Name, Version

# Confirmer l'accès en écriture à la racine cible (exemple)
$targetRoot = 'C:\Modules\MyCompany.Tools'
Test-Path $targetRoot -PathType Container -IsValid  # Doit renvoyer True ou créer le dossier

Utilisez un court tableau pour aligner les attentes entre postes et runners.

ContrôleCommandeRésultat attendu (exemple)
Version PowerShell$PSVersionTable.PSVersion7.4.x ou 5.1.x
Pester installéGet-Module Pester -ListAvailableAffiche 5.5.0+
ScriptAnalyzerGet-Module PSScriptAnalyzerToute version stable
Provider NuGetGet-PackageProvider NuGetListe une version
Racine cible existeTest-Path C:\Modules\MyCompany.ToolsTrue ou la créer

Si les valeurs diffèrent selon l'environnement, enregistrez‑les afin d'épingler des versions compatibles et éviter la dérive.

Chemin de configuration sécurisé

Cadrer un pilote étroit et inspectable

Commencez par un pilote facile à raisonner et à observer : une fonction qui écrit un fichier de configuration, crée une sauvegarde et supporte WhatIf/Confirm. Vous y apprendrez les patrons de sécurité à réutiliser ensuite.

La fonction (avec WhatIf et comportement favorable au rollback)

# Fichier : Module/Public/Set-ConfigFile.ps1
function Set-ConfigFile {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param(
        [Parameter(Mandatory = $true)]
        [string] $Path,
        [Parameter(Mandatory = $true)]
        [string] $Content,
        [switch] $CreateBackup
    )

    $resolvedPath = (Resolve-Path -LiteralPath $Path -ErrorAction SilentlyContinue) ?? $Path
    $dir = Split-Path -Parent $resolvedPath
    if (-not (Test-Path -LiteralPath $dir)) {
        if ($PSCmdlet.ShouldProcess($dir, 'Create directory')) {
            New-Item -ItemType Directory -Path $dir -Force | Out-Null
        }
    }

    $backupPath = if ($CreateBackup) { "$resolvedPath.bak-$(Get-Date -Format yyyyMMddHHmmss)" } else { $null }

    if (Test-Path -LiteralPath $resolvedPath -PathType Leaf -ErrorAction SilentlyContinue -and $CreateBackup) {
        if ($PSCmdlet.ShouldProcess($resolvedPath, "Backup to $backupPath")) {
            Copy-Item -LiteralPath $resolvedPath -Destination $backupPath -Force
        }
    }

    if ($PSCmdlet.ShouldProcess($resolvedPath, 'Write content')) {
        Set-Content -LiteralPath $resolvedPath -Value $Content -Encoding UTF8 -NoNewline
    }

    [pscustomobject]@{
        Success    = $true
        Path       = $resolvedPath
        BackupPath = $backupPath
    }
}

Points clés :

  • [CmdletBinding(SupportsShouldProcess = $true)] active -WhatIf et -Confirm pour des exécutions sûres.
  • Les sauvegardes sont horodatées pour ne pas écraser l'état précédent.
  • La fonction retourne un objet simple que les opérateurs peuvent journaliser et inspecter.

Tests unitaires avec Pester

# Fichier : Module/Tests/Set-ConfigFile.Tests.ps1
$here = Split-Path -Parent $PSCommandPath
$root = Split-Path -Parent $here
Import-Module "$root\Public\Set-ConfigFile.ps1" -Force

Describe 'Set-ConfigFile' {
    It 'Crée un fichier quand il n\'existe pas' {
        $tmp = Join-Path $env: TEMP 'cfg-test-1.txt'
        if (Test-Path $tmp) { Remove-Item $tmp -Force }
        $result = Set-ConfigFile -Path $tmp -Content 'abc' -WhatIf:$false
        Test-Path $tmp | Should -BeTrue
        (Get-Content -Raw $tmp) | Should -Be 'abc'
        $result.Success | Should -BeTrue
    }

    It 'Crée une sauvegarde sur demande' {
        $tmp = Join-Path $env: TEMP 'cfg-test-2.txt'
        Set-Content -Path $tmp -Value 'v1'
        $result = Set-ConfigFile -Path $tmp -Content 'v2' -CreateBackup -WhatIf:$false
        Test-Path $result.BackupPath | Should -BeTrue
        (Get-Content -Raw $tmp) | Should -Be 'v2'
    }

    It 'Respecte WhatIf pour les dry runs' {
        $tmp = Join-Path $env: TEMP 'cfg-test-3.txt'
        if (Test-Path $tmp) { Remove-Item $tmp -Force }
        $ = Set-ConfigFile -Path $tmp -Content 'dry' -WhatIf
        Test-Path $tmp | Should -BeFalse
    }
}

Analyse statique

Invoke-ScriptAnalyzer -Path .\Module -Recurse -Severity Warning, Error

Corrigez les erreurs avant d'avancer. Les avertissements servent de backlog à trier.

Packager un artefact versionné

Gardez l'emballage simple et déterministe : copiez le module/dossier dans un répertoire versionné, ajoutez un VERSION.txt, puis zippez.

# Fichier : build.ps1 (exécuter en local ou en CI)
$ErrorActionPreference = 'Stop'
$version = Get-Date -Format 'yyyy.MM.dd.HHmm'
$out = Join-Path $PSScriptRoot 'out'
New-Item -ItemType Directory -Path $out -Force | Out-Null
$dst = Join-Path $out 'MyCompany.Tools'
Copy-Item -Path .\Module\* -Destination $dst -Recurse -Force
Set-Content -Path (Join-Path $dst 'VERSION.txt') -Value $version -NoNewline
$zip = Join-Path $out ("MyCompany.Tools-$version.zip")
if (Test-Path $zip) { Remove-Item $zip -Force }
Compress-Archive -Path $dst -DestinationPath $zip -Force
Write-Host "Packaged artifact: $zip"

Exemple CI minimal (runner Windows)

Vous pouvez l'adapter. Le cœur réside dans les étapes PowerShell.

# .github/workflows/ci-powershell.yml
name: ci-powershell
on:
  push:
    branches: [ main ]
jobs:
  test:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4
      - name: Installer les outils
        shell: pwsh
        run: |
          Set-PSRepository PSGallery -InstallationPolicy Trusted
          Install-Module Pester -MinimumVersion 5.5.0 -Scope CurrentUser -Force
          Install-Module PSScriptAnalyzer -Scope CurrentUser -Force
      - name: Analyse statique
        shell: pwsh
        run: Invoke-ScriptAnalyzer -Path . -Recurse -Severity Warning, Error
      - name: Tests unitaires
        shell: pwsh
        run: Invoke-Pester -CI -Output Detailed
      - name: Packager l'artefact
        shell: pwsh
        run: .\build.ps1
      - name: Upload de l'artefact
        uses: actions/upload-artifact@v4
        with:
          name: MyCompany.Tools
          path: out/*.zip

Déploiement avec pré‑contrôles, dry run et rollback

Script de déploiement

Ce script déploie un artefact versionné dans une racine cible avec les dossiers Current et Previous. Le rollback est un renommage + restauration.

# Fichier : deploy.ps1
[CmdletBinding(SupportsShouldProcess = $true)]
param(
    [Parameter(Mandatory = $true)]
    [string] $ArtifactZip,
    [Parameter(Mandatory = $true)]
    [string] $TargetRoot
)

$ErrorActionPreference = 'Stop'
$VerbosePreference = 'Continue'

function Invoke-PreChecks {
    param([string]$Zip, [string]$Root)
    Write-Verbose "Checking artifact at $Zip"
    if (-not (Test-Path -LiteralPath $Zip -PathType Leaf)) {
        throw "Artifact not found: $Zip"
    }
    Write-Verbose "Checking target root at $Root"
    if (-not (Test-Path -LiteralPath $Root)) {
        New-Item -ItemType Directory -Path $Root -Force | Out-Null
    }
    $space = ([System.IO.DriveInfo] (Split-Path -Qualifier $Root)).AvailableFreeSpace
    if ($space -lt 200MB) { throw "Insufficient disk space at $Root" }
}

function Expand-Artifact {
    param([string]$Zip, [string]$Root)
    $stamp = Get-Date -Format 'yyyyMMddHHmmss'
    $staging = Join-Path $Root "Staging-$stamp"
    New-Item -ItemType Directory -Path $staging -Force | Out-Null
    Write-Verbose "Expanding $Zip to $staging"
    Expand-Archive -Path $Zip -DestinationPath $staging -Force
    return $staging
}

function Switch-Version {
    param([string]$Root, [string]$Staging)
    $current = Join-Path $Root 'Current'
    $previous = Join-Path $Root 'Previous'

    if (Test-Path $previous) { Remove-Item $previous -Recurse -Force }
    if (Test-Path $current) {
        Rename-Item -Path $current -NewName 'Previous' -Force
    }
    Rename-Item -Path $Staging -NewName 'Current' -Force
}

Invoke-PreChecks -Zip $ArtifactZip -Root $TargetRoot

# Support du dry run via -WhatIf
if ($PSCmdlet.ShouldProcess($TargetRoot, 'Deploy artifact')) {
    $staging = Expand-Artifact -Zip $ArtifactZip -Root $TargetRoot
    Switch-Version -Root $TargetRoot -Staging $staging
}

Write-Host 'Deployment complete. Current content:'
Get-ChildItem -Path (Join-Path $TargetRoot 'Current') -Recurse | Select-Object -First 5 | Format-List

Exemples d'usage :

# Dry run
pwsh -File .\deploy.ps1 -ArtifactZip .\out\MyCompany.Tools-2024.05.01.1200.zip -TargetRoot C:\Modules\MyCompany.Tools -WhatIf

# Déploiement réel
pwsh -File .\deploy.ps1 -ArtifactZip .\out\MyCompany.Tools-2024.05.01.1200.zip -TargetRoot C:\Modules\MyCompany.Tools -Confirm:$false

Import post‑déploiement et test de fumée

$moduleRoot = 'C:\Modules\MyCompany.Tools\Current'
$public = Join-Path $moduleRoot 'Public'
$functionPath = Join-Path $public 'Set-ConfigFile.ps1'
. $functionPath

$tmp = Join-Path $env: TEMP 'post-deploy-check.txt'
$result = Set-ConfigFile -Path $tmp -Content 'ok' -CreateBackup -WhatIf:$false
$result.Success -and (Get-Content -Raw $tmp) -eq 'ok'

Vous devez voir True, et le contenu du fichier doit être exactement ok.

Vérification et diagnostics

Rendez le succès observable et l'échec diagnosable.

  • Analyse statique : Invoke-ScriptAnalyzer doit se terminer sans erreurs de sévérité Error.
  • Tests : Invoke-Pester doit rapporter 0 échec et un nombre de tests non nul.
  • Packaging : le zip doit contenir un dossier MyCompany.Tools avec VERSION.txt.
  • Pré‑déploiement : le dry run (-WhatIf) doit afficher les actions prévues sans modifier la racine cible.
  • Post‑déploiement : importez et exécutez la fonction ; les modifications attendues doivent exister sur disque.

Exemples de commandes et contrôles :

# Vérifier analyse et tests en CI ou en local
Invoke-ScriptAnalyzer -Path . -Recurse -Severity Error; if ($LASTEXITCODE -or $?) { }
Invoke-Pester -CI -Output Detailed

# Valider la structure de l'artefact
Expand-Archive -Path .\out\MyCompany.Tools-*.zip -DestinationPath .\out\unpacked -Force
Test-Path .\out\unpacked\MyCompany.Tools\Public\Set-ConfigFile.ps1 | Should -BeTrue

# Confirmer que le dry run ne modifie pas le système de fichiers
$before = Get-ChildItem C:\Modules\MyCompany.Tools -Recurse -Force | Select-Object FullName, Length
pwsh -File .\deploy.ps1 -ArtifactZip .\out\MyCompany.Tools-*.zip -TargetRoot C:\Modules\MyCompany.Tools -WhatIf
$after = Get-ChildItem C:\Modules\MyCompany.Tools -Recurse -Force | Select-Object FullName, Length
Compare-Object $before $after | Should -BeNullOrEmpty

# Test de fumée post-déploiement
. C:\Modules\MyCompany.Tools\Current\Public\Set-ConfigFile.ps1
$res = Set-ConfigFile -Path "$env: TEMP\ops-check.txt" -Content 'ok' -CreateBackup -WhatIf:$false
$res.Success | Should -BeTrue

Pour le diagnostic, activez la sortie verbeuse et, si nécessaire, une transcription.

$VerbosePreference = 'Continue'
Start-Transcript -Path "$env: TEMP\deploy-$(Get-Date -Format yyyyMMddHHmmss).log"
try {
  # run deploy
}
finally {
  Stop-Transcript
}

Pannes courantes et reprise

Anticipez ces problèmes fréquents et gardez les correctifs à portée de main.

SymptômeCause probableCorrectif
Script bloqué au démarrageExecutionPolicy trop restrictiveExécutez powershell.exe -ExecutionPolicy Bypass -File deploy.ps1 pour des chemins de confiance ou définissez une stratégie adaptée.
Provider 'NuGet' introuvableProvider non bootstrappéExécutez Get-PackageProvider -Name NuGet -ForceBootstrap.
WhatIf affiche des actions mais rien ne se passeManque SupportsShouldProcess ou oubli de ShouldProcessAssurez [CmdletBinding(SupportsShouldProcess=$true)] et encapsulez les actions avec $PSCmdlet.ShouldProcess.
Erreurs de chemin sous Linux/macOSChemins Windows en durUtilisez Join-Path, évitez les antislashs, paramétrez les racines.
Fichier verrouillé pendant le déploiementAutre processus détenteurUtilisez le duo Current/Previous pour éviter l'écrasement in‑place ; réessayez après libération du verrou.
Pester n'importe pas la fonctionMauvais chemin d'importDot-source le bon chemin ou créez un manifeste de module puis Import-Module.

Procédure de rollback

Le script de déploiement conserve la version précédente sous Previous. Pour revenir en arrière :

$root = 'C:\Modules\MyCompany.Tools'
$current = Join-Path $root 'Current'
$previous = Join-Path $root 'Previous'

if (-not (Test-Path $previous)) { throw 'No Previous version to roll back to.' }

# Dry run d'abord
if ($PSCmdlet) { $PSCmdlet.WhatIfPreference = $true }
Rename-Item -Path $current -NewName "Broken-$(Get-Date -Format yyyyMMddHHmmss)" -Force
Rename-Item -Path $previous -NewName 'Current' -Force

# Vérifier
Test-Path (Join-Path $root 'Current') | Should -BeTrue

Si vous préférez ne pas renommer le dossier défectueux, supprimez‑le plus tard après confirmation du rollback.

Reprise après un déploiement partiel

  • Stoppez toute modification. Ne relancez pas le déploiement tant que l'état n'est pas clarifié.
  • Considérez le dossier Previous comme source d'autorité pour restaurer.
  • Rejouez les tests de fumée post‑déploiement pour confirmer la santé.
  • Capturez journaux et diffs pour l'analyse de cause racine avant une nouvelle tentative.

Checklist d'exploitation

Servez‑vous de ce runbook pour rester cohérents. Remplacez les chemins d'exemple par les vôtres.

  • Inventaire
  • Exécuter $PSVersionTable et consigner la version.
  • Installer ou mettre à jour Pester et PSScriptAnalyzer.
  • Confirmer l'existence du provider NuGet.
  • Vérifier la racine cible et l'espace disque libre.
  • Validation
  • Exécuter Invoke-ScriptAnalyzer -Path . -Recurse -Severity Warning, Error.
  • Exécuter Invoke-Pester -CI et confirmer 0 échec.
  • Packaging
  • Lancer build.ps1 pour générer out/MyCompany.Tools-<version>.zip.
  • Vérifier la présence de VERSION.txt dans le paquet.
  • Pré‑déploiement
  • Lancer deploy.ps1 -WhatIf et confirmer que seules les actions prévues s'affichent.
  • S'assurer qu'aucun processus ne verrouille les fichiers sous la racine cible.
  • Déploiement
  • Lancer deploy.ps1 -Confirm:$false.
  • Observer les dossiers Current et Previous sous la racine.
  • Vérification
  • Dot‑sourcer Set-ConfigFile.ps1 depuis Current et exécuter un test de fumée.
  • Vérifier le contenu des fichiers sur disque.
  • Rollback (si nécessaire)
  • Renommer Current avec un horodatage et Previous en Current.
  • Rejouer le test de fumée pour confirmer la reprise.
  • Documentation
  • Consigner version, horodatages début/fin, succès/échecs et écarts.

Conclusion

Un pilote étroit et inspectable est le moyen le plus rapide de gagner en confiance dans votre parcours PowerShell CI/CD. En séparant validation et packaging, en utilisant WhatIf/Confirm pour étager le risque, et en déployant avec un motif réversible Current/Previous, vous obtenez :

  • Des mises en production plus sûres, testables en dry run et vérifiables.
  • Des étapes claires et reproductibles qui réduisent les reprises et les surprises.
  • Des artefacts déterministes, traçables et auditables.

Prochaines étapes :

  • Convertir vos fonctions en module avec manifeste et versions sémantiques.
  • Ajouter la signature de code pour renforcer la confiance et réduire la friction liée à l'ExecutionPolicy.
  • Étendre les tests pour inclure des fumées spécifiques à l'environnement (ex. config IIS, tâches planifiées, redémarrage de services) en conservant le même réflexe WhatIf d'abord.
  • Standardiser la structure de vos artefacts entre équipes afin que les opérateurs déploient partout avec les mêmes commandes.

Avec ces patrons, vous pourrez passer d'une seule fonction à un portefeuille d'automatisations PowerShell fiables sans sacrifier la sécurité ni la vitesse.

Score de qualité de l’article

Utilité pour le lecteur 97%
  • check_circle Guide prêt à lire
  • check_circle Exemples pratiques inclus
  • check_circle URL d’article optimisée pour le SEO