Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/actions/spelling/expect/generic_terms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
onebranch
screenshots
scrollbars
Scrollbars

Check warning on line 14 in .github/actions/spelling/expect/generic_terms.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

`Scrollbars` is ignored by check-spelling because another more general variant is also in expect (ignored-expect-variant)
Searchbox
Sdl
sortby
Expand All @@ -21,3 +21,12 @@
versioning
VGpu
vse
debian
euo
linux
pipefail
sudoer
tarball
utf
vendored
versioned
14 changes: 14 additions & 0 deletions .github/actions/spelling/expect/software.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
vscode
Linux

Check warning on line 2 in .github/actions/spelling/expect/software.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

`Linux` is ignored by check-spelling because another more general variant is also in expect (ignored-expect-variant)
dotnet
dotnettool
microsoftdotnet
Expand All @@ -18,3 +18,17 @@
lldb
Rustlang
vadimcn
CPython
gofmt
gradlew
javac
Jdk
mojibake
mvnw
rustc
tsc
uvx
winappcli
winbuild
wincreate
winforms
8 changes: 8 additions & 0 deletions .github/actions/spelling/expect/windows_terms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,11 @@ cursorindicator
packagetype
BSODs
Audiosrv
ADMX
chcp
conhost
MDM
PATHEXT
Redist
sideloaded
UWP
123 changes: 123 additions & 0 deletions WindowsDevScripts/scripts/windows/_common/apply-configuration.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
ο»Ώ<#
.SYNOPSIS
Apply a winget DSC configuration file with retry, refresh PATH in the current
session, verify a list of expected commands, and emit the CI sentinel.

.DESCRIPTION
Flow-level `install.ps1` files are thin shims: the real install logic lives
in each flow's `configuration.winget`. This helper centralizes the glue
that CI needs around `winget configure`:

1. `winget configure --file <ConfigFile>` with exponential-backoff retry
(shared helper; flaky network is common on hosted runners). Always
passes `--accept-configuration-agreements` and `--disable-interactivity`.
Note: `--accept-package-agreements` is NOT a valid flag on
`winget configure` (only on `winget install`). Package-agreement
consent for packages installed by DSC resources flows through
`--accept-configuration-agreements`.
2. Re-read machine+user PATH from the registry into `$env:Path` so the
caller's *current* PowerShell session can see freshly installed
executables (winget updates the registry but not running processes).
3. Assert each command in `-RequireCommands` resolves on PATH.
4. Print `INSTALL_OK: <Id>` as the final line; CI asserts on this.

.PARAMETER Id
Flow id, only used in log prefixes and the final sentinel line.

.PARAMETER ConfigFile
Path to the winget DSC YAML config for the flow.

.PARAMETER RequireCommands
Commands that must resolve on PATH after configuration has been applied.
#>

[CmdletBinding()]
param(
[Parameter(Mandatory)] [string] $Id,
[Parameter(Mandatory)] [string] $ConfigFile,
# AllowEmptyCollection: Windows PowerShell 5.1 rejects empty arrays
# bound to Mandatory parameters. Some flows (e.g. mac-comfort-shell)
# have no post-install CLI to verify - the DSC only installs a font
# and pwsh - so they legitimately pass @() here.
[Parameter(Mandatory)] [AllowEmptyCollection()] [string[]] $RequireCommands
)

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

# Fix #15: force UTF-8 on this process's console + external-program
# pipes. Windows PowerShell 5.1 defaults to the ANSI code page (1252 on
# en-US) for `[Console]::OutputEncoding`, which mangles winget's
# braille-pattern spinner glyphs into scrolling mojibake. winget writes
# UTF-8; matching it up front lets the carriage-return overwrites in
# the spinner work as intended and gives readable progress output.
# Safe no-op on pwsh 7 where UTF-8 is already the default.
try {
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
[Console]::OutputEncoding = $utf8NoBom
$OutputEncoding = $utf8NoBom
} catch {
# Some hosts (e.g. certain CI agents with redirected stdout) reject
# the assignment. Not worth failing the whole flow over cosmetics.
Write-Verbose "Could not force UTF-8 console encoding: $($_.Exception.Message)"
}

