Public/Get-AvmUpdatePlan.ps1
|
function Get-AvmUpdatePlan { <# .SYNOPSIS Scans a directory, looks up the latest version for each AVM module, performs risk analysis, and returns the set of update candidates. .DESCRIPTION Combines Get-AvmModuleReference, Get-AvmLatestVersion, and Get-AvmUpdateRisk into a single pipeline. Returns only modules where an update is available (updateType != none), enriched with risk tier, risk reasons, and the interface diff. Also returns a summary count by updateType. .PARAMETER Path Root directory to scan. Defaults to the current directory. .PARAMETER Exclude Array of glob patterns to exclude. .PARAMETER ConfigPath Path to the avmupdater.config.json file. .OUTPUTS PSCustomObject with: candidates[], summary (counts by updateType and risk tier). .EXAMPLE Get-AvmUpdatePlan -Path ./infra .EXAMPLE Get-AvmUpdatePlan -Path . -Exclude @('**/.terraform/**') -ConfigPath ./config/avmupdater.config.json #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Position = 0)] [string]$Path = '.', [Parameter()] [string[]]$Exclude = @(), [Parameter()] [string]$ConfigPath ) $cfg = Get-AvmConfig -ConfigPath $ConfigPath # Build exclude list from args + config $allExcludes = @($Exclude) + @($cfg.excludePaths) $ignoreModulePatterns = @($cfg.ignoreModules) $pinnedVersions = if ($cfg.pinnedVersions -is [hashtable]) { $cfg.pinnedVersions } else { @{} } Write-Verbose "Scanning $Path for AVM references..." $refs = @(Get-AvmModuleReference -Path $Path -Exclude $allExcludes) Write-Verbose "Found $($refs.Count) AVM references. Looking up versions..." $candidates = [System.Collections.Generic.List[PSCustomObject]]::new() $lookupFailed = [System.Collections.Generic.List[PSCustomObject]]::new() $upToDate = [System.Collections.Generic.List[PSCustomObject]]::new() $ignored = [System.Collections.Generic.List[PSCustomObject]]::new() $pinned = [System.Collections.Generic.List[PSCustomObject]]::new() $upToDateByConstraint = [System.Collections.Generic.List[PSCustomObject]]::new() $skippedConstraints = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($ref in $refs) { # --- Policy: ignoreModules — skip entirely, no version lookup performed --- $ignoreMatch = $ignoreModulePatterns | Where-Object { Test-AvmModuleIdentifierMatch -Reference $ref -Pattern $_ } | Select-Object -First 1 if ($ignoreMatch) { $ignored.Add([PSCustomObject]@{ ecosystem = $ref.ecosystem module = $ref.module registryPath = $ref.registryPath file = $ref.file lineNumber = $ref.lineNumber currentVersion = $ref.currentVersion matchedPattern = $ignoreMatch reason = 'ignored by config (ignoreModules)' }) continue } $lookup = Get-AvmLatestVersion -RegistryPath $ref.registryPath -Ecosystem $ref.ecosystem -Config $cfg if ($lookup.status -eq 'lookup-failed') { $lookupFailed.Add([PSCustomObject]@{ reference = $ref status = 'lookup-failed' riskTier = 'UNKNOWN' riskReasons = @("Version lookup failed — could not reach registry for $($ref.registryPath)") }) continue } $latestStable = $lookup.latestStable if (-not $latestStable) { Write-Warning "No stable version found for $($ref.registryPath)" continue } # --- Policy: pinnedVersions — cap the target at the highest version <= pin --- $pinMatchKey = $pinnedVersions.Keys | Where-Object { Test-AvmModuleIdentifierMatch -Reference $ref -Pattern $_ } | Select-Object -First 1 $pinReason = $null if ($pinMatchKey) { $pinValue = $pinnedVersions[$pinMatchKey] $cappedVersion = Get-AvmPinnedVersion -AllVersions $lookup.allVersions -Pin $pinValue $pinned.Add([PSCustomObject]@{ ecosystem = $ref.ecosystem module = $ref.module registryPath = $ref.registryPath file = $ref.file lineNumber = $ref.lineNumber currentVersion = $ref.currentVersion pin = $pinValue cappedVersion = $cappedVersion matchedPattern = $pinMatchKey }) if (-not $cappedVersion) { Write-Warning "Pinned version '$pinValue' for $($ref.registryPath) has no matching available version — skipping." continue } $latestStable = $cappedVersion $pinReason = "pinned to $pinValue by config" } # --- Constraint refs (Terraform "~>", ">=, <", etc.) get their own evaluation path --- if ($ref.isConstraint) { $constraintEval = Test-AvmTerraformConstraint -Constraint $ref.currentVersion -TargetVersion $latestStable if (-not $constraintEval.parseable) { $skippedConstraints.Add([PSCustomObject]@{ ecosystem = $ref.ecosystem module = $ref.module registryPath = $ref.registryPath file = $ref.file lineNumber = $ref.lineNumber currentVersion = $ref.currentVersion latestStable = $latestStable riskReasons = @("Constraint '$($ref.currentVersion)' could not be parsed — skipped to avoid mangling the version pin. Review and update manually.") }) continue } if ($constraintEval.satisfied) { $upToDateByConstraint.Add([PSCustomObject]@{ ecosystem = $ref.ecosystem module = $ref.module registryPath = $ref.registryPath file = $ref.file lineNumber = $ref.lineNumber currentVersion = $ref.currentVersion latestStable = $latestStable status = 'up-to-date-by-constraint' note = "Constraint '$($ref.currentVersion)' already permits latest stable '$latestStable' — no file change needed; 'terraform init -upgrade' will pick it up." }) continue } if (-not $constraintEval.canRewrite) { $skippedConstraints.Add([PSCustomObject]@{ ecosystem = $ref.ecosystem module = $ref.module registryPath = $ref.registryPath file = $ref.file lineNumber = $ref.lineNumber currentVersion = $ref.currentVersion latestStable = $latestStable riskReasons = @("Range/complex constraint '$($ref.currentVersion)' does not permit latest stable '$latestStable' — skipped rather than auto-rewritten. Review and update manually.") }) continue } # Not satisfied, but a safe rewrite could be computed (pessimistic/exact single clause). $updateType = Get-AvmUpdateType -Current $constraintEval.baseVersion -Target $latestStable # Use a clean semver "currentVersion" (not the raw constraint text) for risk / interface # diff / changelog lookups — those build GitHub/registry URLs from currentVersion and a # literal '~> 0.3' would break the fetch. $effectiveRef = $ref.PSObject.Copy() $effectiveRef.currentVersion = $constraintEval.baseVersion $risk = Get-AvmUpdateRisk -Reference $effectiveRef -TargetVersion $latestStable $riskReasons = @($risk.riskReasons) if ($pinReason) { $riskReasons += $pinReason } $candidates.Add([PSCustomObject]@{ reference = $ref ecosystem = $ref.ecosystem module = $ref.module registryPath = $ref.registryPath file = $ref.file lineNumber = $ref.lineNumber currentVersion = $ref.currentVersion latestStable = $constraintEval.proposedConstraint resolvedVersion = $latestStable updateType = $updateType isConstraint = $true riskTier = $risk.riskTier riskReasons = $riskReasons interfaceDiff = $risk.interfaceDiff changelogLines = $risk.changelogLines status = 'pending' pinnedTo = if ($pinReason) { $pinnedVersions[$pinMatchKey] } else { $null } note = "Constraint rewrite: '$($ref.currentVersion)' → '$($constraintEval.proposedConstraint)' (resolves latest stable '$latestStable')." versionLineMissing = $ref.versionLineMissing }) continue } $updateType = Get-AvmUpdateType -Current $ref.currentVersion -Target $latestStable if ($updateType -eq 'none') { $upToDate.Add([PSCustomObject]@{ ecosystem = $ref.ecosystem module = $ref.module registryPath = $ref.registryPath file = $ref.file lineNumber = $ref.lineNumber currentVersion = $ref.currentVersion latestStable = $latestStable status = 'up-to-date' }) continue } # Enrich with risk analysis $risk = Get-AvmUpdateRisk -Reference $ref -TargetVersion $latestStable $riskReasons = @($risk.riskReasons) if ($pinReason) { $riskReasons += $pinReason } $candidates.Add([PSCustomObject]@{ reference = $ref ecosystem = $ref.ecosystem module = $ref.module registryPath = $ref.registryPath file = $ref.file lineNumber = $ref.lineNumber currentVersion = $ref.currentVersion latestStable = $latestStable resolvedVersion = $latestStable updateType = $updateType isConstraint = $ref.isConstraint riskTier = $risk.riskTier riskReasons = $riskReasons interfaceDiff = $risk.interfaceDiff changelogLines = $risk.changelogLines status = 'pending' pinnedTo = if ($pinReason) { $pinnedVersions[$pinMatchKey] } else { $null } note = $null versionLineMissing = $ref.versionLineMissing }) } # Build summary $allItems = @($candidates) + @($lookupFailed) $summary = [PSCustomObject]@{ scannedAt = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') pathsScanned = @($Path) totalRefs = $refs.Count totalUpdates = $candidates.Count totalUpToDate = $upToDate.Count byUpdateType = @{ major = @($candidates | Where-Object updateType -eq 'major').Count minor = @($candidates | Where-Object updateType -eq 'minor').Count patch = @($candidates | Where-Object updateType -eq 'patch').Count constraint = @($candidates | Where-Object isConstraint -eq $true).Count } byRiskTier = @{ HIGH = @($allItems | Where-Object riskTier -eq 'HIGH').Count MEDIUM = @($allItems | Where-Object riskTier -eq 'MEDIUM').Count LOW = @($allItems | Where-Object riskTier -eq 'LOW').Count UNKNOWN = @($allItems | Where-Object riskTier -eq 'UNKNOWN').Count } lookupFailed = $lookupFailed.Count ignored = $ignored.Count pinned = $pinned.Count upToDateByConstraint = $upToDateByConstraint.Count skippedConstraints = $skippedConstraints.Count } return [PSCustomObject]@{ candidates = $candidates.ToArray() upToDate = $upToDate.ToArray() lookupFailed = $lookupFailed.ToArray() ignored = $ignored.ToArray() pinned = $pinned.ToArray() upToDateByConstraint = $upToDateByConstraint.ToArray() skippedConstraints = $skippedConstraints.ToArray() summary = $summary } } |