Introduction
Ce guide est une présentation pratique, de bout en bout, pour les équipes qui souhaitent automatiser la construction, les tests et le déploiement PowerShell avec CI/CD. Vous allez :
- Faire l'inventaire des versions et des dépendances pour que votre pipeline soit reproductible.
- Implémenter un essai pilote sûr qui modifie un fichier de configuration avec la sémantique WhatIf et Confirm.
- Valider avec PSScriptAnalyzer et Pester avant d'empaqueter un artefact versionné.
- Déployer avec des vérifications préalables, un essai à blanc et une vérification post-déploiement.
- Gérer les échecs courants et effectuer un retour arrière propre.
Les exemples sont volontairement minimaux et ne nécessitent pas de conteneurs. Vous pouvez adapter les scripts à votre service CI/CD préféré ; l'accent reste mis sur les chemins de code PowerShell sur lesquels les opérateurs s'appuient.
Inventaire des versions et de l'environnement
Avant d'automatiser, capturez les versions et les paramètres exacts que vos scripts et exécuteurs utiliseront.
Vérifications rapides
# Version et plateforme PowerShell
$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 fournisseur NuGet (nécessaire pour certaines opérations de modules)
Get-PackageProvider -Name NuGet -ForceBootstrap | Format-List Name, Version
# Confirmer l'accès en écriture à la racine cible prévue (exemple)
$targetRoot = 'C:\Modules\MyCompany.Tools'
Test-Path $targetRoot -PathType Container
Utilisez un court tableau pour aligner les attentes entre les machines et les exécuteurs.
| Vérification | 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 |
| Fournisseur NuGet | Get-PackageProvider NuGet | Liste une version |
| Racine cible existe | Test-Path C:\Modules\MyCompany.Tools | True ou créer |
Si les valeurs diffèrent selon l'environnement, enregistrez-les afin de pouvoir épingler les versions compatibles et éviter la dérive.
Chemin de configuration sécurisé
Définir un essai pilote étroit et inspectable
Commencez par un essai pilote rapide à comprendre et simple à observer : une fonction qui écrit un fichier de configuration, crée une sauvegarde et prend en charge WhatIf/Confirm. Cela enseigne les motifs de sécurité dont vos scripts plus importants auront besoin.
La fonction (avec comportement WhatIf et favorable au retour arrière)
# 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, 'Créer le répertoire')) {
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, "Sauvegarder vers $backupPath" )) {
Copy-Item -LiteralPath $resolvedPath -Destination $backupPath -Force
}
}
if ($PSCmdlet.ShouldProcess($resolvedPath, 'Écrire le contenu')) {
Set-Content -LiteralPath $resolvedPath -Value $Content -Encoding UTF8 -NoNewline
}
[pscustomobject]@{
Success = $true
Path = $resolvedPath
BackupPath = $backupPath
}
}
Points clés :
[CmdletBinding(SupportsShouldProcess = $true)]active-WhatIfet-Confirmpour des exécutions sûres.- Les sauvegardes sont versionnées avec un horodatage et n'écrasent pas 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 aucun n\'existe' {
$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 quand demandée' {
$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 essais à blanc' {
$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
Résolvez les erreurs avant de continuer. Les avertissements constituent un backlog utile mais doivent être triés.
Empaqueter un artefact versionné
Gardez l'empaquetage simple et déterministe : copiez le module/fichiers dans un dossier versionné, ajoutez un VERSION.txt et compressez-le.
# Fichier : build.ps1 (exécution locale ou dans 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 "Artefact empaqueté : $zip"
Exemple CI minimal (exécuteur Windows)
Ceci est un exemple que vous pouvez adapter. L'accent est mis sur 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: Empaqueter l'artefact
shell: pwsh
run: .\build.ps1
- name: Téléverser l'artefact
uses: actions/upload-artifact@v4
with:
name: MyCompany.Tools
path: out/*.zip
Déploiement avec vérifications préalables, essai à blanc et retour arrière
Script de déploiement
Ce script déploie un artefact versionné dans une racine cible avec un motif de dossiers Current et Previous. Le retour arrière 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 "Vérification de l'artefact à $Zip"
if (-not (Test-Path -LiteralPath $Zip -PathType Leaf)) {
throw "Artefact introuvable : $Zip"
}
Write-Verbose "Vérification de la racine cible à $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 "Espace disque insuffisant à $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 "Décompression de $Zip vers $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
# Prise en charge de l'essai à blanc via -WhatIf
if ($PSCmdlet.ShouldProcess($TargetRoot, 'Déployer l\'artefact')) {
$staging = Expand-Artifact -Zip $ArtifactZip -Root $TargetRoot
Switch-Version -Root $TargetRoot -Staging $staging
}
Write-Host 'Déploiement terminé. Contenu Current :'
Get-ChildItem -Path (Join-Path $TargetRoot 'Current') -Recurse | Select-Object -First 5 | Format-List
Exemples d'utilisation :
# Essai à blanc
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 devriez voir True et le contenu du fichier devrait être exactement ok.
Vérification et diagnostics
Rendez le succès observable et l'échec diagnostiquable.
- Analyse statique :
Invoke-ScriptAnalyzerdoit se terminer sans constatations de sévérité Error. - Tests :
Invoke-Pesterdoit signaler 0 Failed et un nombre de tests non nul. - Empaquetage : Le fichier zip de l'artefact doit contenir un dossier
MyCompany.ToolsavecVERSION.txt. - Pré-déploiement : L'essai à blanc (
-WhatIf) doit afficher les actions planifiées sans modifier la racine cible. - Post-déploiement : Importer et exécuter la fonction ; les changements de fichiers attendus doivent exister sur le disque.
Commandes et vérifications d'exemple :
# Vérifier l'analyse et les tests dans CI ou localement
Invoke-ScriptAnalyzer -Path . -Recurse -Severity Error
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
# Confirmer que l'essai à blanc 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
# Exécution du 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
Pour les diagnostics, activez la sortie verbose et, au besoin, une transcription.
$VerbosePreference = 'Continue'
Start-Transcript -Path "$env:TEMP\deploy-$(Get-Date -Format yyyyMMddHHmmss).log"
try {
# exécuter le déploiement
}
finally {
Stop-Transcript
}
Modes d'échec et récupération
Anticipez ces problèmes courants et gardez les correctifs à portée de main.
| Symptôme | Cause probable | Correctif |
|---|---|---|
| Script bloqué au démarrage | ExecutionPolicy restreinte | Exécuter powershell.exe -ExecutionPolicy Bypass -File deploy.ps1 pour les chemins de code approuvés ou définir une stratégie appropriée via la gestion des stratégies. |
| Fournisseur 'NuGet' introuvable | Fournisseur NuGet non amorcé | Exécuter Get-PackageProvider -Name NuGet -ForceBootstrap. |
| WhatIf affiche des actions mais rien ne se passe en exécution réelle | SupportsShouldProcess manquant ou ShouldProcess non appelé | S'assurer que [CmdletBinding(SupportsShouldProcess=$true)] et envelopper les actions avec $PSCmdlet.ShouldProcess. |
| Erreurs de chemin sur Linux/macOS | Chemins Windows codés en dur | Utiliser Join-Path, éviter les barres obliques inverses et paramétrer les racines. |
| Fichier verrouillé pendant le déploiement | Un autre processus retient le fichier | Utiliser la permutation des dossiers Current/Previous pour éviter l'écrasement sur place ; réessayer après libération du verrou. |
| Pester n'arrive pas à importer la fonction | Chemin d'importation incorrect | Faire un dot-source du chemin correct ou convertir en manifeste de module et Import-Module. |
Procédure de retour arrière
Le script de déploiement préserve 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 'Aucune version Previous vers laquelle revenir.' }
# Essai à blanc d'abord
$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')
Si vous préférez ne pas renommer le dossier cassé, supprimez-le plus tard après avoir confirmé que le retour arrière a fonctionné.
Récupération après un déploiement partiel
- Arrêtez les changements supplémentaires. Ne relancez pas le déploiement avant d'avoir confirmé l'état.
- Utilisez le dossier Previous comme source faisant autorité pour la restauration.
- Réexécutez les tests de fumée post-déploiement pour confirmer la santé.
- Capturez les journaux et les différences pour l'analyse des causes racines avant la prochaine tentative.
Liste de contrôle des opérations
Utilisez ce guide d'exploitation pour maintenir la cohérence des opérations. Remplacez les chemins d'exemple par les vôtres.
- Inventaire
- Exécuter
$PSVersionTableet enregistrer la version. - Installer ou mettre à jour Pester et PSScriptAnalyzer.
- Confirmer l'existence du fournisseur NuGet.
- Vérifier le chemin de la racine cible et l'espace disque libre.
- Valider
- Exécuter
Invoke-ScriptAnalyzer -Path . -Recurse -Severity Warning, Error. - Exécuter
Invoke-Pester -CIet confirmer 0 échec. - Empaqueter
- Exécuter
build.ps1pour générerout/MyCompany.Tools-<version>.zip. - Vérifier que
VERSION.txtexiste dans le paquet. - Pré-déploiement
- Exécuter
deploy.ps1 -WhatIfet confirmer que seules les actions planifiées sont listées. - S'assurer qu'aucun processus ne verrouille les fichiers sous la racine cible.
- Déploiement
- Exécuter
deploy.ps1 -Confirm:$false. - Observer les dossiers Current et Previous sous la racine cible.
- Vérifier
- Faire un dot-source de
Set-ConfigFile.ps1depuis Current et exécuter un test de fumée. - Vérifier le contenu du fichier sur le disque comme prévu.
- Retour arrière (si nécessaire)
- Renommer Current en dossier horodaté et Previous à nouveau en Current.
- Réexécuter le test de fumée pour confirmer la récupération.
- Documenter
- Enregistrer la version, les horodatages de début/fin, réussite/échec, et toute déviation.
Conclusion
Un essai pilote étroit et inspectable est le moyen le plus rapide de gagner en confiance dans votre chemin CI/CD PowerShell. En séparant la validation de l'empaquetage, en utilisant WhatIf et Confirm pour étager le risque, et en déployant avec un motif Current/Previous réversible, vous obtenez :
- Des mises en production plus sûres que vous pouvez tester à blanc et vérifier.
- Des étapes claires et répétables qui réduisent la reprise et les surprises.
- Des artefacts déterministes que vous pouvez suivre et auditer.
Prochaines étapes :
- Convertir vos fonctions en un module approprié avec un manifeste de module et des versions sémantiques.
- Ajouter la signature de code pour augmenter la confiance et réduire les frictions liées à la stratégie d'exécution.
- Étendre les tests pour inclure des tests de fumée spécifiques à l'environnement (configuration d'application IIS, définitions de tâches planifiées, ou redémarrages de service, selon le cas) en utilisant le même motif WhatIf-first.
- Standardiser la disposition de vos artefacts entre les équipes pour que les opérateurs puissent déployer avec les mêmes commandes partout.
Avec ces motifs en place, vous pouvez passer d'une seule fonction à un portefeuille d'automatisations PowerShell fiables sans sacrifier la sécurité ni la vitesse.