Private/steps-osd/Step-OSDCloudUpdatePowerShellModules.ps1

function Step-OSDCloudUpdatePowerShellModules {
    <#
    .SYNOPSIS
    Updates locally staged PowerShell modules from PowerShell Gallery.
 
    .DESCRIPTION
    Verifies connectivity to PowerShell Gallery, ensures the local PowerShell folders exist,
    and updates modules under C:\Program Files\WindowsPowerShell\Modules when newer gallery
    versions are available.
 
    .EXAMPLE
    Step-OSDCloudUpdatePowerShellModules
    Checks gallery connectivity and updates any out-of-date modules in the local modules path.
 
    .LINK
    https://github.com/OSDeploy/OSD/tree/master/docs
 
    .NOTES
    Author: David Segura - Recast Software
    2026-07-15 - Added comment-based help and improved module update logic with summary metrics
    #>

    [CmdletBinding()]
    param ()
    #=================================================
    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)]"
    #=================================================
    if ($env:SystemDrive -ne 'X:') {
        # Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] Skip. Not running in WinPE (X:)"
        return
    }
    #=================================================
    # Is it reachable online?
    try {
        $WebRequest = Invoke-WebRequest -Uri 'https://www.powershellgallery.com' -UseBasicParsing -Method Head -ErrorAction Stop
        if ($WebRequest.StatusCode -eq 200) {
            Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] PowerShell Gallery returned a 200 status code. OK."
        }
    }
    catch {
        Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] PowerShell Gallery is not reachable."
        return
    }
    #=================================================
    #region Main
    $PowerShellSavePath = 'C:\Program Files\WindowsPowerShell'
    $ConfigurationPath = Join-Path -Path $PowerShellSavePath -ChildPath 'Configuration'
    $ModulesPath = Join-Path -Path $PowerShellSavePath -ChildPath 'Modules'
    $ScriptsPath = Join-Path -Path $PowerShellSavePath -ChildPath 'Scripts'

    foreach ($Path in @($ConfigurationPath, $ModulesPath, $ScriptsPath)) {
        if (-not (Test-Path $Path)) {
            New-Item -Path $Path -ItemType Directory -Force | Out-Null
        }
    }

    $ExistingModules = Get-ChildItem -Path $ModulesPath -Directory -ErrorAction SilentlyContinue |
        Select-Object -ExpandProperty Name |
        Sort-Object -Unique

    $UpdatedCount = 0
    $UpToDateCount = 0
    $SkippedCount = 0
    $FailedCount = 0

    foreach ($Name in $ExistingModules) {
        $FindModule = Find-Module -Name $Name -ErrorAction SilentlyContinue
        if ($null -eq $FindModule) {
            $SkippedCount++
            continue
        }

        $ModulePath = Join-Path -Path $ModulesPath -ChildPath $Name
        $CurrentVersion = Get-ChildItem -Path $ModulePath -Directory -ErrorAction SilentlyContinue |
            Where-Object { $_.Name -match '^\d+(\.\d+){1,3}$' } |
            ForEach-Object { [version]$_.Name } |
            Sort-Object -Descending |
            Select-Object -First 1

        if (($null -ne $CurrentVersion) -and ($CurrentVersion -ge $FindModule.Version)) {
            Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] $Name is current ($CurrentVersion)."
            $UpToDateCount++
            continue
        }

        try {
            Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] Updating $Name to $($FindModule.Version)"
            Save-Module -Name $Name -Path $ModulesPath -Force -ErrorAction Stop
            $UpdatedCount++
        }
        catch {
            Write-Warning "[$(Get-Date -format s)] Save-Module failed: $Name"
            $FailedCount++
        }
    }

    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] Module update summary: Updated=$UpdatedCount UpToDate=$UpToDateCount Skipped=$SkippedCount Failed=$FailedCount"
    #=================================================
    Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] End"
    #=================================================
}