$common = Split-Path -Parent $PSCommandPath
. (Join-Path $common 'invoke-retry.ps1')

# Hard-fail fast if `winget configure` isn't available on this host. Every
# flow in this repo -- and the CmdPal extension that launches them -- uses
# `winget configure` as its only install path, so this is a stop-the-world
# prerequisite, not a warn-and-continue diagnostic.
#
# Fix #16: if the assert fails on first try, auto-invoke the canonical
# remediation (`enable-winget-configure.ps1`) once and then re-assert.
# The remediation runs `winget configure --enable` and installs
# Microsoft.VCRedist.2015+.x64, which covers the two failure modes a
# fresh VM actually hits. The remediation script itself self-elevates
# via UAC when needed; when we're already elevated (the install.ps1
# entry point in practice) it runs in-process and does not pause.
$assertScript = Join-Path $common 'assert-winget-configure.ps1'
$enableScript = Join-Path $common 'enable-winget-configure.ps1'
try {
& $assertScript
}
catch {
Write-Host ''
Write-Host "--- winget configure not available; auto-remediating via $enableScript ---" -ForegroundColor Yellow
Write-Host " (reason: $($_.Exception.Message.Split([Environment]::NewLine)[0]))" -ForegroundColor DarkGray
Write-Host ''
& $enableScript
# Re-assert; surface the original failure mode if remediation did
# not actually fix it.
& $assertScript
}

if (-not (Test-Path -LiteralPath $ConfigFile)) {
throw "DSC config file not found: $ConfigFile"
}

Write-Host "--- $Id flow: winget configure --file $ConfigFile ---"

