Public/Update-PSDependencyCanary.ps1
|
function Update-PSDependencyCanary { <# .SYNOPSIS Updates a module project's dependencies only after baseline and candidate tests pass. .DESCRIPTION Finds newer PSGallery versions for dependencies declared in requirements.psd1 and RequiredModules. When updates are available, the unchanged project must pass its Test build task before any files are edited. The complete candidate is then applied temporarily and tested again with dependency bootstrap enabled. Failed candidates are restored. Build-only updates do not change the module version; a RequiredModules update patch-bumps the shipped module manifest and updates the changelog. This supporting command is invoked by the shared Canary task in PSDepend, PowerShellBuild, and psake projects. .PARAMETER ProjectRoot Root directory of the PowerShell module project. Defaults to BHProjectPath when available and otherwise to the current directory. .PARAMETER ModuleManifestPath Optional module manifest path. Conventional src layouts are auto-discovered. .PARAMETER RequirementsPath PSDepend build requirements file. Defaults to requirements.psd1. .PARAMETER ChangelogPath Keep a Changelog document updated when RequiredModules changes. Defaults to CHANGELOG.md in the project root. The file is required only when a runtime dependency changes and changelog updates are enabled. .PARAMETER SkipChangelog Patch-bump runtime dependency candidates without reading or updating a changelog. .EXAMPLE Update-PSDependencyCanary -ProjectRoot . -WhatIf Previews available changes in a compatible project without bootstrapping, testing, or editing. Use the shared Canary task to apply a candidate. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param( [string]$ProjectRoot, [string]$ModuleManifestPath, [string]$RequirementsPath, [string]$ChangelogPath, [switch]$SkipChangelog ) if ([string]::IsNullOrWhiteSpace($ProjectRoot)) { $ProjectRoot = if ($env:BHProjectPath) { $env:BHProjectPath } else { (Get-Location).Path } } $ProjectRoot = (Resolve-Path -LiteralPath $ProjectRoot).Path if ([string]::IsNullOrWhiteSpace($ModuleManifestPath)) { $ModuleManifestPath = Get-PSDependencyCanaryProjectManifest -Root $ProjectRoot } elseif (-not [IO.Path]::IsPathRooted($ModuleManifestPath)) { $ModuleManifestPath = Join-Path -Path $ProjectRoot -ChildPath $ModuleManifestPath } $ModuleManifestPath = (Resolve-Path -LiteralPath $ModuleManifestPath).Path if ([string]::IsNullOrWhiteSpace($RequirementsPath)) { $RequirementsPath = Join-Path -Path $ProjectRoot -ChildPath 'requirements.psd1' } elseif (-not [IO.Path]::IsPathRooted($RequirementsPath)) { $RequirementsPath = Join-Path -Path $ProjectRoot -ChildPath $RequirementsPath } $RequirementsPath = (Resolve-Path -LiteralPath $RequirementsPath).Path if ($SkipChangelog -and $PSBoundParameters.ContainsKey('ChangelogPath')) { throw 'ChangelogPath and SkipChangelog cannot be used together.' } if (-not $SkipChangelog) { if ([string]::IsNullOrWhiteSpace($ChangelogPath)) { $ChangelogPath = Join-Path -Path $ProjectRoot -ChildPath 'CHANGELOG.md' } elseif (-not [IO.Path]::IsPathRooted($ChangelogPath)) { $ChangelogPath = Join-Path -Path $ProjectRoot -ChildPath $ChangelogPath } $ChangelogPath = [IO.Path]::GetFullPath($ChangelogPath) } else { $ChangelogPath = $null } $utf8NoBom = New-Object System.Text.UTF8Encoding($false) $originalRequirements = [IO.File]::ReadAllText($RequirementsPath) $originalManifest = [IO.File]::ReadAllText($ModuleManifestPath) $changelogExists = $ChangelogPath -and (Test-Path -LiteralPath $ChangelogPath -PathType Leaf) $originalChangelog = if ($changelogExists) { [IO.File]::ReadAllText($ChangelogPath) } else { $null } $manifestData = Import-PowerShellDataFile -LiteralPath $ModuleManifestPath $moduleName = [IO.Path]::GetFileNameWithoutExtension($ModuleManifestPath) $currentVersion = [version]$manifestData.ModuleVersion $buildSpecifications = @( Get-PSDependencyCanaryRequirement -Content $originalRequirements -Path $RequirementsPath | ForEach-Object { [pscustomobject]@{ Type = 'Requirement' ModuleName = $_.ModuleName CurrentVersion = $_.CurrentVersion } } ) $runtimeSpecifications = @( Get-PSDependencyCanaryRequiredModule -Content $originalManifest -Path $ModuleManifestPath | ForEach-Object { [pscustomobject]@{ Type = 'RequiredModule' ModuleName = $_.ModuleName CurrentVersion = $_.CurrentVersion } } ) $allSpecifications = @($buildSpecifications + $runtimeSpecifications) $requirementNames = @($buildSpecifications.ModuleName | Sort-Object -Unique) $missingRuntimeRequirements = @( $runtimeSpecifications.ModuleName | Sort-Object -Unique | Where-Object { $requirementNames -notcontains $_ } ) if ($missingRuntimeRequirements.Count -gt 0) { throw "RequiredModules must also be declared in requirements.psd1: $($missingRuntimeRequirements -join ', ')." } $candidateVersions = @{} $updates = @() foreach ($dependencyName in @($allSpecifications.ModuleName | Sort-Object -Unique)) { $latestVersion = [version](Find-PSDependencyCanaryModuleVersion -Name $dependencyName) $declaredVersions = @( $allSpecifications | Where-Object ModuleName -EQ $dependencyName | ForEach-Object { $parsedVersion = $null if ([version]::TryParse([string]$_.CurrentVersion, [ref]$parsedVersion)) { $parsedVersion } } ) $candidateVersion = @($declaredVersions + $latestVersion | Sort-Object -Descending | Select-Object -First 1)[0] $candidateVersions[$dependencyName] = [string]$candidateVersion foreach ($specification in @($allSpecifications | Where-Object ModuleName -EQ $dependencyName)) { $current = $null $hasVersion = [version]::TryParse([string]$specification.CurrentVersion, [ref]$current) if (-not $hasVersion -or $candidateVersion -gt $current) { $updates += [pscustomobject][ordered]@{ Type = $specification.Type Name = $dependencyName CurrentVersion = if ($hasVersion) { [string]$current } else { [string]$specification.CurrentVersion } CandidateVersion = [string]$candidateVersion } } } } $requirementsChanged = @($updates | Where-Object Type -EQ 'Requirement').Count -gt 0 $requiredModulesChanged = @($updates | Where-Object Type -EQ 'RequiredModule').Count -gt 0 $changed = $requirementsChanged -or $requiredModulesChanged $candidateVersion = $currentVersion $candidateRequirements = $originalRequirements $candidateManifest = $originalManifest $candidateChangelog = $originalChangelog if ($requirementsChanged) { $candidateRequirements = ConvertTo-PSDependencyCanaryRequirementContent ` -Content $originalRequirements ` -Path $RequirementsPath ` -Version $candidateVersions } if ($requiredModulesChanged) { $candidateManifest = ConvertTo-PSDependencyCanaryRequiredModuleContent ` -Content $originalManifest ` -Path $ModuleManifestPath ` -Version $candidateVersions $candidateVersion = Get-PSDependencyCanaryNextVersion -Version $currentVersion $candidateManifest = ConvertTo-PSDependencyCanaryManifestContent ` -Content $candidateManifest ` -Path $ModuleManifestPath ` -Version $candidateVersion $runtimeVersionMap = @{} foreach ($update in @($updates | Where-Object Type -EQ 'RequiredModule')) { $runtimeVersionMap[$update.Name] = $update.CandidateVersion } } $applied = $false $baselineTested = $false $tested = $false if ($changed -and $PSCmdlet.ShouldProcess( $ProjectRoot, 'Run baseline tests, bootstrap dependency updates, run candidate tests, and retain the passing candidate' )) { if ($requiredModulesChanged -and -not $SkipChangelog) { if (-not $changelogExists) { throw "The changelog was not found at '$ChangelogPath'. Specify -ChangelogPath or use -SkipChangelog." } $candidateChangelog = ConvertTo-PSDependencyCanaryChangelog ` -Content $originalChangelog ` -Version $candidateVersion ` -Dependency $runtimeVersionMap } $powerShellPath = (Get-Process -Id $PID).Path Write-Verbose "Running the unchanged project's baseline Test task." Invoke-PSDependencyCanaryProjectBuild ` -ProjectRoot $ProjectRoot ` -Task Test ` -PowerShellPath $powerShellPath $baselineTested = $true try { if ($requirementsChanged) { [IO.File]::WriteAllText($RequirementsPath, $candidateRequirements, $utf8NoBom) } if ($requiredModulesChanged) { [IO.File]::WriteAllText($ModuleManifestPath, $candidateManifest, $utf8NoBom) if (-not $SkipChangelog) { [IO.File]::WriteAllText($ChangelogPath, $candidateChangelog, $utf8NoBom) } } Write-Verbose 'Bootstrapping the updated requirements and running the dependency candidate Test task.' Invoke-PSDependencyCanaryProjectBuild ` -ProjectRoot $ProjectRoot ` -Task Test ` -PowerShellPath $powerShellPath ` -Bootstrap $tested = $true $applied = $true } catch { [IO.File]::WriteAllText($RequirementsPath, $originalRequirements, $utf8NoBom) [IO.File]::WriteAllText($ModuleManifestPath, $originalManifest, $utf8NoBom) if ($requiredModulesChanged -and -not $SkipChangelog) { [IO.File]::WriteAllText($ChangelogPath, $originalChangelog, $utf8NoBom) } throw } } [pscustomobject][ordered]@{ Changed = $changed RequirementsChanged = $requirementsChanged RequiredModulesChanged = $requiredModulesChanged Applied = $applied BaselineTested = $baselineTested Tested = $tested ModuleName = $moduleName CurrentVersion = [string]$currentVersion CandidateVersion = [string]$candidateVersion Updates = @($updates) RequirementsPath = $RequirementsPath.Substring($ProjectRoot.Length).TrimStart('\', '/') ModuleManifestPath = $ModuleManifestPath.Substring($ProjectRoot.Length).TrimStart('\', '/') ChangelogPath = if ($ChangelogPath) { $ChangelogPath.Substring($ProjectRoot.Length).TrimStart('\', '/') } else { $null } ChangelogSkipped = [bool]$SkipChangelog } } |