From 294eecbd8fb0461c19ca1b2efeebc8e36554a5ff Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 25 Sep 2026 22:04:07 -0500 Subject: [PATCH 1/2] Add unattended mode to SandboxTest.ps1 * Add -Unattended to wait for the sandbox run and surface its output on the host * Write a transcript, metadata.json and WinGet logs to a Results folder inside the sandbox * Document the new option, the results layout and exit code 5 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Tools/SandboxTest.ps1 | 70 ++++++++++++++++++++++++++++++++++++++-- doc/tools/SandboxTest.md | 30 +++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/Tools/SandboxTest.ps1 b/Tools/SandboxTest.ps1 index b84600af73d4d..c8ea7a5a37b24 100644 --- a/Tools/SandboxTest.ps1 +++ b/Tools/SandboxTest.ps1 @@ -5,6 +5,7 @@ # 2 = Unable to kill a running process # 3 = WinGet is not installed # 4 = Manifest validation error +# 5 = Unattended run did not complete within the timeout ### [CmdletBinding()] @@ -68,6 +69,10 @@ param( return $true })] [string] $WinGetOptions, + # Unattended + [Parameter(HelpMessage = 'Wait up to this many minutes for the sandbox run to complete and write its output to the host')] + [ValidateRange(1, [int]::MaxValue)] + [int] $Unattended, # Switches [switch] $SkipManifestValidation, [switch] $Prerelease, @@ -84,8 +89,11 @@ enum DependencySources { $ProgressPreference = 'SilentlyContinue' $ErrorActionPreference = 'Stop' # This gets overridden most places, but is set explicitly here to help catch errors if ($PSBoundParameters.Keys -notcontains 'InformationAction') { $InformationPreference = 'Continue' } # If the user didn't explicitly set an InformationAction, Override their preference +$script:IsUnattended = $PSBoundParameters.Keys -contains 'Unattended' if ($PSBoundParameters.Keys -contains 'WarningAction') { $script:OnMappedFolderWarning = $PSBoundParameters.WarningAction +} elseif ($script:IsUnattended) { + $script:OnMappedFolderWarning = 'Continue' } else { $script:OnMappedFolderWarning = 'Inquire' } @@ -783,6 +791,14 @@ function Get-ARPTable { } Push-Location $($script:SandboxTestDataFolder) +`$Unattended = $([int]$script:IsUnattended) +`$ResultsFolder = Join-Path '$($script:SandboxTestDataFolder)' 'Results' +`$metadata = [ordered]@{ StartTime = (Get-Date).ToString('o') } +if (`$Unattended) { + New-Item -Path `$ResultsFolder -ItemType Directory -Force | Out-Null + Start-Transcript -Path (Join-Path `$ResultsFolder 'transcript.log') | Out-Null +} +try { Write-Host @' --> Installing WinGet '@ @@ -875,7 +891,14 @@ if (`$manifestFolder) { `"@ `$originalARP = Get-ARPTable - winget install -m `$manifestFolder --accept-package-agreements --verbose-logs --ignore-local-archive-malware-scan --dependency-source winget $WinGetOptions + `$metadata.Manifest = `$manifestFolder | Split-Path -Leaf + if (`$Unattended) { + winget install -m `$manifestFolder --accept-package-agreements --verbose-logs --ignore-local-archive-malware-scan --dependency-source winget $WinGetOptions | Out-Host + } else { + winget install -m `$manifestFolder --accept-package-agreements --verbose-logs --ignore-local-archive-malware-scan --dependency-source winget $WinGetOptions + } + `$metadata.InstallExitCode = `$LASTEXITCODE + `$metadata.InstallExitCodeHex = '0x{0:X8}' -f `$LASTEXITCODE Write-Host @' @@ -899,7 +922,28 @@ if (`$BoundParameterScript) { } '@ -& `$BoundParameterScript.FullName + `$global:LASTEXITCODE = 0 + try { + & `$BoundParameterScript.FullName + `$metadata.ScriptSucceeded = `$? + } catch { + `$metadata.ScriptSucceeded = `$false + `$metadata.ScriptError = "`$_" + throw + } finally { + `$metadata.ScriptExitCode = `$LASTEXITCODE + } +} +} finally { + if (`$Unattended) { + `$metadata.EndTime = (Get-Date).ToString('o') + if (Get-Command winget -ErrorAction SilentlyContinue) { `$metadata.WinGetVersion = winget --version } + `$metadata.OSVersion = '{0}.{1}' -f [Environment]::OSVersion.Version.ToString(3), (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').UBR + `$metadata | ConvertTo-Json | Out-File -FilePath (Join-Path `$ResultsFolder 'metadata.json') -Encoding utf8 + Copy-Item -Path (Join-Path `$env:LOCALAPPDATA 'Packages\$($script:AppInstallerPFN)\LocalState\DiagOutputDir') -Destination (Join-Path `$ResultsFolder 'WinGetLogs') -Recurse -ErrorAction SilentlyContinue + Stop-Transcript | Out-Null + New-Item -Path (Join-Path `$ResultsFolder 'done') -ItemType File -Force | Out-Null + } } Pop-Location @@ -966,4 +1010,26 @@ $Script Write-Verbose "Invoking the sandbox using $script:ConfigurationFile" WindowsSandbox $script:ConfigurationFile + +if ($script:IsUnattended) { + $resultsFolder = Join-Path -Path $script:TestDataFolder -ChildPath 'Results' + $doneFile = Join-Path -Path $resultsFolder -ChildPath 'done' + $deadline = (Get-Date).AddMinutes($Unattended) + Write-Information "--> Waiting up to $Unattended minutes for the sandbox run to complete" + while (!(Test-Path -Path $doneFile) -and (Get-Date) -lt $deadline) { Start-Sleep -Seconds 5 } + + Get-Content -Path (Join-Path -Path $resultsFolder -ChildPath 'transcript.log') -ErrorAction SilentlyContinue | ForEach-Object { Write-Information $_ } + Write-Information @" +--> Sandbox results: + - Transcript: $(Join-Path -Path $resultsFolder -ChildPath 'transcript.log') + - Metadata: $(Join-Path -Path $resultsFolder -ChildPath 'metadata.json') + - WinGet Logs: $(Join-Path -Path $resultsFolder -ChildPath 'WinGetLogs') +"@ + if (!(Test-Path -Path $doneFile)) { + Write-Error -Category OperationTimeout "The sandbox run did not complete within $Unattended minutes" -ErrorAction Continue + Invoke-CleanExit -ExitCode 5 + } + Get-Content -Path (Join-Path -Path $resultsFolder -ChildPath 'metadata.json') -ErrorAction SilentlyContinue | ForEach-Object { Write-Information $_ } +} + Invoke-CleanExit -ExitCode 0 diff --git a/doc/tools/SandboxTest.md b/doc/tools/SandboxTest.md index 102a4fe453152..f33b5f64a222a 100644 --- a/doc/tools/SandboxTest.md +++ b/doc/tools/SandboxTest.md @@ -46,6 +46,7 @@ Run the script with the full path to the manifest as an argument. This validates | **-MapFolder** | Folder to map in Sandbox (default: current directory) | | **-WinGetVersion** | Specify the version of WinGet to use in Sandbox | | **-WinGetOptions** | Additional options for the `winget install` command | +| **-Unattended** | Wait up to the specified number of minutes for the Sandbox run to finish, then write its output to the host | | **-SkipManifestValidation** | Skip `winget validate -m ` if already validated | | **-Prerelease** | Use preview release versions of WinGet | | **-EnableExperimentalFeatures** | Enable WinGet experimental features | @@ -80,6 +81,35 @@ Run the script with the full path to the manifest as an argument. This validates --- +## Unattended Mode + +By default the script starts the Sandbox and returns immediately; the output of the session running inside the Sandbox stays inside the Sandbox. The `-Unattended` option makes the script wait for that session to finish and then write its output to the host, which is useful for automation. + +`-Unattended` requires a value, which is the number of minutes to wait before giving up. 15 is a good starting point. + +```powershell +.\SandboxTest.ps1 -Unattended 15 +``` + +The Sandbox is left open after the run finishes. + +### Results + +The Sandbox session writes its results to `%LOCALAPPDATA%\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\SandboxTest\Results`: + +| Item | Description | +|------------------|--------------------------------------------------------------------------| +| `transcript.log` | Full PowerShell transcript of the Sandbox session | +| `metadata.json` | WinGet version, OS build, manifest folder name, `winget install` exit code (decimal and hexadecimal), `-Script` exit status, and start/end timestamps | +| `WinGetLogs\` | WinGet diagnostic logs copied from the Sandbox | +| `done` | Marker file written once the Sandbox session has finished | + +This folder is deleted at the start of every run, so copy anything you want to keep before running the script again. + +The transcript and the metadata are also written to the host output once the run finishes. If the run does not finish within the given number of minutes, the script writes whatever the transcript contains so far and exits with code 5. + +--- + ## Integration with Other Scripts - **[YamlCreate Script](../../Tools/YamlCreate.ps1):** Uses `SandboxTest.ps1` to test newly created manifests. From 5712b53b142cc705a24db142e689c9a3a1b108dd Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 25 Sep 2026 22:27:00 -0500 Subject: [PATCH 2/2] Add WinGet package family name tokens to spelling expect list Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/actions/spelling/expect.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 504ae449ce99e..ce7d7a3819d66 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -1,4 +1,5 @@ automerge +bbwe denelon dkbennett Freeware @@ -11,4 +12,5 @@ nullsoft stephengillie img UTF +wekyb wix