From aa19a481e9051968a140889fcb098bab27c0ee06 Mon Sep 17 00:00:00 2001 From: David Segura Date: Tue, 29 Sep 2026 16:57:35 -0500 Subject: [PATCH 1/6] fix(driverpack): use InvariantCulture for date parsing in driver pack catalog --- .../core-driverpack/Get-OSDCoreDriverPackCatalogHP.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OSDCloud/private/core-driverpack/Get-OSDCoreDriverPackCatalogHP.ps1 b/OSDCloud/private/core-driverpack/Get-OSDCoreDriverPackCatalogHP.ps1 index 47bb5c5..d98af7d 100644 --- a/OSDCloud/private/core-driverpack/Get-OSDCoreDriverPackCatalogHP.ps1 +++ b/OSDCloud/private/core-driverpack/Get-OSDCoreDriverPackCatalogHP.ps1 @@ -54,7 +54,7 @@ function Get-OSDCoreDriverPackCatalogHP { Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Building driver pack catalog" $hpCatalogRoot = $xmlCatalogContent.NewDataSet.HPClientDriverPackCatalog if ($hpCatalogRoot -and $hpCatalogRoot.DateReleased) { - $dtDateReleased = [datetime]::ParseExact($hpCatalogRoot.DateReleased, 'yyyy-MM-dd', $null) + $dtDateReleased = [datetime]::ParseExact($hpCatalogRoot.DateReleased, 'yyyy-MM-dd', [System.Globalization.CultureInfo]::InvariantCulture) $catalogVersion = $dtDateReleased.ToString('yy.MM.dd') } else { @@ -76,7 +76,7 @@ function Get-OSDCoreDriverPackCatalogHP { $osVersion = $osVersion.Substring($osVersion.Length - 4) $template = 'M/d/yyyy hh:mm:ss tt' - $dtReleaseDate = [datetime]::ParseExact($hpSoftPaq.DateReleased, $template, $null) + $dtReleaseDate = [datetime]::ParseExact($hpSoftPaq.DateReleased, $template, [System.Globalization.CultureInfo]::InvariantCulture) $releaseDate = $dtReleaseDate.ToString('yy.MM.dd') $systemIds = if ($item.SystemId) { From 3339d3876312d9f3d412eadd9253465c04243069 Mon Sep 17 00:00:00 2001 From: David Segura Date: Wed, 30 Sep 2026 10:22:33 -0500 Subject: [PATCH 2/6] feat(sync): add synchronization script and workflow for operating system parity --- .../osdcloud-catalog-update.instructions.md | 2 +- .../scripts/Sync-OSDOperatingSystemParity.ps1 | 155 ++++++++++++++++++ .../scripts/osd-operating-system-sync.json | 32 ++++ .../SKILL.md | 8 +- .../check-osd-operating-system-parity.yaml | 38 +++++ 5 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 .github/scripts/Sync-OSDOperatingSystemParity.ps1 create mode 100644 .github/scripts/osd-operating-system-sync.json create mode 100644 .github/workflows/check-osd-operating-system-parity.yaml diff --git a/.github/instructions/osdcloud-catalog-update.instructions.md b/.github/instructions/osdcloud-catalog-update.instructions.md index ba81267..62c1b69 100644 --- a/.github/instructions/osdcloud-catalog-update.instructions.md +++ b/.github/instructions/osdcloud-catalog-update.instructions.md @@ -220,4 +220,4 @@ Key differences: - **Omitting the converter switch case** — a new OS XML will load without errors but produce zero OS options in the UX because the converter returns no release information for the build. - **Stale `surface.json` driver URLs** — `Get-OSDCoreDriverPackCatalogSurface` serves MSI URLs from `OSDCloud/core/driverpacks/surface.json`; if `UpdatePage` links change and the automated workflow has not run, deployed drivers may point to outdated or removed files. Run `.github/scripts/Update-MicrosoftCatalog.ps1` manually to refresh. - **Editing OEM XML snapshots manually** — Dell/HP/Lenovo XML is replaced wholesale from upstream; manual edits will be lost on the next snapshot refresh. -- **Adding a Windows version string to workflow configs without the catalog XML** — the UX will offer the version but `Get-OSDCloudCoreOperatingSystems` will return no matching ESD entries, causing `Initialize-DeployOSDCloud` to throw. +- **Adding a Windows version string to workflow configs without the catalog XML** — the UX will offer the version but `Get-OSDCoreOperatingSystems` will return no matching ESD entries, causing `Initialize-DeployOSDCloud` to throw. diff --git a/.github/scripts/Sync-OSDOperatingSystemParity.ps1 b/.github/scripts/Sync-OSDOperatingSystemParity.ps1 new file mode 100644 index 0000000..380bb52 --- /dev/null +++ b/.github/scripts/Sync-OSDOperatingSystemParity.ps1 @@ -0,0 +1,155 @@ +<# +.SYNOPSIS +Synchronizes canonical operating-system files from RecastOSDCloud to OSD. + +.DESCRIPTION +Reads the adjacent synchronization manifest and compares canonical RecastOSDCloud +operating-system functions and catalog files with an OSD repository checkout. In +check mode, reports drift and exits with an error without changing files. Otherwise, +copies missing or changed files and removes extra managed catalog files. + +.PARAMETER DestinationRepositoryPath +Specifies the root of the OSD repository to validate or update. + +.PARAMETER Check +Validates synchronization without changing the destination repository. + +.EXAMPLE +.\Sync-OSDOperatingSystemParity.ps1 -DestinationRepositoryPath ..\..\OSD -Check + +Checks an adjacent OSD repository for operating-system parity drift. + +.EXAMPLE +.\Sync-OSDOperatingSystemParity.ps1 -DestinationRepositoryPath ..\..\OSD + +Synchronizes the mapped operating-system files into an adjacent OSD repository. + +.LINK +https://github.com/OSDeploy/OSD/tree/master/docs + +.NOTES +Author: David Segura - Recast Software +2026-09-30 - Initial operating-system parity synchronization script +#> +[CmdletBinding(SupportsShouldProcess = $true)] +param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$DestinationRepositoryPath, + + [Parameter()] + [switch]$Check +) + +$Error.Clear() +$ErrorActionPreference = 'Stop' +$sourceRepositoryPath = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..')) +$destinationRepositoryPath = [System.IO.Path]::GetFullPath($DestinationRepositoryPath) +$manifestPath = Join-Path $PSScriptRoot 'osd-operating-system-sync.json' + +if (-not (Test-Path -LiteralPath $destinationRepositoryPath -PathType Container)) { + throw "Destination OSD repository was not found at '$destinationRepositoryPath'." +} +if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw "Operating-system synchronization manifest was not found at '$manifestPath'." +} + +$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json +$results = @() + +foreach ($mapping in $manifest.Files) { + $sourcePath = Join-Path $sourceRepositoryPath $mapping.Source + $destinationPath = Join-Path $destinationRepositoryPath $mapping.Destination + if (-not (Test-Path -LiteralPath $sourcePath -PathType Leaf)) { + throw "Canonical operating-system file was not found at '$sourcePath'." + } + + $state = if (-not (Test-Path -LiteralPath $destinationPath -PathType Leaf)) { + 'Missing' + } + elseif ((Get-FileHash -LiteralPath $sourcePath -Algorithm SHA256).Hash -cne (Get-FileHash -LiteralPath $destinationPath -Algorithm SHA256).Hash) { + 'Different' + } + else { + 'InSync' + } + + if (-not $Check -and $state -ne 'InSync' -and $PSCmdlet.ShouldProcess($destinationPath, 'Synchronize canonical operating-system file')) { + $null = New-Item -Path (Split-Path -Path $destinationPath -Parent) -ItemType Directory -Force + Copy-Item -LiteralPath $sourcePath -Destination $destinationPath -Force + $state = 'Synchronized' + } + + $results += [pscustomobject]@{ + Source = $mapping.Source + Destination = $mapping.Destination + State = $state + } +} + +foreach ($directoryMapping in $manifest.Directories) { + $sourceDirectory = Join-Path $sourceRepositoryPath $directoryMapping.Source + $destinationDirectory = Join-Path $destinationRepositoryPath $directoryMapping.Destination + if (-not (Test-Path -LiteralPath $sourceDirectory -PathType Container)) { + throw "Canonical operating-system directory was not found at '$sourceDirectory'." + } + if (-not (Test-Path -LiteralPath $destinationDirectory -PathType Container) -and -not $Check) { + $null = New-Item -Path $destinationDirectory -ItemType Directory -Force + } + + $sourceFiles = @(Get-ChildItem -LiteralPath $sourceDirectory -Filter $directoryMapping.Pattern -File) + foreach ($sourceFile in $sourceFiles) { + $relativeDestination = Join-Path $directoryMapping.Destination $sourceFile.Name + $destinationPath = Join-Path $destinationRepositoryPath $relativeDestination + $state = if (-not (Test-Path -LiteralPath $destinationPath -PathType Leaf)) { + 'Missing' + } + elseif ((Get-FileHash -LiteralPath $sourceFile.FullName -Algorithm SHA256).Hash -cne (Get-FileHash -LiteralPath $destinationPath -Algorithm SHA256).Hash) { + 'Different' + } + else { + 'InSync' + } + + if (-not $Check -and $state -ne 'InSync' -and $PSCmdlet.ShouldProcess($destinationPath, 'Synchronize canonical operating-system catalog')) { + Copy-Item -LiteralPath $sourceFile.FullName -Destination $destinationPath -Force + $state = 'Synchronized' + } + + $results += [pscustomobject]@{ + Source = Join-Path $directoryMapping.Source $sourceFile.Name + Destination = $relativeDestination + State = $state + } + } + + if ($directoryMapping.RemoveExtra -and (Test-Path -LiteralPath $destinationDirectory -PathType Container)) { + $sourceNames = @($sourceFiles.Name) + foreach ($destinationFile in Get-ChildItem -LiteralPath $destinationDirectory -Filter $directoryMapping.Pattern -File) { + if ($destinationFile.Name -in $sourceNames) { + continue + } + + $relativeDestination = Join-Path $directoryMapping.Destination $destinationFile.Name + $state = 'Extra' + if (-not $Check -and $PSCmdlet.ShouldProcess($destinationFile.FullName, 'Remove unmanaged operating-system catalog')) { + Remove-Item -LiteralPath $destinationFile.FullName -Force + $state = 'Removed' + } + + $results += [pscustomobject]@{ + Source = $null + Destination = $relativeDestination + State = $state + } + } + } +} + +$results +if ($Check) { + $drift = @($results | Where-Object State -NE 'InSync') + if ($drift.Count -gt 0) { + throw "Operating-system parity check found $($drift.Count) drifted file(s)." + } +} diff --git a/.github/scripts/osd-operating-system-sync.json b/.github/scripts/osd-operating-system-sync.json new file mode 100644 index 0000000..24db33f --- /dev/null +++ b/.github/scripts/osd-operating-system-sync.json @@ -0,0 +1,32 @@ +{ + "Files": [ + { + "Source": "OSDCloud/private/core-operatingsystem/ConvertTo-OSDCoreOperatingSystemInfo.ps1", + "Destination": "Private/core-operatingsystem/ConvertTo-OSDCoreOperatingSystemInfo.ps1" + }, + { + "Source": "OSDCloud/private/core-operatingsystem/Initialize-ModuleCoreOperatingSystems.ps1", + "Destination": "Private/core-operatingsystem/Initialize-ModuleCoreOperatingSystems.ps1" + }, + { + "Source": "OSDCloud/private/core-operatingsystem/Get-OSDCoreOperatingSystems.ps1", + "Destination": "Private/core-operatingsystem/Get-OSDCoreOperatingSystems.ps1" + }, + { + "Source": "OSDCloud/private/core-operatingsystem/Set-OSDCoreOperatingSystemCloudObject.ps1", + "Destination": "Private/core-operatingsystem/Set-OSDCoreOperatingSystemCloudObject.ps1" + }, + { + "Source": "OSDCloud/private/Update-OSDCloudCoreCatalogOS.ps1", + "Destination": "Private/core-operatingsystem/Update-OSDCloudCoreCatalogOS.ps1" + } + ], + "Directories": [ + { + "Source": "OSDCloud/core/operatingsystems", + "Destination": "core/operatingsystems", + "Pattern": "*.xml", + "RemoveExtra": true + } + ] +} diff --git a/.github/skills/osdcore-operating-system-cloud-object/SKILL.md b/.github/skills/osdcore-operating-system-cloud-object/SKILL.md index 3bd6bcc..5e649a9 100644 --- a/.github/skills/osdcore-operating-system-cloud-object/SKILL.md +++ b/.github/skills/osdcore-operating-system-cloud-object/SKILL.md @@ -1,6 +1,6 @@ --- name: osdcore-operating-system-cloud-object -description: "Use when identifying, mapping, reviewing, or changing $OSDCoreOperatingSystemCloudObject, Set-OSDCoreOperatingSystemCloudObject, Get-OSDCoreOperatingSystems, or Get-OSDCloudCoreOperatingSystems across the OSD and OSDCloud PowerShell modules." +description: "Use when identifying, mapping, reviewing, or changing $OSDCoreOperatingSystemCloudObject, Set-OSDCoreOperatingSystemCloudObject, or Get-OSDCoreOperatingSystems across the OSD and OSDCloud PowerShell modules." argument-hint: "OS filters or code path, for example: Windows 11 25H2 amd64 Retail en-us" --- @@ -17,7 +17,7 @@ Identify the selected object by its properties before consuming it: | Module shape | Strong indicators | Provider | | --- | --- | --- | | `OSD` | `Name`, `Version`, `ReleaseID`, `Architecture`, `Language`, `Activation`, `Build`, `Url`, `SHA1`, `SHA256` | `Get-OSDCoreOperatingSystems` | -| `OSDCloud` | `Id`, `OperatingSystem`, `OSName`, `OSVersion`, `OSArchitecture`, `OSActivation`, `OSLanguageCode`, `OSBuild`, `OSBuildVersion`, `FilePath`, `Sha1`, `Sha256` | `Get-OSDCloudCoreOperatingSystems` | +| `OSDCloud` | `Id`, `OperatingSystem`, `OSName`, `OSVersion`, `OSArchitecture`, `OSActivation`, `OSLanguageCode`, `OSBuild`, `OSBuildVersion`, `FilePath`, `Sha1`, `Sha256` | `Get-OSDCoreOperatingSystems` | Prefer module context when it is available: @@ -64,9 +64,7 @@ PowerShell property lookup is case-insensitive, but preserve the native property 1. Read `OSDCloud/private/core-operatingsystem/Set-OSDCoreOperatingSystemCloudObject.ps1` and the provider functions before editing. 2. Determine whether the call is running in `OSD`, `OSDCloud`, or helper/test code without a module name. -3. Load the matching catalog provider: - - `OSD`: `Get-OSDCoreOperatingSystems` - - `OSDCloud`: `Get-OSDCloudCoreOperatingSystems` +3. Load `Get-OSDCoreOperatingSystems`; it returns the native schema for its current module context. 4. Normalize input filters before comparing: architecture and language should be lowercase; map `x64` to `amd64` where user input or catalog data can contain either spelling. 5. Filter with equivalent properties, not a single module schema. 6. Sort by the full build equivalent: `Build` for `OSD`, `OSBuildVersion` for `OSDCloud`. diff --git a/.github/workflows/check-osd-operating-system-parity.yaml b/.github/workflows/check-osd-operating-system-parity.yaml new file mode 100644 index 0000000..5dca440 --- /dev/null +++ b/.github/workflows/check-osd-operating-system-parity.yaml @@ -0,0 +1,38 @@ +name: Check OSD Operating System Parity + +on: + workflow_dispatch: + pull_request: + paths: + - '.github/scripts/Sync-OSDOperatingSystemParity.ps1' + - '.github/scripts/osd-operating-system-sync.json' + - '.github/workflows/check-osd-operating-system-parity.yaml' + - 'OSDCloud/core/operatingsystems/**' + - 'OSDCloud/private/core-operatingsystem/**' + - 'OSDCloud/private/Update-OSDCloudCoreCatalogOS.ps1' + +permissions: + contents: read + +jobs: + check: + runs-on: windows-latest + + steps: + - name: Checkout RecastOSDCloud + uses: actions/checkout@v5 + with: + path: RecastOSDCloud + + - name: Checkout OSD + uses: actions/checkout@v5 + with: + repository: OSDeploy/OSD + path: OSD + + - name: Check operating system parity + shell: pwsh + run: | + & '.\RecastOSDCloud\.github\scripts\Sync-OSDOperatingSystemParity.ps1' ` + -DestinationRepositoryPath '.\OSD' ` + -Check From 8f4fe42091a33074a508f8996df1cfdaaabf32c2 Mon Sep 17 00:00:00 2001 From: David Segura Date: Wed, 30 Sep 2026 10:23:00 -0500 Subject: [PATCH 3/6] refactor(os): unify operating system retrieval functions and enhance documentation --- .../Get-OSDCloudCoreOperatingSystems.ps1 | 139 ------------------ .../Get-OSDCoreOperatingSystems.ps1 | 138 ++++++++--------- .../Initialize-ModuleCoreOperatingSystems.ps1 | 11 +- .../Set-OSDCoreOperatingSystemCloudObject.ps1 | 64 +------- .../dev/Get-OSDCloudDefaultOS.ps1 | 4 +- 5 files changed, 88 insertions(+), 268 deletions(-) delete mode 100644 OSDCloud/private/core-operatingsystem/Get-OSDCloudCoreOperatingSystems.ps1 diff --git a/OSDCloud/private/core-operatingsystem/Get-OSDCloudCoreOperatingSystems.ps1 b/OSDCloud/private/core-operatingsystem/Get-OSDCloudCoreOperatingSystems.ps1 deleted file mode 100644 index 0685b96..0000000 --- a/OSDCloud/private/core-operatingsystem/Get-OSDCloudCoreOperatingSystems.ps1 +++ /dev/null @@ -1,139 +0,0 @@ -function Get-OSDCloudCoreOperatingSystems { - <# - .SYNOPSIS - Gets parsed operating system records for OSDCloud selection workflows. - - .DESCRIPTION - Converts raw module catalog records from Initialize-ModuleCoreOperatingSystems into - normalized OSDCloud operating system objects. The function maps build numbers - to known Windows releases, derives build versions, normalizes architecture, - identifies activation channel, and emits sorted records with media metadata - such as language, size, file name, path, and hashes. - - .EXAMPLE - Get-OSDCloudCoreOperatingSystems - - Returns all available OSDCloud operating system records. - - .EXAMPLE - Get-OSDCloudCoreOperatingSystems | Where-Object { $_.OSName -eq 'Windows 11' } - - Returns only Windows 11 operating system records. - - .EXAMPLE - Get-OSDCloudCoreOperatingSystems | Group-Object OSArchitecture - - Groups results by normalized architecture. - - .INPUTS - None - You cannot pipe input to this function. - - .OUTPUTS - PSCustomObject[] - Parsed operating system records for OSDCloud catalog operations. - - .LINK - https://www.osdeploy.com/ - - .NOTES - Author: OSDeploy - 2026-08-05 - Standardized and expanded comment-based help - #> - [CmdletBinding()] - [OutputType([pscustomobject[]])] - param () - - $ErrorActionPreference = 'Stop' - $records = @() - $mctRecords = @() - - Initialize-ModuleCoreOperatingSystems - if (-not ($global:ModuleCoreOperatingSystems)) { - throw "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Unable to load Module Core Operating Systems." - } - $mctRecords = $global:ModuleCoreOperatingSystems - - if (-not $mctRecords) { - return $records - } - - foreach ($node in ($mctRecords | Sort-Object FileName, LanguageCode, Architecture)) { - # Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Processing $($node.FileName)" - - if ([string]::IsNullOrWhiteSpace($node.OSBuild) -or [string]::IsNullOrWhiteSpace($node.OSBuildVersion)) { - continue - } - #================================================= - # OperatingSystem / OSName / OSVersion - $operatingSystemInfo = ConvertTo-OSDCoreOperatingSystemInfo -OSBuild $node.OSBuild - if (-not $operatingSystemInfo) { - continue - } - $OperatingSystem = $operatingSystemInfo.OperatingSystem - $OSName = $operatingSystemInfo.OSName - $OSVersion = $operatingSystemInfo.OSVersion - #================================================= - # OSBuild / OSBuildVersion - #================================================= - $OSBuild = [string]$node.OSBuild - $OSBuildVersion = [string]$node.OSBuildVersion - #================================================= - # OSArchitecture - # Avoids confusion between x64 releases (amd64/arm64) - #================================================= - if ($node.Architecture -match 'x64') { - $OSArchitecture = 'amd64' - } - elseif ($node.Architecture -match 'arm64') { - $OSArchitecture = 'arm64' - } - else { - $OSArchitecture = 'x86' - continue - } - #================================================= - # OSActivation - #================================================= - if ($node.FileName -match 'clientconsumer_ret') { - $OSActivation = 'Retail' - } - elseif ($node.FileName -match 'CLIENTBUSINESS_VOL') { - $OSActivation = 'Volume' - } - else { - $OSActivation = 'Unknown' - continue - } - #================================================= - # Id - #================================================= - $Id = "$OperatingSystem $OSArchitecture $OSActivation $($node.LanguageCode) $OSBuildVersion" - #================================================= - # ObjectProperties - #================================================= - $records += [pscustomobject]@{ - Id = $Id - OperatingSystem = $OperatingSystem - OSName = $OSName - OSVersion = $OSVersion - OSArchitecture = $OSArchitecture - OSActivation = $OSActivation - OSLanguageCode = $node.LanguageCode - OSLanguage = $node.Language - OSBuild = $OSBuild - OSBuildVersion = $OSBuildVersion - # Architecture = $node.Architecture - Size = $node.Size - Sha1 = $node.Sha1 - Sha256 = $node.Sha256 - FileName = $node.FileName - FilePath = $node.FilePath - # IsRetailOnly = $node.IsRetailOnly - } - } - $records = $records | Sort-Object -Property FileName -Unique - $records = $records | Sort-Object -Property @{Expression = { $_.OperatingSystem }; Descending = $true }, OSArchitecture, OSActivation, OSLanguageCode - $records | Export-Clixml -Path (Join-Path -Path $env:TEMP -ChildPath 'OSDCloudCoreOperatingSystems.xml') -Force - return $records -} diff --git a/OSDCloud/private/core-operatingsystem/Get-OSDCoreOperatingSystems.ps1 b/OSDCloud/private/core-operatingsystem/Get-OSDCoreOperatingSystems.ps1 index c25e4d2..eb4607c 100644 --- a/OSDCloud/private/core-operatingsystem/Get-OSDCoreOperatingSystems.ps1 +++ b/OSDCloud/private/core-operatingsystem/Get-OSDCoreOperatingSystems.ps1 @@ -1,14 +1,15 @@ function Get-OSDCoreOperatingSystems { <# .SYNOPSIS - Gets parsed OSD core operating system catalog entries. + Gets parsed module-native operating system catalog entries. .DESCRIPTION Imports normalized raw catalog records from Initialize-ModuleCoreOperatingSystems and - transforms them into OSD core operating system objects used by selection and - deployment workflows. The function derives build identity, Windows family and - release, normalized architecture, activation channel, and compatibility flags, - then returns unique sorted records. + transforms them into operating system objects used by selection and deployment + workflows. OSD receives its existing public property schema, while OSDCloud receives + its native OS-prefixed property schema. The function derives build identity, Windows + family and release, normalized architecture, and activation channel before returning + unique sorted records. .EXAMPLE Get-OSDCoreOperatingSystems @@ -31,7 +32,7 @@ function Get-OSDCoreOperatingSystems { .OUTPUTS PSCustomObject[] - Parsed operating system records with OSD-specific properties. + Parsed operating system records with the current module's native properties. .LINK https://github.com/OSDeploy/OSD/tree/master/docs @@ -40,14 +41,21 @@ function Get-OSDCoreOperatingSystems { Author: David Segura - Recast Software 2026-07-22 - Initial help block created 2026-08-05 - Expanded help content and examples + 2026-09-30 - Merged OSD and OSDCloud native output projections #> [CmdletBinding()] [OutputType([pscustomobject[]])] param () + $Error.Clear() $ErrorActionPreference = 'Stop' $records = @() $mctRecords = @() + $moduleName = $MyInvocation.MyCommand.Module.Name + + if ($moduleName -notin @('OSD', 'OSDCloud')) { + throw "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Unsupported module context '$moduleName'." + } Initialize-ModuleCoreOperatingSystems if (-not ($global:ModuleCoreOperatingSystems)) { @@ -71,6 +79,7 @@ function Get-OSDCoreOperatingSystems { if (-not $operatingSystemInfo) { continue } + $OperatingSystem = $operatingSystemInfo.OperatingSystem $OSName = $operatingSystemInfo.OSName $OSVersion = $operatingSystemInfo.OSVersion #================================================= @@ -104,73 +113,64 @@ function Get-OSDCoreOperatingSystems { $OSActivation = 'Unknown' continue } - #================================================= - # Win10 / Win11 - #================================================= - if ($OSName -eq 'Windows 10') { - $Win10 = $true - $Win11 = $false - } - elseif ($OSName -eq 'Windows 11') { - $Win10 = $false - $Win11 = $true + if ($moduleName -eq 'OSD') { + $Win10 = $OSName -eq 'Windows 10' + $Win11 = $OSName -eq 'Windows 11' + $DisplayName = "$OSName $OSVersion $OSArchitecture $($node.LanguageCode) $OSActivation $OSBuildVersion" + + $records += [pscustomobject]@{ + Status = $null + ReleaseDate = $null + Name = $DisplayName + Version = $OSName + ReleaseID = $OSVersion + Architecture = $OSArchitecture + Language = $node.LanguageCode + Activation = $OSActivation + Build = $OSBuildVersion + FileName = $node.FileName + ImageIndex = $node.ImageIndex + ImageName = $node.ImageName + Url = $node.FilePath + SHA1 = $node.Sha1 + SHA256 = $node.Sha256 + UpdateID = $node.UpdateID + Win10 = $Win10 + Win11 = $Win11 + } } else { - $Win10 = $false - $Win11 = $false - } - #================================================= - # OSD Module Properties - #================================================= - # DisplayName should be in the format "Win11-25H2-amd64" - $DisplayName = "$OSName $OSVersion $OSArchitecture $($node.LanguageCode) $OSActivation $OSBuildVersion" - #================================================= - # ObjectProperties - #================================================= - <# - Status : - ReleaseDate : 2023-12-04 - Name : Windows 10 22H2 x64 ar-sa Retail 19045.3803 - Version : Windows 10 - ReleaseID : 22H2 - Architecture : x64 - Language : ar-sa - Activation : Retail - Build : 19045.3803 - FileName : 19045.3803.231204-0204.22h2_release_svc_refresh_CLIENTCONSUMER_RET_x64FRE_ar-sa.esd - ImageIndex : - ImageName : - Url : http://dl.delivery.mp.microsoft.com/filestreamingservice/files/39d366c6-bb66-4938-9a78-0670eda8304d/19045.3803.231204-0204.22h2_release_svc_refresh_CLIENTCONSUMER_RET_x64FRE_ar-sa.esd - SHA1 : 2119ef0efd432f98cdccdf525cd17fcceacef111 - UpdateID : - Win10 : True - Win11 : False - #> - - $records += [pscustomobject]@{ - Status = $null - ReleaseDate = $null - Name = $DisplayName - Version = $OSName - ReleaseID = $OSVersion - Architecture = $OSArchitecture - Language = $node.LanguageCode - Activation = $OSActivation - Build = $OSBuildVersion - FileName = $node.FileName - ImageIndex = $node.ImageIndex - ImageName = $node.ImageName - Url = $node.FilePath - SHA1 = $node.Sha1 - SHA256 = $node.Sha256 - UpdateID = $node.UpdateID - Win10 = $Win10 - Win11 = $Win11 + $Id = "$OperatingSystem $OSArchitecture $OSActivation $($node.LanguageCode) $OSBuildVersion" + + $records += [pscustomobject]@{ + Id = $Id + OperatingSystem = $OperatingSystem + OSName = $OSName + OSVersion = $OSVersion + OSArchitecture = $OSArchitecture + OSActivation = $OSActivation + OSLanguageCode = $node.LanguageCode + OSLanguage = $node.Language + OSBuild = [string]$node.OSBuild + OSBuildVersion = $OSBuildVersion + Size = $node.Size + Sha1 = $node.Sha1 + Sha256 = $node.Sha256 + FileName = $node.FileName + FilePath = $node.FilePath + } } } - $records = $records | Sort-Object -Property Url -Unique - $records = $records | Sort-Object -Property Name - $records | Export-Clixml -Path (Join-Path -Path $env:TEMP -ChildPath 'OSDCoreOperatingSystems.xml') -Force + if ($moduleName -eq 'OSD') { + $records = $records | Sort-Object -Property Url -Unique + $records = $records | Sort-Object -Property Name + $records | Export-Clixml -Path (Join-Path -Path $env:TEMP -ChildPath 'OSDCoreOperatingSystems.xml') -Force + } + else { + $records = $records | Sort-Object -Property FileName -Unique + $records = $records | Sort-Object -Property @{ Expression = { $_.OperatingSystem }; Descending = $true }, OSArchitecture, OSActivation, OSLanguageCode + $records | Export-Clixml -Path (Join-Path -Path $env:TEMP -ChildPath 'OSDCloudCoreOperatingSystems.xml') -Force + } return $records } diff --git a/OSDCloud/private/core-operatingsystem/Initialize-ModuleCoreOperatingSystems.ps1 b/OSDCloud/private/core-operatingsystem/Initialize-ModuleCoreOperatingSystems.ps1 index 4a17e2d..be05e4b 100644 --- a/OSDCloud/private/core-operatingsystem/Initialize-ModuleCoreOperatingSystems.ps1 +++ b/OSDCloud/private/core-operatingsystem/Initialize-ModuleCoreOperatingSystems.ps1 @@ -10,6 +10,7 @@ function Initialize-ModuleCoreOperatingSystems { is converted into a PowerShell object, excluded metadata properties are removed, and duplicate properties are normalized. Duplicate catalog rows are grouped by FilePath, FileName, LanguageCode, and Architecture, then the preferred row is selected by hash availability. + The bundled module catalog wins when an external catalog has the same build revision. .EXAMPLE Initialize-ModuleCoreOperatingSystems @@ -37,6 +38,7 @@ function Initialize-ModuleCoreOperatingSystems { 2026-07-22 - Initial help block created 2026-08-05 - Expanded help content and examples 2026-09-17 - Added external catalog discovery and latest build selection + 2026-09-30 - Preferred bundled catalogs when external revisions are equal #> [CmdletBinding()] [OutputType([pscustomobject[]])] @@ -166,7 +168,14 @@ function Initialize-ModuleCoreOperatingSystems { Sort-Object -Property BuildVersion -Descending | Select-Object -First 1 -ExpandProperty BuildVersion - $_.Group | Where-Object { $_.BuildVersion -eq $latestBuildVersion } + $latestCatalogs = @($_.Group | Where-Object { $_.BuildVersion -eq $latestBuildVersion }) + $bundledCatalogs = @($latestCatalogs | Where-Object { -not $_.External }) + if ($bundledCatalogs.Count -gt 0) { + $bundledCatalogs + } + else { + $latestCatalogs + } } | Sort-Object -Property MajorBuild, BuildVersion, @{ Expression = { $_.File.FullName } } diff --git a/OSDCloud/private/core-operatingsystem/Set-OSDCoreOperatingSystemCloudObject.ps1 b/OSDCloud/private/core-operatingsystem/Set-OSDCoreOperatingSystemCloudObject.ps1 index 1976105..13cc690 100644 --- a/OSDCloud/private/core-operatingsystem/Set-OSDCoreOperatingSystemCloudObject.ps1 +++ b/OSDCloud/private/core-operatingsystem/Set-OSDCoreOperatingSystemCloudObject.ps1 @@ -83,47 +83,6 @@ function Set-OSDCoreOperatingSystemCloudObject { [Parameter(Mandatory = $false)] [switch]$RefreshCatalog ) - <# OSD PowerShell Module - PS C:\Users\david> $OSDCoreOperatingSystemCloudObject - - Status : - ReleaseDate : - Name : Windows 11 25H2 amd64 en-us Retail 26200.8873 - Version : Windows 11 - ReleaseID : 25H2 - Architecture : amd64 - Language : en-us - Activation : Retail - Build : 26200.8873 - FileName : 26200.8873.260710-2020.25h2_ge_release_svc_refresh_CLIENTCONSUMER_RET_x64FRE_en-us.esd - ImageIndex : - ImageName : - Url : http://dl.delivery.mp.microsoft.com/filestreamingservice/files/0f68e999-6e25-4ae7-92db-23cbb3a723a9/26200.8873.260710-2020.25h2_ge_release_svc_refresh_CLIENTCONSUMER_RET_x64FRE_en-us.esd - SHA1 : - SHA256 : e4c251a99eeade29aa5d462047bcb257d640cd2c98dfb7a2305f45876d1790c1 - UpdateID : - Win10 : False - Win11 : True - #> - <# OSDCloud PowerShell Module - PS C:\Users\david> $OSDCoreOperatingSystemCloudObject - - Id : Windows 11 25H2 amd64 Retail en-us 26200.8873 - OperatingSystem : Windows 11 25H2 - OSName : Windows 11 - OSVersion : 25H2 - OSArchitecture : amd64 - OSActivation : Retail - OSLanguageCode : en-us - OSLanguage : English (United States) - OSBuild : 26200 - OSBuildVersion : 26200.8873 - Size : 6050059595 - Sha1 : - Sha256 : e4c251a99eeade29aa5d462047bcb257d640cd2c98dfb7a2305f45876d1790c1 - FileName : 26200.8873.260710-2020.25h2_ge_release_svc_refresh_CLIENTCONSUMER_RET_x64FRE_en-us.esd - FilePath : http://dl.delivery.mp.microsoft.com/filestreamingservice/files/0f68e999-6e25-4ae7-92db-23cbb3a723a9/26200.8873.260710-2020.25h2_ge_release_svc_refresh_CLIENTCONSUMER_RET_x64FRE_en-us.esd - #> $Error.Clear() $ModuleName = $($MyInvocation.MyCommand.Module.Name) @@ -138,28 +97,20 @@ function Set-OSDCoreOperatingSystemCloudObject { $normalizedLanguageCode = $OSLanguageCode.ToLowerInvariant() Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Normalized filters: Architecture='$normalizedArchitecture', Language='$normalizedLanguageCode'" - $catalogProvider = $null - $catalogShape = $null + $catalogProvider = 'Get-OSDCoreOperatingSystems' if ($ModuleName -eq 'OSD') { - $catalogProvider = 'Get-OSDCoreOperatingSystems' $catalogShape = 'OSD' } elseif ($ModuleName -eq 'OSDCloud') { - $catalogProvider = 'Get-OSDCloudCoreOperatingSystems' - $catalogShape = 'OSDCloud' - } - elseif (Get-Command -Name 'Get-OSDCloudCoreOperatingSystems' -ErrorAction Ignore) { - $catalogProvider = 'Get-OSDCloudCoreOperatingSystems' $catalogShape = 'OSDCloud' } - elseif (Get-Command -Name 'Get-OSDCoreOperatingSystems' -ErrorAction Ignore) { - $catalogProvider = 'Get-OSDCoreOperatingSystems' - $catalogShape = 'OSD' - } else { - throw "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Unable to load core operating systems provider command." + $catalogShape = $null + } + if (-not (Get-Command -Name $catalogProvider -ErrorAction Ignore)) { + throw "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Unable to load core operating systems provider command '$catalogProvider'." } - Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Using operating systems provider '$catalogProvider' for '$catalogShape' object shape" + Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Using operating systems provider '$catalogProvider'" $existingCatalogShape = $null if ($global:OSDCoreOperatingSystems) { @@ -206,8 +157,7 @@ function Set-OSDCoreOperatingSystemCloudObject { catch { [version]'0.0' } - }; Descending = $true - } | + }; Descending = $true } | Select-Object -First 1 if (-not $global:OSDCoreOperatingSystemCloudObject) { diff --git a/OSDCloud/private/core-operatingsystem/dev/Get-OSDCloudDefaultOS.ps1 b/OSDCloud/private/core-operatingsystem/dev/Get-OSDCloudDefaultOS.ps1 index 68e5641..2839d2d 100644 --- a/OSDCloud/private/core-operatingsystem/dev/Get-OSDCloudDefaultOS.ps1 +++ b/OSDCloud/private/core-operatingsystem/dev/Get-OSDCloudDefaultOS.ps1 @@ -4,7 +4,7 @@ function Get-OSDCloudDefaultOS { Gets the default OSDCloud operating system record for the current context. .DESCRIPTION - Retrieves operating system records from Get-OSDCloudCoreOperatingSystems and + Retrieves operating system records from Get-OSDCoreOperatingSystems and applies architecture and language preference filters to select a single default record. Language preference order is: 1) $global:OSDCLOUD_OSLANGUAGECODE @@ -71,7 +71,7 @@ function Get-OSDCloudDefaultOS { FilePath : http://dl.delivery.mp.microsoft.com/filestreamingservice/files/79a3f5e0-d04d-4689-a5d4-3ea35f8b189a/26200.7462.251207-0044.25h2_ge_release_svc_refresh_CLIENTCONSUMER_RET_x64FRE_en-gb.esd #> - $records = Get-OSDCloudCoreOperatingSystems + $records = Get-OSDCoreOperatingSystems #================================================= # Limit the results based on $env:PROCESSOR_ARCHITECTURE $ProcessorArchitecture = $env:PROCESSOR_ARCHITECTURE From 22a80d9d375b8a06d3d16f30384d1d1c3961d726 Mon Sep 17 00:00:00 2001 From: David Segura Date: Wed, 30 Sep 2026 10:23:19 -0500 Subject: [PATCH 4/6] refactor(os): simplify core operating systems retrieval logic --- OSDCloud/private/Initialize-DeployOSDCloud.ps1 | 17 +++-------------- .../private/Update-OSDCloudCoreCatalogOS.ps1 | 5 +++++ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/OSDCloud/private/Initialize-DeployOSDCloud.ps1 b/OSDCloud/private/Initialize-DeployOSDCloud.ps1 index ca416e7..8028dc9 100644 --- a/OSDCloud/private/Initialize-DeployOSDCloud.ps1 +++ b/OSDCloud/private/Initialize-DeployOSDCloud.ps1 @@ -134,20 +134,9 @@ function Initialize-DeployOSDCloud { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Ready: OSDCoreDriverPacks" #================================================= # CoreOperatingSystems - $ModuleName = $($MyInvocation.MyCommand.Module.Name) - if ($ModuleName -eq 'OSD') { - $global:OSDCoreOperatingSystems = Get-OSDCoreOperatingSystems | - Where-Object { $_.Architecture -match $processorArchitecture } - Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Ready: OSDCoreOperatingSystems" - } - elseif ($ModuleName -eq 'OSDCloud') { - $global:OSDCoreOperatingSystems = Get-OSDCloudCoreOperatingSystems | - Where-Object { $_.OSArchitecture -match $processorArchitecture } - Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Ready: OSDCoreOperatingSystems" - } - else { - throw "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Unable to load core operating systems provider command." - } + $global:OSDCoreOperatingSystems = Get-OSDCoreOperatingSystems | + Where-Object { ($_.Architecture -match $processorArchitecture) -or ($_.OSArchitecture -match $processorArchitecture) } + Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Ready: OSDCoreOperatingSystems" #================================================= # OSDCloudDeploy $global:OSDCloudDeploy = $null diff --git a/OSDCloud/private/Update-OSDCloudCoreCatalogOS.ps1 b/OSDCloud/private/Update-OSDCloudCoreCatalogOS.ps1 index 71c3ecc..75686fe 100644 --- a/OSDCloud/private/Update-OSDCloudCoreCatalogOS.ps1 +++ b/OSDCloud/private/Update-OSDCloudCoreCatalogOS.ps1 @@ -44,7 +44,12 @@ function Update-OSDCloudCoreCatalogOS { System.Management.Automation.PSCustomObject. Returns the detected build, module catalog path, publication status, synchronized drive catalog paths, item count, and SHA256 hash. + .LINK + https://github.com/OSDeploy/OSD/tree/master/docs + .NOTES + Author: David Segura - Recast Software + 2026-09-30 - Added validated operating-system catalog publication Requires Windows, Windows PowerShell 5.1 or later, internet access, expand.exe, writable temporary storage, and write access to the module catalog directory when updating. #> From 483d7152ca3dc1d81fba84a89c23b2ccba6d168a Mon Sep 17 00:00:00 2001 From: David Segura Date: Wed, 30 Sep 2026 10:27:06 -0500 Subject: [PATCH 5/6] chore(manifest): update module version and improve description formatting --- OSDCloud/OSDCloud.psd1 | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/OSDCloud/OSDCloud.psd1 b/OSDCloud/OSDCloud.psd1 index 04a857f..b1d04fe 100644 --- a/OSDCloud/OSDCloud.psd1 +++ b/OSDCloud/OSDCloud.psd1 @@ -2,25 +2,24 @@ # Module manifest for module 'OSDCloud' # # Generated by: David Segura -# Generated on: 2026-04-22 # @{ # --- Identity --- - RootModule = 'OSDCloud.psm1' - ModuleVersion = '26.9.29.1' - CompatiblePSEditions = @('Core','Desktop') - GUID = '2fbd5c65-79c7-4561-9a2e-c4a4eebc89c7' + RootModule = 'OSDCloud.psm1' + ModuleVersion = '26.9.30.1' + CompatiblePSEditions = @('Core', 'Desktop') + GUID = '2fbd5c65-79c7-4561-9a2e-c4a4eebc89c7' # --- Authoring --- - Author = 'David Segura' - CompanyName = 'Recast Software' - Copyright = '(c) 2026 Recast Software. All rights reserved.' + Author = 'David Segura' + CompanyName = 'Recast Software' + Copyright = '(c) 2026 Recast Software. All rights reserved.' Description = @' -Recast OSDCloud PowerShell Module -Windows 11 26H2 Release +Recast OSDCloud PowerShell Module. PowerShell module for OSDCloud v2 deployment automation and WinPEStartup from recastsoftware.com. Includes operating system, driver pack, and device provisioning workflows. +Windows 11 26H2 GA Release '@ # --- Requirements --- From 0e21de2c94d4bea213ff96ac05617cbd9b4bfe45 Mon Sep 17 00:00:00 2001 From: David Segura Date: Wed, 30 Sep 2026 10:31:00 -0500 Subject: [PATCH 6/6] docs(release): document version 26.9.30.1 --- CHANGELOG.md | 22 ++++++++++++++++++++++ README.md | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0e9ee0..6b0346c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ All notable changes to this project will be documented in this file. +## 26.9.30.1 - September 30, 2026 + +### Added + +- Added a manifest-driven synchronization check for canonical operating-system functions and catalog XML files shared with the OSD module. +- Added a read-only GitHub Actions workflow that detects operating-system parity drift against `OSDeploy/OSD`. + +### Changed + +- Module version bumped to `26.9.30.1` and the module description updated to identify the Windows 11 26H2 GA release. +- Made bundled operating-system catalogs win when an external catalog has the same build revision, while preserving higher-revision external overlays. +- Merged the OSD and OSDCloud catalog projections into `Get-OSDCoreOperatingSystems`, retaining each module's native property schema. +- Updated deployment initialization, operating-system selection, default OS resolution, catalog refresh, maintenance guidance, and function help to use the unified provider and synchronized catalog model. + +### Fixed + +- Made HP driver pack catalog release-date parsing culture-independent. + +### Removed + +- Removed the redundant private `Get-OSDCloudCoreOperatingSystems` provider after migrating its callers to the unified function. + ## 26.9.29.1 - September 29, 2026 ### Added diff --git a/README.md b/README.md index 33f941e..76a29b0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ OSDCloud is a PowerShell module for deploying Windows with cloud-hosted operatin ## Overview -- Current repository module version: `26.9.29.1`. +- Current repository module version: `26.9.30.1`. - Defaults to Windows 11 26H2 for amd64 and arm64 deployments. - Focused on Windows deployment workflows driven by PowerShell. - Supports WinPEStartup helpers and deployment UX options.