Invoke-Retry -Name "winget configure $Id" -ScriptBlock {
winget configure `
--file $ConfigFile `
--accept-configuration-agreements `
--disable-interactivity
if ($LASTEXITCODE -ne 0) {
throw "winget configure failed with exit code $LASTEXITCODE"
}
}

# winget updates the registry copy of PATH but not the PATH of this already
# running PowerShell process. Rehydrate so subsequent CI steps see new tools.
& (Join-Path $common 'refresh-path.ps1')

foreach ($cmd in $RequireCommands) {
if (-not (Get-Command $cmd -ErrorAction SilentlyContinue)) {
throw "$cmd not found on PATH after applying $ConfigFile"
}
Write-Host "$cmd : $(& $cmd --version 2>&1 | Select-Object -First 1)"
}

Write-Host "INSTALL_OK: $Id"
154 changes: 154 additions & 0 deletions WindowsDevScripts/scripts/windows/_common/assert-winget-configure.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
ο»Ώ<#
.SYNOPSIS
Hard-fail preflight: assert that `winget configure` is available on this
host. Every Windows flow in this repo -- and the CmdPal extension that
launches them -- uses `winget configure` as its only install path. If it
isn't wired up, there is nothing useful to do but bail with an
actionable message.

.DESCRIPTION
Failure modes this catches, in order of likelihood:

1. winget (Microsoft.DesktopAppInstaller) is not installed at all, or
is too old / broken to expose the `configure` subcommand.
2. The `configuration` experimental feature flag is turned off in
`winget settings` (`experimentalFeatures.configuration = false`).
Only relevant on winget < 1.6; harmless to check on newer builds.
3. Group Policy / MDM has disabled configure via the ADMX policy
`EnableWindowsPackageManagerConfiguration` (registry value
`HKLM:\SOFTWARE\Policies\Microsoft\Windows\AppInstaller\
EnableWindowsPackageManagerConfiguration`, `0` = disabled).
4. Running in a non-interactive context where the AppInstaller COM
server cannot spin up (headless service accounts, SSH sessions
without a desktop). We can't always detect this -- we just surface
it as a fallback hint when the other checks pass but configure
still errors.

This script never "warns and continues" -- the whole point of this repo
is `winget configure`, so a failure here is a stop-the-world condition.

.PARAMETER Quiet
Suppress the "OK" line on success. Error output is always emitted.
#>

[CmdletBinding()]
param(
[switch] $Quiet
)

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

# UTF-8 console encoding, matching winget's output. Without this, a
# `winget configure --help` probe under Windows PowerShell 5.1 prints
# garbled glyphs in the error message we surface. See issue #15.
try {
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
[Console]::OutputEncoding = $utf8NoBom
$OutputEncoding = $utf8NoBom
} catch {
Write-Verbose "Could not force UTF-8 console encoding: $($_.Exception.Message)"
}

function Test-ConfigurePolicyAllowed {
# Returns $true if the GPO key is absent or set to anything other than 0.
# Returns $false ONLY when the key is explicitly 0 (disabled by policy).
$keyPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\AppInstaller'
try {
$val = (Get-ItemProperty -Path $keyPath -Name 'EnableWindowsPackageManagerConfiguration' -ErrorAction Stop).EnableWindowsPackageManagerConfiguration
return [int]$val -ne 0
} catch {
# Key or value absent => not policy-restricted.
return $true
}
}

# 1. winget itself must be present.
$wingetCmd = Get-Command winget -ErrorAction SilentlyContinue
if (-not $wingetCmd) {
throw @"
winget is not installed or not on PATH.

This repository's flows (and the CmdPal extension) require Windows Package
Manager (winget) with the `configure` subcommand. To fix:

1. Install / update 'App Installer' from the Microsoft Store, or
2. Grab the latest MSIX from
https://github.com/microsoft/winget-cli/releases/latest
(look for Microsoft.DesktopAppInstaller_*.msixbundle).

Then re-run your command.
"@
}

# 2. GPO / MDM check first -- cheapest, and its error message is the most
# actionable, so surface it before the subprocess call.
if (-not (Test-ConfigurePolicyAllowed)) {
throw @"
`winget configure` is disabled by Group Policy on this machine.

Registry key:
HKLM:\SOFTWARE\Policies\Microsoft\Windows\AppInstaller
EnableWindowsPackageManagerConfiguration = 0

This is ADMX policy 'Enable Windows Package Manager Configuration'
(Computer Configuration > Administrative Templates > Windows Components >
App Installer). If this is your box, set the policy to 'Enabled' (or
delete the value) and reboot. If the box is domain-managed, file a
ticket with IT -- every flow in this repo depends on configure being
allowed.
"@
}

# 3. Probe the configure subcommand itself. `--help` is a pure no-op that
# exits 0 iff the subcommand is wired up and the experimental flag
# (if required) is on.
$helpOutput = & winget configure --help 2>&1
$helpExit = $LASTEXITCODE

$looksRecognized = ($helpExit -eq 0) -and ($helpOutput -join "`n") -match '(?i)configuration|configure'

if (-not $looksRecognized) {
throw @"
`winget configure --help` did not succeed on this machine
(exit=$helpExit). winget itself is present ($($wingetCmd.Source)) but the
`configure` subcommand is not wired up.

Output from `winget configure --help`:
--------
$($helpOutput -join [Environment]::NewLine)
--------

To fix, run the canonical remediation script (elevates via UAC):

scripts\windows\_common\enable-winget-configure.ps1

It runs `winget configure --enable` and installs the required
Microsoft.VCRedist.2015+.x64 dependency, then re-verifies. The CmdPal
extension's red banner launches the same script. If you prefer to run
the steps by hand:

winget configure --enable
winget install -s winget --id Microsoft.VCRedist.2015+.x64 ``
--accept-package-agreements --accept-source-agreements

Still failing after the script? Likely causes:

1. App Installer itself is too old to know `--enable`. Update it:
winget source update
winget upgrade --id Microsoft.AppInstaller --accept-source-agreements --accept-package-agreements
or install the latest MSIX from
https://github.com/microsoft/winget-cli/releases/latest

2. You are running from a non-interactive session (SSH, Scheduled
Task 'run whether user is logged on or not', headless service
account). winget's configure path needs the AppInstaller COM
server, which requires an interactive desktop. Re-run from a
foreground PowerShell / Windows Terminal window.
"@
}

if (-not $Quiet) {
$ver = (& winget --version) 2>$null
Write-Host "winget configure: available ($ver)"
}
Loading
Loading