<# .SYNOPSIS Builds a software update group from the most-needed updates and deploys it to a collection. .DESCRIPTION Finds updates that are required on at least one client, aren't superseded or expired, and match your title filters. Takes the top N by number of clients missing them, puts them in a new software update group, downloads them into a deployment package, and creates a deployment. Supports -WhatIf, which shows the update list without creating anything. .PARAMETER SiteCode Your three-character site code, for example ABC. .PARAMETER CollectionName The device collection to deploy to. Start with a pilot collection. .PARAMETER DeploymentPackageName An existing software update deployment package to download the content into. .PARAMETER TitleLike Wildcard patterns an update title must match (any one of them). Default: cumulative and security updates. .PARAMETER Top How many updates to include, most-missing first. Default: 10. .PARAMETER DeadlineDays Days from now until the deadline. Default: 7. .PARAMETER Available Deploy as Available instead of Required. .EXAMPLE .\New-CMTopUpdateDeployment.ps1 -SiteCode ABC -CollectionName 'Pilot - Workstations' -DeploymentPackageName 'Windows Updates 2026' -WhatIf #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)][ValidatePattern('^[A-Za-z0-9]{3}$')][string]$SiteCode, [string]$ProviderMachineName = $env:COMPUTERNAME, [Parameter(Mandatory)][string]$CollectionName, [Parameter(Mandatory)][string]$DeploymentPackageName, [string[]]$TitleLike = @('*Cumulative Update*', '*Security Update*'), [ValidateRange(1, 1000)][int]$Top = 10, [ValidateRange(0, 90)][int]$DeadlineDays = 7, [string]$GroupName = ('{0:yyyy-MM-dd} Top Missing Updates' -f (Get-Date)), [switch]$Available ) $ErrorActionPreference = 'Stop' if (-not (Get-Module ConfigurationManager)) { if (-not $env:SMS_ADMIN_UI_PATH) { throw 'The Configuration Manager console is not installed on this machine.' } Import-Module (Join-Path $env:SMS_ADMIN_UI_PATH '..\ConfigurationManager.psd1') } if (-not (Get-PSDrive -Name $SiteCode -PSProvider CMSite -ErrorAction SilentlyContinue)) { New-PSDrive -Name $SiteCode -PSProvider CMSite -Root $ProviderMachineName | Out-Null } Push-Location "$($SiteCode):\" try { # Fail early on typos, before we build anything. if (-not (Get-CMDeviceCollection -Name $CollectionName)) { throw "Collection '$CollectionName' not found." } if (-not (Get-CMSoftwareUpdateDeploymentPackage -Name $DeploymentPackageName)) { throw "Deployment package '$DeploymentPackageName' not found." } Write-Verbose 'Querying software updates. On a big site this takes a minute.' $candidates = Get-CMSoftwareUpdate -Fast -IsSuperseded $false -IsExpired $false | Where-Object { $update = $_ $update.NumMissing -gt 0 -and ($TitleLike | Where-Object { $update.LocalizedDisplayName -like $_ }) } $selected = @($candidates | Sort-Object NumMissing -Descending | Select-Object -First $Top) if ($selected.Count -eq 0) { Write-Warning 'No required updates matched your filters. Nothing to do.' return } # Show the list either way, so -WhatIf is actually useful. $report = foreach ($u in $selected) { [pscustomobject]@{ ArticleID = "KB$($u.ArticleID)" Title = $u.LocalizedDisplayName NumMissing = $u.NumMissing CI_ID = $u.CI_ID InGroup = $false } } if (-not $PSCmdlet.ShouldProcess("$($selected.Count) updates -> '$CollectionName'", "Create group '$GroupName' and deploy")) { return $report } if (Get-CMSoftwareUpdateGroup -Name $GroupName) { throw "Update group '$GroupName' already exists. Use -GroupName to pick another." } New-CMSoftwareUpdateGroup -Name $GroupName -Description "Top $Top missing updates, created by script" | Out-Null foreach ($row in $report) { try { Add-CMSoftwareUpdateToGroup -SoftwareUpdateGroupName $GroupName -SoftwareUpdateId $row.CI_ID $row.InGroup = $true Write-Verbose "Added $($row.ArticleID)" } catch { Write-Warning "Couldn't add $($row.ArticleID): $($_.Exception.Message)" } } if (-not ($report | Where-Object InGroup)) { throw "No updates could be added to '$GroupName'. Nothing was deployed." } Write-Verbose "Downloading content into '$DeploymentPackageName'" Save-CMSoftwareUpdate -SoftwareUpdateGroupName $GroupName -DeploymentPackageName $DeploymentPackageName $now = Get-Date $deployArgs = @{ SoftwareUpdateGroupName = $GroupName CollectionName = $CollectionName DeploymentName = "$GroupName - $CollectionName" DeploymentType = if ($Available) { 'Available' } else { 'Required' } AvailableDateTime = $now TimeBasedOn = 'LocalTime' UserNotification = 'DisplaySoftwareCenterOnly' } if (-not $Available) { $deployArgs.DeadlineDateTime = $now.AddDays($DeadlineDays) } New-CMSoftwareUpdateDeployment @deployArgs | Out-Null $report } finally { Pop-Location }