Automation-Pipeline-Examples/update-module-and-pipelines.ps1
|
#Requires -Version 5.1 # AZLOCAL-UPDATER-VERSION: 1.4.0 <# .SYNOPSIS Refresh the AzLocal.UpdateManagement CI/CD pipeline files in THIS repository to match the latest published module version, then commit and push. .DESCRIPTION This script was dropped into your repository root by Copy-AzLocalPipelineExample. Run it whenever a new AzLocal.UpdateManagement version is published to PowerShell Gallery to: 1. Install/upgrade the module to the latest published version (only when the gallery is newer than what is already installed), then import it. 2. Refresh the bundled pipeline YAMLs via Update-AzLocalPipelineExample - a MARKER-AWARE merge that preserves any operator customisations you placed inside the AZLOCAL-CUSTOMIZE marker regions (schedule CRONs, service-connection names, runner labels, ITSM secret bindings, etc.). 3. Stage ONLY the workflow folder, repo-root config\ and DevChannel\ folders, the managed README.md (when module-managed), and THIS script itself (managed files may have refreshed in place when the module shipped newer templates), then commit and push when (and only when) something actually changed. The target platform and workflow folder were baked in at drop time by Copy-AzLocalPipelineExample, so the script is turnkey for this repo. You can still override them via parameters. .PARAMETER RepoRoot Repository root. Defaults to the folder this script lives in (the repo root, where Copy-AzLocalPipelineExample dropped it). .PARAMETER Platform Pipeline platform this repo uses ('GitHub' or 'AzureDevOps'). Baked in at drop time. .PARAMETER WorkflowSubPath Workflow / pipeline folder, RELATIVE to the repo root (for example '.github/workflows' on GitHub, or 'pipelines' on Azure DevOps). Baked in at drop time. .PARAMETER Scope Install scope passed to Install-Module when an upgrade is required. 'CurrentUser' (default) needs no elevation; 'AllUsers' requires an elevated session. .PARAMETER RequiredVersion Exact module version to install, import, and use for the pipeline refresh. Unlike the default latest-listed lookup, this resolves unlisted PowerShell Gallery candidates and is intended for development-channel validation. .PARAMETER LatestListed Explicitly resolve, install, and import the latest listed PSGallery version, even when a newer unlisted version is installed side-by-side. Mutually exclusive with -RequiredVersion. .PARAMETER AllowRollbackMarkerRemoval Allow a rollback to remove AZLOCAL-CUSTOMIZE sections that do not exist in the older bundled template. Common marker bodies remain preserved. Without this switch the refresh stops before committing when such a loss is found. .PARAMETER KeepNewerVersions Keep installed module versions greater than the latest listed Gallery version. By default, -LatestListed prompts to uninstall each higher version so PowerShell auto-loading cannot reactivate a development-channel candidate in a fresh session. Keeping them is an advanced side-by-side or forensic option and leaves that auto-loading risk in place. .PARAMETER NoPush Refresh the YAMLs only - skip the git add / commit / push. Review the result yourself with 'git status' / 'git diff'. .EXAMPLE .\Update-Module-And-Pipelines.ps1 Upgrade the module if needed, refresh the pipelines, then commit and push any changes. .EXAMPLE .\Update-Module-And-Pipelines.ps1 -NoPush Refresh the pipelines but leave the commit/push to you. .EXAMPLE .\Update-Module-And-Pipelines.ps1 -RequiredVersion 0.9.29 -NoPush Install and import the exact candidate, including an unlisted Gallery version, then refresh pipelines without committing. .NOTES Platform : __PLATFORM__ Workflow folder : __WORKFLOW_SUBPATH__ Module : AzLocal.UpdateManagement Generated by : Copy-AzLocalPipelineExample Template version: 1.4.0 (managed file - Copy/Update-AzLocalPipelineExample auto-refresh this script in place when the module ships a newer template version. Tune behaviour via PARAMETERS, not by editing the body; edits to the body are replaced on refresh. Pass -SkipStarterUpdater to freeze it.) #> [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Latest')] param( [string]$RepoRoot = $PSScriptRoot, [ValidateSet('GitHub', 'AzureDevOps')] [string]$Platform = '__PLATFORM__', [string]$WorkflowSubPath = '__WORKFLOW_SUBPATH__', [ValidateSet('CurrentUser', 'AllUsers')] [string]$Scope = 'CurrentUser', [Parameter(Mandatory = $true, ParameterSetName = 'Exact')] [ValidatePattern('^\d+\.\d+\.\d+(?:\.\d+)?$')] [version]$RequiredVersion, [Parameter(ParameterSetName = 'Latest')] [switch]$LatestListed, [switch]$AllowRollbackMarkerRemoval, [switch]$KeepNewerVersions, [switch]$NoPush ) $ErrorActionPreference = 'Stop' $moduleName = 'AzLocal.UpdateManagement' # --------------------------------------------------------------------------- # 1. Ensure the latest published module version is installed and imported. # We deliberately do NOT Uninstall-Module first - that needs elevation for # AllUsers installs and would needlessly remove a pinned/side-by-side # version. Install-Module -Force lays the new version down alongside. # --------------------------------------------------------------------------- $installed = Get-Module -ListAvailable -Name $moduleName | Sort-Object Version -Descending | Select-Object -First 1 $targetVersion = $null try { if ($PSBoundParameters.ContainsKey('RequiredVersion')) { $targetVersion = (Find-Module -Name $moduleName -RequiredVersion $RequiredVersion -Repository PSGallery -ErrorAction Stop).Version } else { $targetVersion = (Find-Module -Name $moduleName -Repository PSGallery -ErrorAction Stop).Version } } catch { if ($PSBoundParameters.ContainsKey('RequiredVersion')) { throw "Could not resolve exact $moduleName version $RequiredVersion from PowerShell Gallery. $($_.Exception.Message)" } if ($LatestListed.IsPresent) { throw "Could not resolve the latest listed $moduleName version from PowerShell Gallery. Refusing to fall back to an installed version because it may be an unlisted development-channel candidate. $($_.Exception.Message)" } Write-Warning "Could not query PowerShell Gallery for '$moduleName' ($($_.Exception.Message)). Falling back to the installed version." if ($installed) { $targetVersion = $installed.Version } } if (-not $targetVersion) { throw "$moduleName is not installed and PowerShell Gallery could not be reached. Install it manually, then re-run." } $installedTarget = Get-Module -ListAvailable -Name $moduleName | Where-Object { $_.Version -eq [version]$targetVersion } | Select-Object -First 1 if (-not $installedTarget) { $fromText = if ($installed) { $installed.Version } else { '(not installed)' } Write-Host "Installing $moduleName $targetVersion (highest installed: $fromText)..." -ForegroundColor Cyan Install-Module -Name $moduleName -Scope $Scope -RequiredVersion $targetVersion -Force -AllowClobber } else { Write-Host "$moduleName $targetVersion is already installed." -ForegroundColor Green } Get-Module -Name $moduleName | Remove-Module -Force -ErrorAction SilentlyContinue Import-Module -Name $moduleName -RequiredVersion $targetVersion -Force $version = (Get-Module -Name $moduleName).Version.ToString() Write-Host "Using $moduleName $version." -ForegroundColor Green # --------------------------------------------------------------------------- # 2. Refresh the pipeline YAMLs (marker-aware merge; preserves customisations). # --------------------------------------------------------------------------- $workflowFull = Join-Path -Path $RepoRoot -ChildPath $WorkflowSubPath if (-not (Test-Path -LiteralPath $workflowFull)) { throw "Workflow folder not found: '$workflowFull'. Pass -RepoRoot / -WorkflowSubPath to override." } Write-Host "Refreshing $Platform pipelines under '$workflowFull'..." -ForegroundColor Cyan $preflightResults = @(Update-AzLocalPipelineExample -Destination $workflowFull -Platform $Platform -PassThru -WhatIf 6>$null) $preflightMarkerRemovals = @($preflightResults | Where-Object { @($_.RemovedMarkers).Count -gt 0 }) if ($LatestListed.IsPresent -and $preflightMarkerRemovals.Count -gt 0 -and -not $AllowRollbackMarkerRemoval.IsPresent) { $preflightDetails = ($preflightMarkerRemovals | ForEach-Object { "{0} [{1}]" -f (Split-Path -Leaf $_.File), ([string]::Join(',', @($_.RemovedMarkers))) }) -join '; ' throw "Pipeline rollback preflight stopped because the target templates would remove AZLOCAL-CUSTOMIZE sections: $preflightDetails. Review those bodies, then rerun with -AllowRollbackMarkerRemoval to approve only those removals. No pipeline files or installed newer module versions were changed." } if ($LatestListed.IsPresent) { $newerInstalledVersions = @(Get-Module -ListAvailable -Name $moduleName | Where-Object { $_.Version -gt [version]$targetVersion } | Select-Object -ExpandProperty Version -Unique | Sort-Object -Descending) if ($newerInstalledVersions.Count -gt 0) { if ($KeepNewerVersions.IsPresent) { Write-Warning ("Keeping module version(s) newer than latest listed {0}: {1}. A fresh PowerShell session may auto-load the highest retained version unless imports use -RequiredVersion." -f $targetVersion, ($newerInstalledVersions -join ', ')) } else { Get-Module -Name $moduleName | Remove-Module -Force -ErrorAction SilentlyContinue foreach ($newerVersion in $newerInstalledVersions) { $caption = "Confirm removal of newer-than-listed $moduleName module version: $newerVersion" $message = "$moduleName $newerVersion is installed above latest listed version $targetVersion. It may be an unlisted development-channel candidate and PowerShell may auto-load it in a fresh session. Remove it now?" if (-not $PSCmdlet.ShouldContinue($message, $caption)) { throw "Latest-listed rollback cancelled because $moduleName $newerVersion remains installed. Rerun and approve removal, or pass -KeepNewerVersions to accept the auto-loading risk explicitly." } if ($PSCmdlet.ShouldProcess("$moduleName $newerVersion", 'Uninstall module version newer than latest listed')) { Uninstall-Module -Name $moduleName -RequiredVersion $newerVersion -Force -ErrorAction Stop Write-Host "Removed $moduleName $newerVersion." -ForegroundColor Green } } Import-Module -Name $moduleName -RequiredVersion $targetVersion -Force } } } $refreshParameters = @{ Destination = $workflowFull Platform = $Platform PassThru = $true } if ($AllowRollbackMarkerRemoval -and (Get-Command Update-AzLocalPipelineExample).Parameters.ContainsKey('AllowRollbackMarkerRemoval')) { $refreshParameters.AllowRollbackMarkerRemoval = $true } $refreshResults = @(Update-AzLocalPipelineExample @refreshParameters) $blockedRollbacks = @($refreshResults | Where-Object { $_.Action -eq 'Skipped-RollbackMarkerRemoval' }) if ($blockedRollbacks.Count -gt 0) { $blockedNames = ($blockedRollbacks | ForEach-Object { Split-Path -Leaf $_.File }) -join ', ' throw "Pipeline rollback stopped because older templates would remove customized marker sections from: $blockedNames. Review the diff, then rerun with -AllowRollbackMarkerRemoval to approve only those marker removals." } # --------------------------------------------------------------------------- # 3. Stage ONLY the workflow folder, the repo-root config\, the managed # README.md, and THIS script, then commit and push when there is something # to commit. Scoping the 'git add' keeps unrelated working-tree changes out # of this automated commit. '-A -- <path>' also captures the canonical-name # renames/deletes that Update may perform. # --------------------------------------------------------------------------- if ($NoPush) { Write-Host "-NoPush set; skipping git commit/push. Review with 'git status' / 'git diff'." -ForegroundColor Yellow return } # Include THIS script too: Update-AzLocalPipelineExample may have refreshed it # in place (version-gated self-update) when the module shipped a newer # template. Staging it commits/pushes that improvement instead of leaving it as # a dirty working-tree change. Only added when the script actually lives inside # the repo (the normal dropped-at-repo-root case); resolved to a repo-relative, # forward-slashed path so it survives the Test-Path filter below. $selfRel = $null if ($PSCommandPath) { try { $selfFull = (Resolve-Path -LiteralPath $PSCommandPath).ProviderPath $repoFull = (Resolve-Path -LiteralPath $RepoRoot).ProviderPath.TrimEnd('\', '/') if ($selfFull.StartsWith($repoFull, [System.StringComparison]::OrdinalIgnoreCase)) { $selfRel = $selfFull.Substring($repoFull.Length).TrimStart('\', '/') -replace '\\', '/' } } catch { $selfRel = $null } } $candidatePaths = @($WorkflowSubPath, 'config', 'DevChannel') if ($selfRel) { $candidatePaths += $selfRel } # Include the managed repo README too, but ONLY when it is module-managed # (carries the hidden AZLOCAL-README-VERSION marker). An operator-owned README - # one without the marker - is deliberately left out so unrelated README edits # are never swept into this automated commit. $readmeFull = Join-Path -Path $RepoRoot -ChildPath 'README.md' if ((Test-Path -LiteralPath $readmeFull -PathType Leaf) -and ((Get-Content -LiteralPath $readmeFull -Raw) -match 'AZLOCAL-README-VERSION')) { $candidatePaths += 'README.md' } $gitPaths = $candidatePaths | Where-Object { Test-Path -LiteralPath (Join-Path -Path $RepoRoot -ChildPath $_) } if ($gitPaths.Count -eq 0) { Write-Warning "Neither '$WorkflowSubPath' nor 'config' exists under '$RepoRoot'; nothing to stage." return } & git -C $RepoRoot add -A -- $gitPaths if ($LASTEXITCODE -ne 0) { throw "git add failed (exit code $LASTEXITCODE)." } & git -C $RepoRoot diff --cached --quiet if ($LASTEXITCODE -eq 0) { Write-Host "No changes to commit - pipelines already match $moduleName $version." -ForegroundColor Green return } if ($PSCmdlet.ShouldProcess($RepoRoot, "git commit and push AzLocal pipeline refresh ($moduleName $version)")) { & git -C $RepoRoot commit -m "Refresh AzLocal update pipelines for $moduleName $version" if ($LASTEXITCODE -ne 0) { throw "git commit failed (exit code $LASTEXITCODE)." } & git -C $RepoRoot push if ($LASTEXITCODE -ne 0) { throw "git push failed (exit code $LASTEXITCODE)." } Write-Host "Pushed pipeline refresh for $moduleName $version." -ForegroundColor Green } |