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ôle | Commande | Résultat attendu (exemple) |
|---|---|---|
| Version PowerShell | $PSVersionTable.PSVersion | 7.4.x ou 5.1.x |
| Pester installé | Get-Module Pester -ListAvailable | Affiche 5.5.0+ |
| ScriptAnalyzer | Get-Module PSScriptAnalyzer | Toute version stable |
| Provider NuGet | Get-PackageProvider NuGet | Liste une version |
| Racine cible existe | Test-Path C:\Modules\MyCompany.Tools | True 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ôme | Cause probable | Correctif |
|---|---|---|
| Script bloqué au démarrage | ExecutionPolicy trop restrictive | Exécutez powershell.exe -ExecutionPolicy Bypass -File deploy.ps1 pour des chemins de confiance ou définissez une stratégie adaptée. |
| Provider 'NuGet' introuvable | Provider non bootstrappé | Exécutez Get-PackageProvider -Name NuGet -ForceBootstrap. |
| WhatIf affiche des actions mais rien ne se passe | Manque SupportsShouldProcess ou oubli de ShouldProcess | Assurez [CmdletBinding(SupportsShouldProcess=$true)] et encapsulez les actions avec $PSCmdlet.ShouldProcess. |
| Erreurs de chemin sous Linux/macOS | Chemins Windows en dur | Utilisez Join-Path, évitez les antislashs, paramétrez les racines. |
| Fichier verrouillé pendant le déploiement | Autre processus détenteur | Utilisez le duo Current/Previous pour éviter l'écrasement in‑place ; réessayez après libération du verrou. |
| Pester n'importe pas la fonction | Mauvais chemin d'import | Dot-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
$PSVersionTableet 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 -CIet confirmer 0 échec. - Packaging
- Lancer
build.ps1pour générerout/MyCompany.Tools-<version>.zip. - Vérifier la présence de
VERSION.txtdans le paquet. - Pré‑déploiement
- Lancer
deploy.ps1 -WhatIfet 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
CurrentetPrevioussous la racine. - Vérification
- Dot‑sourcer
Set-ConfigFile.ps1depuisCurrentet exécuter un test de fumée. - Vérifier le contenu des fichiers sur disque.
- Rollback (si nécessaire)
- Renommer
Currentavec un horodatage etPreviousenCurrent. - 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.