public/Update-OSDeployCoreCatalogs.ps1

#Requires -PSEdition Core
#Requires -Version 7.4

function Update-OSDeployCoreCatalogs {
    <#
    .SYNOPSIS
        Updates OSDeploy operating system catalogs
 
    .DESCRIPTION
        Runs the module's validated Windows 11 25H2 catalog updater when the module
        operating system catalog directory is writable. After validation and publication,
        copies the current catalog to the OSDeploy Core OSDCloud catalog directory under
        OSDCloud\catalogs\operatingsystems.
 
        Existing module catalogs remain in place. Any prerequisite, download, validation,
        publication, or cache-copy failure is reported as a warning and does not produce a
        terminating error.
 
    .PARAMETER MinimumItemCount
        Specifies the minimum number of ESD records required before a catalog is accepted.
        The default is 50. Valid values are 1 through 100000.
 
    .EXAMPLE
        PS> Update-OSDeployCoreCatalogs
 
        Updates the module operating system catalog and synchronizes it to the OSDeploy Core
        catalog cache.
 
    .EXAMPLE
        PS> Update-OSDeployCoreCatalogs -WhatIf
 
        Downloads and validates the current catalog, then previews module publication and
        cache synchronization without changing either location.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        System.Management.Automation.PSCustomObject. Returns the detected build, module and
        cache catalog paths, publication and cache-copy status, item count, and SHA256 hash
        after successful catalog validation.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
        Version: 1.0.0
        Date: 2026-09-17
 
        Requires Windows, PowerShell 7.4 or later, internet access, expand.exe, writable
        temporary storage, and write access to the module catalog and OSDeploy Core cache
        directories when updating.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'The public command name explicitly represents multiple module catalog types.')]
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param (
        [Parameter()]
        [ValidateRange(1, 100000)]
        [int]
        $MinimumItemCount = 50
    )

    $functionName = $MyInvocation.MyCommand.Name
    $moduleCatalogDirectory = Join-Path $script:OSDeployModuleBase 'core\operatingsystems'
    $updaterPath = Join-Path $script:OSDeployModuleBase 'core\scripts\update-operatingsystems.ps1'
    $cacheCatalogDirectory = Join-Path $script:OSDeployCorePath 'OSDCloud\catalogs\operatingsystems'
    $writeProbePath = $null

    try {
        if (-not $IsWindows) {
            throw 'Windows is required.'
        }
        if (-not (Test-Path -LiteralPath $updaterPath -PathType Leaf)) {
            throw "Catalog updater was not found at '$updaterPath'."
        }
        if (-not (Test-Path -LiteralPath $moduleCatalogDirectory -PathType Container)) {
            throw "Module catalog directory was not found at '$moduleCatalogDirectory'."
        }

        # A create-and-delete probe verifies effective write access instead of inferring it from ACLs.
        if (-not $WhatIfPreference) {
            $writeProbePath = Join-Path $moduleCatalogDirectory ('.write-test-' + [guid]::NewGuid() + '.tmp')
            $writeProbe = [System.IO.File]::Open(
                $writeProbePath,
                [System.IO.FileMode]::CreateNew,
                [System.IO.FileAccess]::Write,
                [System.IO.FileShare]::None
            )
            $writeProbe.Dispose()
            Remove-Item -LiteralPath $writeProbePath -Force
            $writeProbePath = $null
        }

        $operation = 'Update module operating system catalogs and synchronize the OSDeploy Core catalog cache'
        $approved = $PSCmdlet.ShouldProcess($moduleCatalogDirectory, $operation)
        if (-not $approved -and -not $WhatIfPreference) {
            return
        }

        # The updater retains acquisition and integrity validation under WhatIf.
        $updateResult = & $updaterPath `
            -MinimumItemCount $MinimumItemCount `
            -WhatIf:$WhatIfPreference `
            -Confirm:$false

        if (-not $updateResult) {
            throw 'Catalog updater returned no validation result.'
        }

        $cacheCatalogPath = Join-Path $cacheCatalogDirectory ([System.IO.Path]::GetFileName($updateResult.DestinationPath))
        $cached = $false

        if (-not $WhatIfPreference) {
            if (-not (Test-Path -LiteralPath $updateResult.DestinationPath -PathType Leaf)) {
                throw "Validated catalog was not found at '$($updateResult.DestinationPath)'."
            }

            # Stage in the cache directory so replacement cannot expose a partial catalog.
            $null = New-Item -Path $cacheCatalogDirectory -ItemType Directory -Force
            $stagedCachePath = Join-Path $cacheCatalogDirectory ('.' + [System.IO.Path]::GetFileName($cacheCatalogPath) + '.' + [guid]::NewGuid() + '.tmp')
            try {
                [System.IO.File]::Copy($updateResult.DestinationPath, $stagedCachePath, $false)
                [System.IO.File]::Move($stagedCachePath, $cacheCatalogPath, $true)
            }
            finally {
                if (Test-Path -LiteralPath $stagedCachePath -PathType Leaf) {
                    Remove-Item -LiteralPath $stagedCachePath -Force -ErrorAction SilentlyContinue
                }
            }

            $cached = $true
        }

        [pscustomobject]@{
            Build = $updateResult.Build
            ModuleCatalogPath = $updateResult.DestinationPath
            CacheCatalogPath = $cacheCatalogPath
            Published = [bool]$updateResult.Published
            Cached = $cached
            ItemCount = $updateResult.ItemCount
            Sha256 = $updateResult.Sha256
        }
    }
    catch {
        Write-Warning "[$(Get-Date -Format s)] [$functionName] Catalog update was not successful. $($_.Exception.Message)"
    }
    finally {
        if ($writeProbePath -and (Test-Path -LiteralPath $writeProbePath -PathType Leaf)) {
            Remove-Item -LiteralPath $writeProbePath -Force -ErrorAction SilentlyContinue
        }
    }
}