From 740e3e0a54b622effc0a21f17bffed976ab49798 Mon Sep 17 00:00:00 2001 From: Pacific Northwest Computers <142192730+Pnwcomputers@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:23:25 -0700 Subject: [PATCH 1/7] Update DeployWorkstation.ps1 --- DeployWorkstation.ps1 | 306 ++++++++++++++++++++++++++++++++---------- 1 file changed, 232 insertions(+), 74 deletions(-) diff --git a/DeployWorkstation.ps1 b/DeployWorkstation.ps1 index 9880908..cda497a 100644 --- a/DeployWorkstation.ps1 +++ b/DeployWorkstation.ps1 @@ -1,10 +1,17 @@ # DeployWorkstation.ps1 – Optimized Win10/11 Setup & Clean-up -# Version: 5.2 – PNWC Edition 6.22.2026 +# Version: 5.3 – PNWC Edition 6.22.2026 # New in 5.0: Write-Progress console bars, embedded en-US / es-ES localization # New in 5.1: Winget auto-bootstrap, install retry logic, WU guard, OEM OneDrive, edition awareness # New in 5.11: App update mode (-UpdateApps), startup banner # New in 5.2: Updated .NET runtimes (8 LTS + 10 LTS); bug fixes in QuickStart.cmd, # winget version detection, CI workflow; code quality improvements +# New in 5.3: Package ID auto-resolution (Resolve-WingetPackageId with per-app +# fallback IDs + winget search suggestions); fixed .NET Framework ID +# (Microsoft.DotNet.Framework.4.8 -> Microsoft.DotNet.Framework.Runtime); +# corrected winget exit-code handling (reboot codes 0x8A150109/10A/10B +# were misclassified as network errors and retried); reboot-required +# installs now count as success + end-of-run restart notice; +# --exact and --disable-interactivity on all winget install/upgrade calls #Requires -Version 5.1 #Requires -RunAsAdministrator @@ -53,7 +60,7 @@ if ($PSVersionTable.PSEdition -eq 'Core') { # ── Startup Banner ─────────────────────────────────────────────────────────── # Pure-ASCII art — no UTF-8 box-drawing chars. Safe on PS 5.1 regardless of # console codepage (some hosts decode as cp1252, producing mojibake with box-drawing). -try { $host.UI.RawUI.WindowTitle = "DeployWorkstation v5.2" } catch {} +try { $host.UI.RawUI.WindowTitle = "DeployWorkstation v5.3" } catch {} Clear-Host Write-Host "" Write-Host " ###### ## ## ## ## ######" -ForegroundColor Cyan @@ -66,7 +73,7 @@ Write-Host " Pacific Northwest Computers" -ForegroundColor White Write-Host " Windows Workstation Deployment Toolkit" -ForegroundColor DarkGray Write-Host "" Write-Host ("=" * 70) -ForegroundColor DarkCyan -Write-Host " DeployWorkstation v5.2 -- Automated Win10/11 Setup & Hardening " -ForegroundColor Cyan +Write-Host " DeployWorkstation v5.3 -- Automated Win10/11 Setup & Hardening " -ForegroundColor Cyan Write-Host " Pacific Northwest Computers | jon@pnwcomputers.com " -ForegroundColor Gray Write-Host " Bloatware Removal / App Install / System Configuration " -ForegroundColor DarkGray Write-Host ("=" * 70) -ForegroundColor DarkCyan @@ -87,7 +94,7 @@ $script:Strings = @{ 'en-US' = @{ # Startup - Started = 'DeployWorkstation v5.2 Started' + Started = 'DeployWorkstation v5.3 Started' WingetRequired = "Winget is required. Install 'App Installer' from the Microsoft Store." WingetFound = 'Winget found' WingetMissing = 'Winget not found on PATH.' @@ -172,7 +179,13 @@ $script:Strings = @{ WingetDownload = 'Downloading App Installer from Microsoft' # Reliability - InstallRetrying = 'Network error, retrying' + InstallRetrying = 'Transient error, retrying' + RebootToFinish = 'Installed - restart required to finish' + NotInstalledSkip = 'Not installed on this machine, skipping' + IdNotFound = 'Package ID not found in winget source' + IdChanged = 'Package ID changed, using' + IdCandidates = 'Possible replacement IDs for' + SumReboot = '*** One or more installs require a RESTART to finish ***' CapWuUnavail = 'Skipped - Windows Update not accessible on this system' HomeEditionNote = 'Policy key written but has no effect on Windows Home edition' OneDriveOem = 'OneDrive OEM binary removal' @@ -236,7 +249,7 @@ $script:Strings = @{ 'es-ES' = @{ # Startup - Started = 'DeployWorkstation v5.2 Iniciado' + Started = 'DeployWorkstation v5.3 Iniciado' WingetRequired = "Se requiere Winget. Instale 'App Installer' desde Microsoft Store." WingetFound = 'Winget encontrado' WingetMissing = 'Winget no encontrado en el PATH.' @@ -321,7 +334,13 @@ $script:Strings = @{ WingetDownload = 'Descargando App Installer de Microsoft' # Reliability - InstallRetrying = 'Error de red, reintentando' + InstallRetrying = 'Error transitorio, reintentando' + RebootToFinish = 'Instalado - se requiere reinicio para finalizar' + NotInstalledSkip = 'No instalado en este equipo, omitiendo' + IdNotFound = 'ID de paquete no encontrado en la fuente winget' + IdChanged = 'ID de paquete cambiado, usando' + IdCandidates = 'Posibles IDs de reemplazo para' + SumReboot = '*** Una o mas instalaciones requieren REINICIO para finalizar ***' CapWuUnavail = 'Omitido - Windows Update no accesible en este sistema' HomeEditionNote = 'Clave de politica escrita pero sin efecto en Windows Home' OneDriveOem = 'Eliminacion de OneDrive OEM' @@ -481,8 +500,10 @@ function Clear-PhaseProgress { $script:Summary = @{ AppsInstalled = 0 AppsFailed = 0 + AppsSkipped = 0 AppsUpdated = 0 AppsUpdateFailed = 0 + RebootNeeded = $false AppxRemoved = 0 CapabilitiesRemoved = 0 McAfeeRemoved = 0 @@ -493,21 +514,33 @@ $script:Summary = @{ $script:Results = [System.Collections.Generic.List[hashtable]]::new() # Applications managed by DeployWorkstation — used by both Install-StandardApps and Update-InstalledApps +# Id : primary winget package ID (verified against the winget source at run time) +# Name : display name (also used as the winget search term if all IDs fail to resolve) +# Fallbacks : optional list of alternate IDs tried in order when the primary ID +# no longer exists in the winget source (IDs get renamed/retired over time) $script:ManagedApps = @( # ---- Security & Maintenance ---- @{ Id = 'Malwarebytes.Malwarebytes'; Name = 'Malwarebytes' }, @{ Id = 'BleachBit.BleachBit'; Name = 'BleachBit' }, # ---- Browsers & Productivity ---- - @{ Id = 'Google.Chrome'; Name = 'Google Chrome' }, + @{ Id = 'Google.Chrome'; Name = 'Google Chrome' + Fallbacks = @('Google.Chrome.EXE') }, @{ Id = 'Adobe.Acrobat.Reader.64-bit'; Name = 'Adobe Acrobat Reader (64-bit)' }, @{ Id = '7zip.7zip'; Name = '7-Zip' }, @{ Id = 'VideoLAN.VLC'; Name = 'VLC Media Player' }, # ---- .NET Runtimes ---- - @{ Id = 'Microsoft.DotNet.Framework.4.8'; Name = '.NET Framework 4.8' }, - @{ Id = 'Microsoft.DotNet.DesktopRuntime.8'; Name = '.NET 8 Desktop Runtime' }, - @{ Id = 'Microsoft.DotNet.DesktopRuntime.10'; Name = '.NET 10 Desktop Runtime' }, + # NOTE: 'Microsoft.DotNet.Framework.4.8' was never a valid winget ID (it always + # returns 0x8A150014 NO_APPLICATIONS_FOUND). The runtime package is + # 'Microsoft.DotNet.Framework.Runtime' (currently 4.8.1). .NET Fx 4.8 also ships + # in-box on Win10 1903+ and all Win11, so on modern builds this resolves to a no-op. + @{ Id = 'Microsoft.DotNet.Framework.Runtime'; Name = '.NET Framework Runtime' + Fallbacks = @('Microsoft.DotNet.Framework.DeveloperPack_4') }, + @{ Id = 'Microsoft.DotNet.DesktopRuntime.8'; Name = '.NET 8 Desktop Runtime' + Fallbacks = @('Microsoft.DotNet.DesktopRuntime.8.x64') }, + @{ Id = 'Microsoft.DotNet.DesktopRuntime.10'; Name = '.NET 10 Desktop Runtime' + Fallbacks = @('Microsoft.DotNet.DesktopRuntime.10.x64') }, # ---- Visual C++ Redistributables ---- @{ Id = 'Microsoft.VCRedist.2015+.x64'; Name = 'VC++ 2015-2022 Redist (x64)' }, @@ -572,8 +605,9 @@ function Set-RegistryValue { # ================================ function Install-WingetIfNeeded { - # Minimum usable winget version (supports --source winget, --accept-source-agreements) - $minVersion = [Version]'1.2.0' + # Minimum usable winget version + # (1.4+ required for --disable-interactivity and --include-unknown) + $minVersion = [Version]'1.4.0' Set-PhaseProgress -Activity (T 'ProgWingetCheck') -Status (T 'Checking') -Current 1 -Total 3 @@ -681,6 +715,126 @@ function Initialize-WingetSources { } } +# ================================ +# Winget Exit Codes & Package ID Resolution +# ================================ + +# Codes that mean "package is effectively present" — count as success +$script:WingetOkCodes = @( + -1978335189, # 0x8A15002B UPDATE_NOT_APPLICABLE (already installed / no applicable upgrade) + -1978334963, # 0x8A15010D INSTALL_ALREADY_INSTALLED (another version already installed) + -1978334962 # 0x8A15010E INSTALL_DOWNGRADE (a HIGHER version is already installed) +) + +# Success, but Windows needs a restart to finish — count as success + flag reboot +$script:WingetRebootOkCodes = @( + -1978334967, # 0x8A150109 INSTALL_REBOOT_REQUIRED_TO_FINISH + -1978334965 # 0x8A15010B INSTALL_REBOOT_INITIATED +) + +# Genuinely transient — worth retrying after a delay +# (NOTE: 0x8A150109/0x8A15010A are REBOOT codes, not network codes — pre-5.3 +# versions retried them as "network errors", masking the real condition) +$script:WingetTransientCodes = @( + -1978334969, # 0x8A150107 INSTALL_NO_NETWORK + -1978335215, # 0x8A150011 INSTALLER_HASH_MISMATCH (often a stale manifest/CDN race — refresh + retry once) + -2147012894, # 0x80072EE2 WinHTTP timeout + -2147012887, # 0x80072EE9 connection reset by peer + -2147012873, # 0x80072EF7 DNS name not resolved + -2147012867, # 0x80072EFD connection refused + -2147012889 # 0x80072EE7 InternetOpenUrl failed / WinHTTP unknown error +) + +# Package ID missing from source — triggers ID re-resolution, never a retry +$script:WingetNotFoundCode = -1978335212 # 0x8A150014 NO_APPLICATIONS_FOUND + +function Get-WingetFailReason { + param([int]$ExitCode) + switch ($ExitCode) { + -1978335215 { 'Installer hash mismatch — manifest may be stale; check proxy/AV' } # 0x8A150011 + -1978335212 { 'Package not found in winget source (ID may have changed)' } # 0x8A150014 + -1978334961 { 'Installer blocked by security policy' } # 0x8A15010F + -1978334960 { 'A dependency failed to install' } # 0x8A150110 + -1978334959 { 'Application is in use — close it and retry' } # 0x8A150111 + -1978334966 { 'Windows must be restarted before this app can install' } # 0x8A15010A + -1978334971 { 'Disk full' } # 0x8A150105 + -1978334969 { 'Network failure — no connectivity (all retries exhausted)' } # 0x8A150107 + -1978334955 { 'Installer returned a custom error — see winget log' } # 0x8A150115 + -2147012894 { 'Network failure — timed out (all retries exhausted)' } # 0x80072EE2 + -2147012887 { 'Network failure — connection reset (all retries exhausted)' } # 0x80072EE9 + -2147012873 { 'Network failure — DNS not resolved (all retries exhausted)' } # 0x80072EF7 + -2147012867 { 'Network failure — connection refused (all retries exhausted)' } # 0x80072EFD + -2147012889 { 'Network failure — WinHTTP error (all retries exhausted)' } # 0x80072EE7 + default { "Exit code $ExitCode (0x{0:X8})" -f ($ExitCode -band 0xFFFFFFFFL) } + } +} + +# Cache so install + update runs don't re-resolve the same app twice +$script:ResolvedIds = @{} + +function Test-WingetIdExists { + param([string]$Id) + $null = winget show --id $Id --exact --source winget --accept-source-agreements 2>&1 + return ($LASTEXITCODE -eq 0) +} + +function Resolve-WingetPackageId { + <# + Returns a usable winget package ID for an app entry, or $null. + Order: cached result -> primary Id -> each Fallback Id -> winget search by Name. + A search hit is auto-selected only when it is unambiguous (exactly one + candidate ID); otherwise candidates are logged for manual list maintenance. + #> + param([hashtable]$App) + + if ($script:ResolvedIds.ContainsKey($App.Id)) { return $script:ResolvedIds[$App.Id] } + + # 1) Primary ID + if (Test-WingetIdExists -Id $App.Id) { + $script:ResolvedIds[$App.Id] = $App.Id + return $App.Id + } + Write-Log "$(T 'IdNotFound'): $($App.Id)" -Level 'WARN' + + # 2) Known fallback IDs + foreach ($alt in @($App.Fallbacks)) { + if (-not $alt) { continue } + if (Test-WingetIdExists -Id $alt) { + Write-Log "$(T 'IdChanged'): $($App.Id) -> $alt" -Level 'WARN' + $script:ResolvedIds[$App.Id] = $alt + return $alt + } + } + + # 3) Last resort: search the winget source by display name and parse candidate IDs. + # winget's table output is locale/width dependent, so instead of column slicing + # we harvest tokens that look like package IDs (publisher.product[.suffix]). + try { + $searchOut = winget search "$($App.Name)" --source winget --accept-source-agreements 2>&1 + $candidates = @( + $searchOut | + ForEach-Object { [regex]::Matches("$_", '(?<=\s|^)([A-Za-z0-9+][\w+.-]*\.[\w+.-]*[A-Za-z0-9+])(?=\s|$)') } | + ForEach-Object { $_.Groups[1].Value } | + Where-Object { $_ -notmatch '^\d+(\.\d+)+$' } | # drop bare version numbers + Select-Object -Unique + ) + } catch { $candidates = @() } + + if ($candidates.Count -eq 1) { + # Unambiguous — verify and adopt it + if (Test-WingetIdExists -Id $candidates[0]) { + Write-Log "$(T 'IdChanged'): $($App.Id) -> $($candidates[0]) (via winget search)" -Level 'WARN' + $script:ResolvedIds[$App.Id] = $candidates[0] + return $candidates[0] + } + } elseif ($candidates.Count -gt 1) { + Write-Log "$(T 'IdCandidates') '$($App.Name)': $($candidates -join ', ')" -Level 'WARN' + } + + $script:ResolvedIds[$App.Id] = $null + return $null +} + # ================================ # Bloatware Removal # ================================ @@ -927,18 +1081,6 @@ function Remove-McAfeeProducts { function Install-StandardApps { Write-Log "--- $(T 'ProgApps') ---" -Level 'SECTION' - $alreadyInstalledCode = -1978335189 # winget 0x8A15002B - - # Winget exit codes that indicate a transient network problem — worth retrying - $networkErrorCodes = @( - -1978334967, # 0x8A150109 winget download failed - -1978334966, # 0x8A15010A winget network timeout - -2147012887, # 0x80072EE9 connection reset by peer - -2147012873, # 0x80072EF7 DNS name not resolved - -2147012867, # 0x80072EFD connection refused - -2147012889 # 0x80072EE7 InternetOpenUrl failed / WinHTTP unknown error - ) - $maxRetries = 2 $retryDelaySec = 10 @@ -955,6 +1097,15 @@ function Install-StandardApps { Write-Log "$(T 'Installing'): $($app.Name) [$($app.Id)]" try { + # Verify the package ID still exists; fall back / search if it changed + $resolvedId = Resolve-WingetPackageId -App $app + if (-not $resolvedId) { + Write-Log "$(T 'InstallFail'): $($app.Name) - $(T 'IdNotFound')" -Level 'WARN' + Add-Result -Section (T 'PhaseApps') -Item $app.Name -Status 'WARN' -Detail (T 'IdNotFound') + $script:Summary.AppsFailed++ + continue + } + $attempt = 0 $exitCode = -1 $wingetOut = $null @@ -962,15 +1113,19 @@ function Install-StandardApps { do { $attempt++ # Capture output rather than discarding it — logged on failure - $wingetOut = winget install --id $app.Id --source winget ` + $wingetOut = winget install --id $resolvedId --exact --source winget ` --accept-package-agreements --accept-source-agreements ` - --silent 2>&1 + --silent --disable-interactivity 2>&1 $exitCode = $LASTEXITCODE - if ($exitCode -eq 0 -or $exitCode -eq $alreadyInstalledCode) { break } + if ($exitCode -eq 0 -or + $exitCode -in $script:WingetOkCodes -or + $exitCode -in $script:WingetRebootOkCodes) { break } - if ($attempt -le $maxRetries -and $exitCode -in $networkErrorCodes) { + if ($attempt -le $maxRetries -and $exitCode -in $script:WingetTransientCodes) { Write-Log "$(T 'InstallRetrying') ($attempt/$maxRetries): $($app.Name) [exit $exitCode]" -Level 'WARN' + # Hash mismatches are often a stale local manifest cache — refresh before retrying + if ($exitCode -eq -1978335215) { winget source update --name winget 2>$null | Out-Null } Start-Sleep -Seconds $retryDelaySec } else { break @@ -981,27 +1136,19 @@ function Install-StandardApps { Write-Log "$(T 'InstallOK'): $($app.Name)" -Level 'SUCCESS' Add-Result -Section (T 'PhaseApps') -Item $app.Name -Status 'OK' -Detail (T 'InstallOK') $script:Summary.AppsInstalled++ - } elseif ($exitCode -eq $alreadyInstalledCode) { + } elseif ($exitCode -in $script:WingetRebootOkCodes) { + # Install succeeded but Windows needs a restart to finish — NOT a failure + Write-Log "$(T 'InstallOK'): $($app.Name) - $(T 'RebootToFinish')" -Level 'SUCCESS' + Add-Result -Section (T 'PhaseApps') -Item $app.Name -Status 'OK' -Detail (T 'RebootToFinish') + $script:Summary.AppsInstalled++ + $script:Summary.RebootNeeded = $true + } elseif ($exitCode -in $script:WingetOkCodes) { Write-Log "$(T 'AlreadyInstalled'): $($app.Name)" -Level 'SUCCESS' Add-Result -Section (T 'PhaseApps') -Item $app.Name -Status 'OK' -Detail (T 'AlreadyInstalled') $script:Summary.AppsInstalled++ } else { - # Map exit code to a human-readable reason. - # Using switch($int) avoids hashtable string/int key-type ambiguity. - # Network codes here cover the "all retries exhausted" path. - $failReason = switch ($exitCode) { - -1978335215 { 'Installer hash mismatch — retry later or check proxy/AV' } # 0x8A150011 - -1978335212 { 'Package not found in winget source (ID may have changed)' } # 0x8A15002C - -1978334960 { 'Installer blocked by security policy' } # 0x8A150110 - -1978335132 { 'Installer requires reboot before continuing' } # 0x8A150064 - -1978334967 { 'Network failure — download failed (all retries exhausted)' } # 0x8A150109 - -1978334966 { 'Network failure — timed out (all retries exhausted)' } # 0x8A15010A - -2147012887 { 'Network failure — connection reset (all retries exhausted)' } # 0x80072EE9 - -2147012873 { 'Network failure — DNS not resolved (all retries exhausted)' } # 0x80072EF7 - -2147012867 { 'Network failure — connection refused (all retries exhausted)' } # 0x80072EFD - -2147012889 { 'Network failure — WinHTTP error (all retries exhausted)' } # 0x80072EE7 - default { "Exit code $exitCode" } - } + $failReason = Get-WingetFailReason -ExitCode $exitCode + if ($exitCode -eq -1978334966) { $script:Summary.RebootNeeded = $true } # reboot-before-install Write-Log "$(T 'InstallFail'): $($app.Name) - $failReason" -Level 'WARN' # Log last clean lines of winget output — strip progress-bar/spinner noise $diagLines = ($wingetOut | Where-Object { "$_".Trim() }) | Select-Object -Last 8 @@ -1032,16 +1179,9 @@ function Install-StandardApps { # ================================ function Update-InstalledApps { - $alreadyUpToDateCode = -1978335189 # 0x8A15002B No applicable upgrade found - - $networkErrorCodes = @( - -1978334967, # 0x8A150109 download failed - -1978334966, # 0x8A15010A winget network timeout - -2147012887, # 0x80072EE9 connection reset by peer - -2147012873, # 0x80072EF7 DNS name not resolved - -2147012867, # 0x80072EFD connection refused - -2147012889 # 0x80072EE7 InternetOpenUrl failed / WinHTTP unknown error - ) + # 0x8A150014 NO_APPLICATIONS_FOUND from `winget upgrade --id` means the app + # isn't installed on this machine — a skip, not an error, in update mode + $notInstalledCode = $script:WingetNotFoundCode $maxRetries = 2 $retryDelaySec = 10 @@ -1058,21 +1198,35 @@ function Update-InstalledApps { Write-Log "$(T 'Updating'): $($app.Name) [$($app.Id)]" try { + $resolvedId = Resolve-WingetPackageId -App $app + if (-not $resolvedId) { + Write-Log "$(T 'UpdateFail'): $($app.Name) - $(T 'IdNotFound')" -Level 'WARN' + Add-Result -Section (T 'PhaseUpdate') -Item $app.Name -Status 'WARN' -Detail (T 'IdNotFound') + $script:Summary.AppsUpdateFailed++ + continue + } + $attempt = 0 $exitCode = -1 $wingetOut = $null do { $attempt++ - $wingetOut = winget upgrade --id $app.Id --source winget ` + # --include-unknown: upgrade apps whose installed version winget can't read + # (common after Windows Update services .NET runtimes out from under winget) + $wingetOut = winget upgrade --id $resolvedId --exact --source winget ` --accept-package-agreements --accept-source-agreements ` - --silent 2>&1 + --include-unknown --silent --disable-interactivity 2>&1 $exitCode = $LASTEXITCODE - if ($exitCode -eq 0 -or $exitCode -eq $alreadyUpToDateCode) { break } + if ($exitCode -eq 0 -or + $exitCode -eq $notInstalledCode -or + $exitCode -in $script:WingetOkCodes -or + $exitCode -in $script:WingetRebootOkCodes) { break } - if ($attempt -le $maxRetries -and $exitCode -in $networkErrorCodes) { + if ($attempt -le $maxRetries -and $exitCode -in $script:WingetTransientCodes) { Write-Log "$(T 'InstallRetrying') ($attempt/$maxRetries): $($app.Name) [exit $exitCode]" -Level 'WARN' + if ($exitCode -eq -1978335215) { winget source update --name winget 2>$null | Out-Null } Start-Sleep -Seconds $retryDelaySec } else { break @@ -1083,24 +1237,22 @@ function Update-InstalledApps { Write-Log "$(T 'UpdateOK'): $($app.Name)" -Level 'SUCCESS' Add-Result -Section (T 'PhaseUpdate') -Item $app.Name -Status 'OK' -Detail (T 'UpdateOK') $script:Summary.AppsUpdated++ - } elseif ($exitCode -eq $alreadyUpToDateCode) { + } elseif ($exitCode -in $script:WingetRebootOkCodes) { + Write-Log "$(T 'UpdateOK'): $($app.Name) - $(T 'RebootToFinish')" -Level 'SUCCESS' + Add-Result -Section (T 'PhaseUpdate') -Item $app.Name -Status 'OK' -Detail (T 'RebootToFinish') + $script:Summary.AppsUpdated++ + $script:Summary.RebootNeeded = $true + } elseif ($exitCode -eq $notInstalledCode) { + Write-Log "$(T 'NotInstalledSkip'): $($app.Name)" -Level 'INFO' + Add-Result -Section (T 'PhaseUpdate') -Item $app.Name -Status 'SKIPPED' -Detail (T 'NotInstalledSkip') + $script:Summary.AppsSkipped++ + } elseif ($exitCode -in $script:WingetOkCodes) { Write-Log "$(T 'AlreadyUpToDate'): $($app.Name)" -Level 'SUCCESS' Add-Result -Section (T 'PhaseUpdate') -Item $app.Name -Status 'OK' -Detail (T 'AlreadyUpToDate') $script:Summary.AppsUpdated++ } else { - $failReason = switch ($exitCode) { - -1978335215 { 'Installer hash mismatch — retry later or check proxy/AV' } - -1978335212 { 'Package not found in winget source (ID may have changed)' } - -1978334960 { 'Installer blocked by security policy' } - -1978335132 { 'Installer requires reboot before continuing' } - -1978334967 { 'Network failure — download failed (all retries exhausted)' } - -1978334966 { 'Network failure — timed out (all retries exhausted)' } - -2147012887 { 'Network failure — connection reset (all retries exhausted)' } - -2147012873 { 'Network failure — DNS not resolved (all retries exhausted)' } - -2147012867 { 'Network failure — connection refused (all retries exhausted)' } - -2147012889 { 'Network failure — WinHTTP error (all retries exhausted)' } - default { "Exit code $exitCode" } - } + $failReason = Get-WingetFailReason -ExitCode $exitCode + if ($exitCode -eq -1978334966) { $script:Summary.RebootNeeded = $true } Write-Log "$(T 'UpdateFail'): $($app.Name) - $failReason" -Level 'WARN' $diagLines = ($wingetOut | Where-Object { "$_".Trim() }) | Select-Object -Last 8 foreach ($line in $diagLines) { @@ -1424,6 +1576,9 @@ function Write-ConsoleSummary { Write-Log "$( (T 'SumConfigOK') ) : $($script:Summary.HardeningApplied)" Write-Log "$( (T 'SumConfigFail')) : $($script:Summary.HardeningFailed)" Write-Log "$( (T 'SumMcAfee') ) : $($script:Summary.McAfeeRemoved)" + if ($script:Summary.RebootNeeded) { + Write-Log (T 'SumReboot') -Level 'WARN' + } Write-Log $border -Level 'SECTION' } @@ -1508,6 +1663,9 @@ try { Write-Host "`n*** $(T 'SetupComplete') ***" -ForegroundColor Green Write-Host " Log : $LogPath" -ForegroundColor Gray Write-Host " Report : $ReportPath" -ForegroundColor Cyan + if ($script:Summary.RebootNeeded) { + Write-Host "`n$(T 'SumReboot')" -ForegroundColor Yellow + } Write-Host "`n$(T 'PressEnter')" -ForegroundColor Yellow Read-Host | Out-Null } From 826a2318c647abc3d9f3c74d8c3c31de9a6aa1ed Mon Sep 17 00:00:00 2001 From: Pacific Northwest Computers <142192730+Pnwcomputers@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:23:45 -0700 Subject: [PATCH 2/7] Update version to DeployWorkstation Launcher v5.3 --- DeployWorkstation.bat | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DeployWorkstation.bat b/DeployWorkstation.bat index fcd9525..ccf7bbc 100644 --- a/DeployWorkstation.bat +++ b/DeployWorkstation.bat @@ -1,14 +1,14 @@ @echo off -title DeployWorkstation Launcher v5.2 +title DeployWorkstation Launcher v5.3 REM ======================================================== REM DeployWorkstation.bat - Launcher for DeployWorkstation.ps1 -REM Version 5.2 - PNWC Edition 6.22.2026 +REM Version 5.3 - PNWC Edition 6.22.2026 REM ======================================================== setlocal enabledelayedexpansion echo. -echo ===== DeployWorkstation Launcher v5.2 ===== +echo ===== DeployWorkstation Launcher v5.3 ===== echo. REM 1) Check if we're already elevated From 32953dfec8cc1b9a488fd9c1a67956f1d3384ce0 Mon Sep 17 00:00:00 2001 From: Pacific Northwest Computers <142192730+Pnwcomputers@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:26:13 -0700 Subject: [PATCH 3/7] Update README.md --- README.md | 138 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 90 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 77fdd5a..6eea865 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,28 @@ A PowerShell-based, automated provisioning solution that transforms Windows 10 & 11 workstation deployment from a 30-step manual process into a single "plug-and-play" operation. Whether you're imaging bare metal, cleaning up an existing PC, or running routine maintenance on already-deployed machines, DeployWorkstation handles bloatware removal, essential application installation, and in-place app upgrades. -## 🐛 Bugs Fixed in v5.2 +## 🐛 Bugs Fixed in v5.3 + +| # | File | Issue | Fix | +|---|------|-------|-----| +| 1 | `DeployWorkstation.ps1` | `Microsoft.DotNet.Framework.4.8` was never a valid winget package ID — every run failed that install with `0x8A150014 NO_APPLICATIONS_FOUND`. | Corrected to `Microsoft.DotNet.Framework.Runtime` (currently 4.8.1), with `Microsoft.DotNet.Framework.DeveloperPack_4` as a fallback ID. On Win10 1903+ / Win11 the runtime ships in-box, so this typically resolves to a fast no-op. | +| 2 | `DeployWorkstation.ps1` | Reboot exit codes `0x8A150109` (restart to finish) and `0x8A15010A` (restart before install) were misclassified as *network errors* — successful installs that only needed a reboot (Adobe Reader does this regularly) were retried twice, then logged as network failures. | Reboot-to-finish now counts as **success**, sets a `RebootNeeded` flag, and a restart notice is shown in the summary and at end of run. Reboot-before-install fails with the correct reason. | +| 3 | `DeployWorkstation.ps1` | Several exit-code hex comments were wrong (`-1978335212` labeled `0x8A15002C`, is actually `0x8A150014`; `-1978334960` labeled "blocked by policy", is actually a dependency failure). | Exit-code tables rebuilt against the official winget `returnCodes.md`; unknown codes now log their hex form for easy lookup. | +| 4 | `DeployWorkstation.ps1` | `winget install`/`upgrade` ran without `--exact` — substring ID matching could hit the wrong package or fail as ambiguous. | Added `--exact` (plus `--disable-interactivity` to prevent hangs on prompts) to all winget install/upgrade calls. | +| 5 | `DeployWorkstation.ps1` | In update mode, apps not installed on the machine were counted as update *failures*. | `0x8A150014` from `winget upgrade --id` is now logged as **SKIPPED** (not installed), tracked separately in the summary. | + +### New in v5.3: Package ID Auto-Resolution + +Winget package IDs get renamed and retired over time. v5.3 verifies every ID against the winget source before installing, and recovers automatically when one has changed: + +1. **Primary ID** checked with `winget show --exact` +2. **Fallback IDs** — optional per-app list of known alternates, tried in order +3. **`winget search` by display name** — adopted automatically only if the result is unambiguous; otherwise candidate IDs are logged so you can update the app list + +Results are cached per run (install + update share the cache), adding only a few seconds to a full deployment. Other reliability improvements: `--include-unknown` on upgrades (handles apps whose installed version winget can't read — common after Windows Update services .NET runtimes out from under winget), and installer hash-mismatch errors now trigger a `winget source update` before retrying, since they're usually a stale manifest. + +
+🐛 Bugs Fixed in v5.2 | # | File | Issue | Fix | |---|------|-------|-----| @@ -24,7 +45,7 @@ A PowerShell-based, automated provisioning solution that transforms Windows 10 & | 2 | `QuickStart.cmd` | `goto start` on invalid input → no `:start` label existed → `cmd.exe` crashed the script. | Added `:start` label at the top of the menu block. | | 3 | `tests/DeployWorkstation.Tests.ps1:42` | Test asserted `DeployWorkstation.cmd` exists - it never did - permanent CI failure. | Changed to check `QuickStart.cmd` which actually exists. | | 4 | `.github/workflows/test-powershell.yml:37` | `upload-artifact@v3` was deprecated and removed by GitHub. | Upgraded to `@v4`. | -| 5 | `DeployWorkstation.ps1:587` | `winget--version` can output multiple lines; `-replace` on an array returns an array; `[Version]` cast on an array throws and silently skips the minimum version check. | Added `Where-Object` + `Select-Object-Last 1` before the replace. | +| 5 | `DeployWorkstation.ps1:587` | `winget --version` can output multiple lines; `-replace` on an array returns an array; `[Version]` cast on an array throws and silently skips the minimum version check. | Added `Where-Object` + `Select-Object -Last 1` before the replace. | ### Code Quality Updates/Fixes: @@ -34,25 +55,29 @@ A PowerShell-based, automated provisioning solution that transforms Windows 10 & | 7 | `DeployWorkstation.ps1:535` | `$script:IsWin11` computed but never referenced anywhere. | Removed. | | 8 | `Export-HtmlReport` | Called `Get-CimInstance Win32_OperatingSystem` a second time at report generation, even though it was already cached as `$script:OsInfo`. | Replaced with `$script:OsInfo`. | +
+ --- ## ✨ Key Features - + - **🔐 Self-Elevating & Policy-Bypassing** - Automatically relaunches under Windows PowerShell 5.1 with `-ExecutionPolicy Bypass` and UAC elevation - **🗑️ UWP "Bloatware" Purge** - Comprehensive removal of built-in apps including Copilot, Teams, New Outlook, Clipchamp, OneDrive, Xbox, and more - **⚙️ Win32/MSI Removal & DISM Cleanup** - Enterprise software removal via WinGet, DISM, and registry manipulation - **📦 Standard App Installation & Upgrade** - Automated install and in-place upgrade of essential third-party tools via WinGet +- **🔎 Package ID Auto-Resolution** *(new in 5.3)* - Verifies every winget ID before install; recovers from renamed/retired IDs via per-app fallbacks and `winget search`, and logs replacement candidates +- **🔄 Reboot-Aware Exit Handling** *(new in 5.3)* - Installs that succeed but require a restart are counted as success, flagged in the summary, and surfaced with a restart notice at end of run - **📋 Centralized Logging** - Detailed operation logs plus a dark-themed HTML report with system info summary and full event log - **🔄 App Update Support** - Detects and upgrades already-installed applications in-place; safe to re-run on existing machines -- **🛡️ Winget Auto-Bootstrap** - Automatically downloads and installs winget on OEM machines where it's missing or outdated -- **🔁 Network Retry Logic** - Automatic retries with delay on transient network errors during installation +- **🛡️ Winget Auto-Bootstrap** - Automatically downloads and installs winget on OEM machines where it's missing or outdated (v5.3 requires winget 1.4+ and bootstraps it automatically) +- **🔁 Smart Retry Logic** - Automatic retries with delay on genuinely transient errors (network faults, stale-manifest hash mismatches); reboot and policy errors are correctly identified instead of blindly retried - **🖥️ Windows Edition Awareness** - Detects Home vs. Pro/Enterprise and warns when policy keys will have no effect - **🗑️ OEM OneDrive Removal** - Three-path removal covering both Appx and embedded OEM binaries - **🌐 Multi-Language Support** - Auto-detects locale via `Get-Culture`; ships with `en-US` and `es-ES` - **✅ Real-time Progress** - `Write-Progress` console bars throughout all major operations ## 🛡️ Automated Removal Capabilities - + ### UWP Applications Removed - 📧 New Outlook (Microsoft.OutlookForWindows) - 🤖 Copilot Assistant @@ -82,7 +107,7 @@ A PowerShell-based, automated provisioning solution that transforms Windows 10 & - Disables Advertising ID ## 📥 Essential Applications Installed - + ### Security & Maintenance - 🦠 **Malwarebytes** - Malware protection - 🧹 **BleachBit** - System cleanup and privacy tool @@ -94,27 +119,27 @@ A PowerShell-based, automated provisioning solution that transforms Windows 10 & - 📹 **VLC Media Player** - Universal media player ### Development Runtimes -- ⚙️ **.NET Framework 4.8** - Legacy app compatibility +- ⚙️ **.NET Framework Runtime** (4.8.1) - Legacy app compatibility; ships in-box on Win10 1903+/Win11, so this is usually a no-op - ⚙️ **.NET 8 Desktop Runtime** - LTS, supported through November 2026 - ⚙️ **.NET 10 Desktop Runtime** - LTS, supported through November 2030 - 🔧 **Visual C++ 2015–2022 Redistributables** (x64 & x86) ## 🚀 Installation & Usage - + ### Prerequisites - 💻 Windows 10/11 (Most Editions) -- 🌐 Internet Connection (for WinGet packages - Winget auto-installs if missing) +- 🌐 Internet Connection (for WinGet packages - Winget 1.4+ auto-installs if missing or outdated) - 👤 Administrator Access - 💾 USB Drive or Network Share (Optional) ### Quick Start - + 1. **📥 Download the Repository** ```bash git clone https://github.com/Pnwcomputers/DeployWorkstation.git cd DeployWorkstation ``` - + 2. **💾 Prepare Deployment Media** ```cmd copy DeployWorkstation.ps1 E:\ @@ -147,12 +172,13 @@ A PowerShell-based, automated provisioning solution that transforms Windows 10 & - Script pauses for final review on completion - HTML report generated: `DeployWorkstation.html` - Detailed log available: `DeployWorkstation.log` + - If any install requires a restart to finish, the summary and end-of-run output say so explicitly - System reboot recommended for a clean finish ### Re-Running on Existing Machines - -v5.2 is safe to run on already-deployed workstations. The upgrade logic updates any managed apps with newer versions available via winget. - + +v5.3 is safe to run on already-deployed workstations. The upgrade logic updates any managed apps with newer versions available via winget, skips apps that aren't installed on the machine, and picks up apps whose installed version winget can't read (`--include-unknown`). + ```powershell # Update managed apps only .\DeployWorkstation.ps1 -SkipBloatwareRemoval -SkipSystemConfig -UpdateApps @@ -160,11 +186,11 @@ v5.2 is safe to run on already-deployed workstations. The upgrade logic updates # Re-run app install on a previously cleaned machine .\DeployWorkstation.ps1 -SkipBloatwareRemoval ``` - + ## 🔧 Advanced Configuration - + ### Command-Line Parameters - + | Parameter | Description | |-----------|-------------| | `-SkipAppInstall` | Skip all application installation | @@ -176,48 +202,52 @@ v5.2 is safe to run on already-deployed workstations. The upgrade logic updates ### Adding Applications -To add or change which apps are installed, edit the `$script:ManagedApps` array near the top of `DeployWorkstation.ps1`. Each entry needs a winget ID and a display name: +To add or change which apps are installed, edit the `$script:ManagedApps` array near the top of `DeployWorkstation.ps1`. Each entry needs a winget ID and a display name; an optional `Fallbacks` list holds alternate IDs to try if the primary one is ever renamed or retired: ```powershell $script:ManagedApps = @( - @{ Id = 'Google.Chrome'; Name = 'Google Chrome' }, + @{ Id = 'Google.Chrome'; Name = 'Google Chrome' + Fallbacks = @('Google.Chrome.EXE') }, @{ Id = 'Notepad++.Notepad++'; Name = 'Notepad++' }, # add more entries here ) ``` +The `Name` field doubles as the `winget search` term of last resort, so keep it close to the app's actual display name in the winget catalog. + Find winget IDs with: `winget search ` - + ## 🎪 Configuration Profiles - + | Profile | Use Case | Applications | Configuration | |---------|----------|--------------|---------------| | **[Corporate](Config/Examples/Corporate.json)** | Business workstations | Office tools, security software | [Details](docs/CONFIGURATION.md#corporate) | | **[Developer](Config/Examples/Developer.json)** | Programming workstations | IDEs, development tools | [Details](docs/CONFIGURATION.md#developer) | | **[Home User](Config/Examples/HomeUser.json)** | Personal computers | Media, communication apps | [Details](docs/CONFIGURATION.md#home-user) | - + ## 📊 Feature Comparison - + | Feature | Manual Deployment | Other Tools | DeployWorkstation | |---------|------------------|-------------|-------------------| | **Automation Level** | ❌ Manual (8+ hours) | ⚠️ Partial (2-4 hours) | ✅ Full Automation (30 minutes) | | **Bloatware Removal** | ❌ Manual deletion | ⚠️ Basic removal | ✅ Comprehensive purge | | **Enterprise Software** | ❌ Manual uninstall | ❌ Often skipped | ✅ Registry-based removal | | **App Updates** | ❌ Manual per-app | ⚠️ Separate tool needed | ✅ In-place upgrade on re-run | -| **Error Handling** | ❌ Manual intervention | ⚠️ Basic logging | ✅ Retry logic + HTML report | +| **Package ID Drift** | ❌ Silent failures | ❌ Silent failures | ✅ Auto-resolution + logged candidates | +| **Error Handling** | ❌ Manual intervention | ⚠️ Basic logging | ✅ Smart retry logic + HTML report | | **Multi-Language** | ❌ | ❌ | ✅ en-US & es-ES auto-detected | - + ## 📈 Performance Metrics - + | Metric | Traditional Method | DeployWorkstation | |--------|-------------------|-------------------| | **Total Time** | 4-8 hours | 30-45 minutes | | **Manual Steps** | 30+ operations | 1 double-click | | **Error Rate** | ~15% (human error) | <2% (automated) | | **Consistency** | Variable | 100% standardized | - + ## 🎯 Use Cases - + ### **🏢 Enterprise Deployment** - New employee workstation setup - Hardware refresh projects @@ -242,16 +272,16 @@ Find winget IDs with: `winget search ` - Small office standardization ## 🔍 Troubleshooting - + ### Common Issues - + **Script won't execute** - Ensure PowerShell execution policy allows scripts - Verify UAC elevation is working - Check Windows PowerShell 5.1 is available **WinGet installation failures** -- The script will attempt to auto-install/repair winget on OEM machines +- The script will attempt to auto-install/repair winget on OEM machines (v5.3 requires winget 1.4+ and bootstraps the current release automatically) - Verify internet connectivity if bootstrap also fails - Update Windows to latest version @@ -260,9 +290,15 @@ Find winget IDs with: `winget search ` - Ensure all user profiles are processed - Check Group Policy restrictions -**App install fails with "package not found"** +**App install fails with "Package ID not found in winget source"** +- v5.3 tries fallback IDs and a `winget search` automatically before reporting this +- Check the log for a "Possible replacement IDs" line — it lists candidates when the search was ambiguous +- Verify the current ID manually (`winget search `), then update the `Id` (or add a `Fallbacks` entry) in `$script:ManagedApps` - Run `winget source update` to refresh the package index -- Verify the winget ID is still current: `winget search ` + +**Install marked OK but says "restart required to finish"** +- This is success, not an error - some installers (Adobe Reader in particular) finish on the next boot +- Reboot the machine; re-running the script afterward will confirm the app as already installed ### Log Analysis ```powershell @@ -272,11 +308,11 @@ Get-Content .\DeployWorkstation.log | Select-String "ERROR|WARN" # Verify WinGet package status winget list --source winget ``` - + The HTML report (`DeployWorkstation.html`) provides the same information in a readable format - open it in any browser after the run completes. - + ## 🛠️ Project Structure - + ```text DeployWorkstation/ ├── DeployWorkstation.ps1 # Main PowerShell script @@ -300,9 +336,9 @@ DeployWorkstation/ └── tests/ └── DeployWorkstation.Tests.ps1 ``` - + ## 🔮 Roadmap - + - 🖥️ GUI Configuration Interface (pre-run checkbox dialog) - 🌐 Web-based Management Console - 🚀 Domain Integration @@ -311,37 +347,43 @@ DeployWorkstation/ - 📊 Analytics & Telemetry (CSV/JSON export for fleet tracking) ## 🤝 Contributing - + We welcome contributions! Here's how to get started: - + - **📖 Documentation** - Improve README clarity, add configuration examples, create troubleshooting guides - **🐛 Bug Reports** - Open issues with detailed descriptions, system info, and log excerpts - **💡 Feature Requests** - Open issues with `[FEATURE]` tag, describe use case and benefits - **🔒 Security Issues** - Email [support@pnwcomputers.com](mailto:support@pnwcomputers.com) with proof of concept; allow reasonable disclosure time ## 📄 License - + This project is licensed under the MIT License - See the [LICENSE](LICENSE.md) file for details. - + ## 📞 Support & Contact - + - 📖 **Documentation**: Check this README and the [project wiki](docs/) - 🐛 **Bug Reports**: Open an issue on GitHub - 💡 **Feature Requests**: Open an issue with `[FEATURE]` tag - 💬 **General Support**: [support@pnwcomputers.com](mailto:support@pnwcomputers.com) --- - + ## 📊 Statistics - + ![GitHub stars](https://img.shields.io/github/stars/Pnwcomputers/DeployWorkstation) ![GitHub forks](https://img.shields.io/github/forks/Pnwcomputers/DeployWorkstation) ![GitHub issues](https://img.shields.io/github/issues/Pnwcomputers/DeployWorkstation) ![GitHub license](https://img.shields.io/github/license/Pnwcomputers/DeployWorkstation) - + **🎯 Transform your Windows deployment process from hours to minutes, and keep it current with every re-run.** - + Built with ❤️ for efficiency, reliability, and zero-touch automation. + +[⭐ Star this repo](https://github.com/Pnwcomputers/DeployWorkstation) if it saved you time and effort! + +--- +*Updated July 2026* +*Tested on Windows 10 (1909+) and Windows 11 - Pro & Home Editions* [⭐ Star this repo](https://github.com/Pnwcomputers/DeployWorkstation) if it saved you time and effort! From 5b5582330efd5a422bbe9bf50cac84330cb4e0a4 Mon Sep 17 00:00:00 2001 From: Pacific Northwest Computers <142192730+Pnwcomputers@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:28:05 -0700 Subject: [PATCH 4/7] Fix bugs and improve package ID resolution in README Fixed bugs in DeployWorkstation.ps1 related to package ID handling and reboot exit codes. Improved reliability of package ID resolution and added fallback mechanisms. --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 6eea865..98d3efd 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,10 @@ A PowerShell-based, automated provisioning solution that transforms Windows 10 & | # | File | Issue | Fix | |---|------|-------|-----| -| 1 | `DeployWorkstation.ps1` | `Microsoft.DotNet.Framework.4.8` was never a valid winget package ID — every run failed that install with `0x8A150014 NO_APPLICATIONS_FOUND`. | Corrected to `Microsoft.DotNet.Framework.Runtime` (currently 4.8.1), with `Microsoft.DotNet.Framework.DeveloperPack_4` as a fallback ID. On Win10 1903+ / Win11 the runtime ships in-box, so this typically resolves to a fast no-op. | -| 2 | `DeployWorkstation.ps1` | Reboot exit codes `0x8A150109` (restart to finish) and `0x8A15010A` (restart before install) were misclassified as *network errors* — successful installs that only needed a reboot (Adobe Reader does this regularly) were retried twice, then logged as network failures. | Reboot-to-finish now counts as **success**, sets a `RebootNeeded` flag, and a restart notice is shown in the summary and at end of run. Reboot-before-install fails with the correct reason. | +| 1 | `DeployWorkstation.ps1` | `Microsoft.DotNet.Framework.4.8` was never a valid winget package ID so every run failed that install with `0x8A150014 NO_APPLICATIONS_FOUND`. | Corrected to `Microsoft.DotNet.Framework.Runtime` (currently 4.8.1), with `Microsoft.DotNet.Framework.DeveloperPack_4` as a fallback ID. On Win10 1903+ / Win11 the runtime ships in-box, so this typically resolves to a fast no-op. | +| 2 | `DeployWorkstation.ps1` | Reboot exit codes `0x8A150109` (restart to finish) and `0x8A15010A` (restart before install) were misclassified as *network errors*; successful installs that only needed a reboot (Adobe Reader does this regularly) were retried twice, then logged as network failures. | Reboot-to-finish now counts as **success**, sets a `RebootNeeded` flag, and a restart notice is shown in the summary and at end of run. Reboot-before-install fails with the correct reason. | | 3 | `DeployWorkstation.ps1` | Several exit-code hex comments were wrong (`-1978335212` labeled `0x8A15002C`, is actually `0x8A150014`; `-1978334960` labeled "blocked by policy", is actually a dependency failure). | Exit-code tables rebuilt against the official winget `returnCodes.md`; unknown codes now log their hex form for easy lookup. | -| 4 | `DeployWorkstation.ps1` | `winget install`/`upgrade` ran without `--exact` — substring ID matching could hit the wrong package or fail as ambiguous. | Added `--exact` (plus `--disable-interactivity` to prevent hangs on prompts) to all winget install/upgrade calls. | +| 4 | `DeployWorkstation.ps1` | `winget install`/`upgrade` ran without `--exact` so a substring ID matching could hit the wrong package or fail as ambiguous. | Added `--exact` (plus `--disable-interactivity` to prevent hangs on prompts) to all winget install/upgrade calls. | | 5 | `DeployWorkstation.ps1` | In update mode, apps not installed on the machine were counted as update *failures*. | `0x8A150014` from `winget upgrade --id` is now logged as **SKIPPED** (not installed), tracked separately in the summary. | ### New in v5.3: Package ID Auto-Resolution @@ -31,10 +31,10 @@ A PowerShell-based, automated provisioning solution that transforms Windows 10 & Winget package IDs get renamed and retired over time. v5.3 verifies every ID against the winget source before installing, and recovers automatically when one has changed: 1. **Primary ID** checked with `winget show --exact` -2. **Fallback IDs** — optional per-app list of known alternates, tried in order -3. **`winget search` by display name** — adopted automatically only if the result is unambiguous; otherwise candidate IDs are logged so you can update the app list +2. **Fallback IDs** optional per-app list of known alternates, tried in order +3. **`winget search` by display name* adopted automatically only if the result is unambiguous; otherwise candidate IDs are logged so you can update the app list -Results are cached per run (install + update share the cache), adding only a few seconds to a full deployment. Other reliability improvements: `--include-unknown` on upgrades (handles apps whose installed version winget can't read — common after Windows Update services .NET runtimes out from under winget), and installer hash-mismatch errors now trigger a `winget source update` before retrying, since they're usually a stale manifest. +Results are cached per run (install + update share the cache), adding only a few seconds to a full deployment. Other reliability improvements: `--include-unknown` on upgrades (handles apps whose installed version winget can't read. This is common after Windows Update services .NET runtimes out from under winget), and installer hash-mismatch errors now trigger a `winget source update` before retrying, since they're usually a stale manifest.
🐛 Bugs Fixed in v5.2 @@ -292,7 +292,7 @@ Find winget IDs with: `winget search ` **App install fails with "Package ID not found in winget source"** - v5.3 tries fallback IDs and a `winget search` automatically before reporting this -- Check the log for a "Possible replacement IDs" line — it lists candidates when the search was ambiguous +- Check the log for a "Possible replacement IDs" line; it lists candidates when the search was ambiguous - Verify the current ID manually (`winget search `), then update the `Id` (or add a `Fallbacks` entry) in `$script:ManagedApps` - Run `winget source update` to refresh the package index From 3062b82b4c31906b3fc9cad1f96682448d6499f2 Mon Sep 17 00:00:00 2001 From: Pacific Northwest Computers <142192730+Pnwcomputers@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:28:50 -0700 Subject: [PATCH 5/7] Update README with version v5.3 (Beta) Added version information for v5.3 (Beta) to README. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 98d3efd..e7e05e2 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,9 @@ ### **Zero-Touch Windows Workstation Provisioning & Maintenance Toolkit** A PowerShell-based, automated provisioning solution that transforms Windows 10 & 11 workstation deployment from a 30-step manual process into a single "plug-and-play" operation. Whether you're imaging bare metal, cleaning up an existing PC, or running routine maintenance on already-deployed machines, DeployWorkstation handles bloatware removal, essential application installation, and in-place app upgrades. - + +# Version v5.3 (Beta) + ## 🐛 Bugs Fixed in v5.3 | # | File | Issue | Fix | From 44551d4b57249453b2831d8883dd9d3cff5c8293 Mon Sep 17 00:00:00 2001 From: Pacific Northwest Computers <142192730+Pnwcomputers@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:29:55 -0700 Subject: [PATCH 6/7] Update README.md From 90ea008defb394edb3a50f67b1b387bc24f2ecd0 Mon Sep 17 00:00:00 2001 From: Jon Eric Pienkowski <142192730+Pnwcomputers@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:42:40 -0700 Subject: [PATCH 7/7] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e7e05e2..98c9d8d 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ A PowerShell-based, automated provisioning solution that transforms Windows 10 & 11 workstation deployment from a 30-step manual process into a single "plug-and-play" operation. Whether you're imaging bare metal, cleaning up an existing PC, or running routine maintenance on already-deployed machines, DeployWorkstation handles bloatware removal, essential application installation, and in-place app upgrades. -# Version v5.3 (Beta) +# Version v5.3 ## 🐛 Bugs Fixed in v5.3