Add PowerShell as a Windows FMA (#46941)

This pull request adds support for PowerShell as a managed Windows app,
including its metadata, versioning, install/uninstall scripts, and
validation logic in the CI workflow. It also ensures that any
pre-installed PowerShell 7 is removed from the test environment before
validation. The most important changes are:

**PowerShell app definition and metadata:**

* Added a new entry for PowerShell in
`ee/maintained-apps/inputs/winget/powershell.json` and included its
metadata in `ee/maintained-apps/outputs/apps.json`, enabling PowerShell
to be managed as a Windows app.
[[1]](diffhunk://#diff-7e4899a379d327948f55b7e6ba4c9893501713edd16020b128b68343569fcbfbR1-R10)
[[2]](diffhunk://#diff-4c1446cfc02c6bb0bda874481e333c65b84e184fcea52f656b49a6489f73c9c2R1985-R1991)
* Created `ee/maintained-apps/outputs/powershell/windows.json`
containing version information, SQL queries for existence and patch
status, installer/uninstaller scripts, and download URL for PowerShell
7.6.2.0.

**CI workflow enhancements for PowerShell:**

* Updated `.github/workflows/test-fma-windows-pr-only.yml` to detect
changes involving PowerShell, set the appropriate output flags, and
conditionally execute PowerShell-specific steps.
[[1]](diffhunk://#diff-51641fd1d2cc19348b81fd8310b62ad270ca5082ceddff2d49064e78f126a1eaR102)
[[2]](diffhunk://#diff-51641fd1d2cc19348b81fd8310b62ad270ca5082ceddff2d49064e78f126a1eaR116)
[[3]](diffhunk://#diff-51641fd1d2cc19348b81fd8310b62ad270ca5082ceddff2d49064e78f126a1eaR155-R162)
* Added a workflow step to remove any pre-installed PowerShell 7 from
the GitHub Actions runner, ensuring a clean environment for validation.
* Ensured that validation and related steps use Windows PowerShell 5.1
(not pwsh) after uninstalling PowerShell 7, to avoid shell availability
issues.

These changes collectively enable PowerShell to be validated, installed,
and uninstalled as part of the managed apps ecosystem and ensure
reliable, repeatable CI validation.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added PowerShell as a supported application with full Windows
integration, including installation and uninstall management
capabilities.
* Enhanced deployment scripts to support flexible input handling for app
configurations.
  * Added PowerShell icon to the frontend UI for visual identification.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Allen Houchins
2026-06-05 17:30:03 -05:00
committed by GitHub
parent 8ed163129a
commit 2b5e5049ed
8 changed files with 173 additions and 5 deletions
+13 -2
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# Script to filter apps.json to only include specified app slugs
# Usage: filter-apps-json.sh <slugs_json_array> <output_file>
# Usage: filter-apps-json.sh <slugs_json_array | slugs_json_file> <output_file>
set -euo pipefail
@@ -16,9 +16,20 @@ if ! command -v jq &> /dev/null; then
fi
# Parse arguments
SLUGS_JSON="$1"
SLUGS_INPUT="$1"
OUTPUT_FILE="$2"
# Accept the slugs as either a literal JSON array string or a path to a file containing
# the JSON array. The file form avoids cross-shell quoting problems: Windows PowerShell
# mangles embedded quotes when forwarding a JSON string as a native-command argument to
# bash, corrupting the value before jq sees it. Callers passing a literal JSON string
# (e.g. on macOS) are unaffected since that value is not a path to an existing file.
if [ -n "$SLUGS_INPUT" ] && [ -f "$SLUGS_INPUT" ]; then
SLUGS_JSON="$(cat "$SLUGS_INPUT")"
else
SLUGS_JSON="$SLUGS_INPUT"
fi
if [ -z "$SLUGS_JSON" ] || [ "$SLUGS_JSON" == "[]" ] || [ "$SLUGS_JSON" == "null" ]; then
echo "No slugs provided, creating empty apps.json"
echo '{"version": 2, "apps": []}' > "$OUTPUT_FILE"
+105 -3
View File
@@ -99,6 +99,7 @@ jobs:
"has_7zip=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"has_firefox=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"has_nodejs=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"has_powershell=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
exit 0
}
@@ -112,6 +113,7 @@ jobs:
"has_7zip=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"has_firefox=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"has_nodejs=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"has_powershell=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
Write-Host "No windows apps changed, skipping Windows workflow"
} else {
"has_windows_apps=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
@@ -150,6 +152,14 @@ jobs:
} else {
"has_nodejs=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
}
# Check if powershell/windows is in the changed apps
if ("powershell/windows" -in $windowsSlugs) {
"has_powershell=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
Write-Host "PowerShell detected in changed apps"
} else {
"has_powershell=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
}
}
shell: pwsh
@@ -405,6 +415,79 @@ jobs:
}
shell: pwsh
- name: Remove pre-installed PowerShell
if: steps.check-windows-apps.outputs.has_windows_apps == 'true' && steps.check-windows-apps.outputs.has_powershell == 'true'
# NOTE: this step (and the verify step below) run under Windows PowerShell 5.1
# (shell: powershell), NOT pwsh. We are about to uninstall PowerShell 7, so we
# must not be executing inside pwsh.exe (it would be locked / unavailable).
run: |
Write-Host "Listing all installed packages containing 'PowerShell':"
Get-Package | Where-Object { $_.Name -like "*PowerShell*" } | ForEach-Object {
Write-Host " - $($_.Name) (Version: $($_.Version))"
}
# PowerShell 7 installs via MSI and registers under "PowerShell 7-x64" /
# "Microsoft Corporation". GitHub-hosted windows runners ship with it
# pre-installed, which must be removed so the validator starts from a clean state.
$uninstallPaths = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)
$found = $false
foreach ($path in $uninstallPaths) {
$entries = Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "PowerShell 7*" -and $_.Publisher -like "*Microsoft Corporation*" }
foreach ($entry in $entries) {
if (-not $entry) { continue }
$found = $true
Write-Host "Found PowerShell uninstall entry: $($entry.DisplayName) (Version: $($entry.DisplayVersion))"
$uninstallString = if ($entry.QuietUninstallString) {
$entry.QuietUninstallString
} elseif ($entry.UninstallString) {
$entry.UninstallString
} else {
$null
}
if ($uninstallString) {
Write-Host "Found PowerShell uninstall path: $uninstallString"
try {
# PowerShell 7 uses an MSI uninstaller (MsiExec.exe /X{GUID} or /I{GUID})
if ($uninstallString -match "/[XI]\{([A-F0-9\-]+)\}") {
$guid = $matches[1]
Write-Host "Uninstalling PowerShell MSI with GUID: $guid"
Start-Process -FilePath "msiexec.exe" -ArgumentList "/X{$guid}", "/quiet", "/norestart" -Wait -NoNewWindow
Write-Host "Successfully removed PowerShell via MSI uninstaller"
} else {
Write-Host "Could not parse uninstall string format: $uninstallString"
}
} catch {
Write-Host "Failed to remove PowerShell: $($_.Exception.Message)"
}
} else {
Write-Host "PowerShell uninstall string not found in registry entry"
}
}
}
if (-not $found) {
Write-Host "PowerShell uninstall path not found in registry"
}
# Force-remove leftover PowerShell 7 directory in case files remain after MSI removal
$psDir = "C:\Program Files\PowerShell\7"
if (Test-Path $psDir) {
Write-Host "Removing leftover directory: $psDir"
Remove-Item -Path $psDir -Recurse -Force -ErrorAction SilentlyContinue
if (Test-Path $psDir) {
Write-Host "WARNING: Failed to fully remove $psDir"
} else {
Write-Host "Removed $psDir"
}
}
shell: powershell
- name: Filter apps.json and verify changed apps
if: steps.check-windows-apps.outputs.has_windows_apps == 'true'
run: |
@@ -415,16 +498,33 @@ jobs:
# Filter changed apps to only include windows platform
$changedAppsJson = '${{ steps.detect-changed.outputs.CHANGED_APPS }}'
$windowsSlugs = ($changedAppsJson | ConvertFrom-Json | Where-Object { $_ -like "*/windows" })
$windowsSlugsJson = ($windowsSlugs | ConvertTo-Json -Compress)
# Build a JSON array of the windows slugs. Construct it explicitly so a single
# slug still serializes as an array (Windows PowerShell unwraps single-element
# arrays via ConvertTo-Json), and write it to a BOM-free file. We then pass the
# file PATH -- not the JSON string -- to the bash script: forwarding a quoted
# JSON string across the PowerShell -> bash argument boundary mangles the
# embedded quotes under Windows PowerShell 5.1, which breaks jq --argjson.
$slugsArray = @($windowsSlugs)
$windowsSlugsJson = "[" + (($slugsArray | ForEach-Object { $_ | ConvertTo-Json -Compress }) -join ",") + "]"
Write-Host "Filtering apps.json for slugs: $windowsSlugsJson"
$windowsSlugsFile = Join-Path $env:TEMP "windows-slugs-$(New-Guid).json"
Set-Content -Path $windowsSlugsFile -Value $windowsSlugsJson -Encoding ascii -NoNewline
# Use forward slashes so Git Bash reads the path reliably (it reads this arg as a file).
$windowsSlugsFileForBash = $windowsSlugsFile -replace '\\', '/'
# Backup original apps.json
Copy-Item -Path "ee\maintained-apps\outputs\apps.json" -Destination "ee\maintained-apps\outputs\apps.json.backup"
# Create filtered apps.json
# Use a fixed path for the temp file to avoid issues with bash
$filteredAppsJson = Join-Path $env:TEMP "filtered-apps-$(New-Guid).json"
bash .github/scripts/filter-apps-json.sh "$windowsSlugsJson" "$filteredAppsJson"
bash .github/scripts/filter-apps-json.sh "$windowsSlugsFileForBash" "$filteredAppsJson"
if ($LASTEXITCODE -ne 0) {
Write-Host "Error: filter-apps-json.sh failed with exit code $LASTEXITCODE"
exit 1
}
# Verify the filtered file was created
if (-not (Test-Path $filteredAppsJson)) {
@@ -441,4 +541,6 @@ jobs:
# Restore original apps.json
Move-Item -Path "ee\maintained-apps\outputs\apps.json.backup" -Destination "ee\maintained-apps\outputs\apps.json" -Force
shell: pwsh
# Use Windows PowerShell 5.1 (not pwsh): when validating the PowerShell FMA we
# uninstall PowerShell 7 in the step above, so pwsh.exe may not be available here.
shell: powershell
@@ -0,0 +1,10 @@
{
"name": "PowerShell",
"slug": "powershell/windows",
"package_identifier": "Microsoft.PowerShell",
"unique_identifier": "PowerShell 7-x64",
"installer_arch": "x64",
"installer_type": "msi",
"installer_scope": "machine",
"default_categories": ["Developer tools"]
}
+7
View File
@@ -1982,6 +1982,13 @@
"unique_identifier": "Microsoft Power BI Desktop (x64)",
"description": "Power BI is an interactive data visualization platform with a primary focus on business intelligence (BI)."
},
{
"name": "PowerShell",
"slug": "powershell/windows",
"platform": "windows",
"unique_identifier": "PowerShell 7-x64",
"description": "PowerShell is a cross-platform task automation solution made up of a command-line shell, a scripting language, and a configuration management framework."
},
{
"name": "PowerToys",
"slug": "powertoys/windows",
@@ -0,0 +1,22 @@
{
"versions": [
{
"version": "7.6.2.0",
"queries": {
"exists": "SELECT 1 FROM programs WHERE name = 'PowerShell 7-x64' AND publisher = 'Microsoft Corporation';",
"patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = 'PowerShell 7-x64' AND publisher = 'Microsoft Corporation' AND version_compare(version, '7.6.2.0') < 0);"
},
"installer_url": "https://github.com/PowerShell/PowerShell/releases/download/v7.6.2/PowerShell-7.6.2-win-x64.msi",
"install_script_ref": "8959087b",
"uninstall_script_ref": "dccec067",
"sha256": "096a6dbb5bb330c5e14559ff1a7081bd274c07c07e2545755b93a93417e32629",
"default_categories": [
"Developer tools"
]
}
],
"refs": {
"8959087b": "$logFile = \"${env:TEMP}/fleet-install-software.log\"\n\ntry {\n\n$installProcess = Start-Process msiexec.exe `\n -ArgumentList \"/quiet /norestart /lv ${logFile} /i `\"${env:INSTALLER_PATH}`\"\" `\n -PassThru -Verb RunAs -Wait\n\nGet-Content $logFile -Tail 500\n\nExit $installProcess.ExitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n",
"dccec067": "$product_code = '{E5C8C749-88E5-48D7-9D16-4C41ED869462}'\n$timeoutSeconds = 300 # 5 minute timeout\n\n# Fleet uninstalls app using product code that's extracted on upload\n$process = Start-Process msiexec -ArgumentList @(\"/quiet\", \"/x\", $product_code, \"/norestart\") -PassThru\n\n# Wait for process with timeout\n$completed = $process.WaitForExit($timeoutSeconds * 1000)\n\nif (-not $completed) {\n Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue\n Exit 1603 # ERROR_UNINSTALL_FAILURE\n}\n\n# Check exit code and output result\nif ($process.ExitCode -eq 0) {\n Write-Output \"Exit 0\"\n Exit 0\n} else {\n Write-Output \"Exit $($process.ExitCode)\"\n Exit $process.ExitCode\n}\n"
}
}
File diff suppressed because one or more lines are too long
@@ -55,6 +55,7 @@ import PlantronicsHub from "./PlantronicsHub";
import PowerAutomate from "./PowerAutomate";
import PowerBi from "./PowerBi";
import Plugdata from "./Plugdata";
import Powershell from "./Powershell";
import Powertoys from "./Powertoys";
import Prisma from "./Prisma";
import Proxifier from "./Proxifier";
@@ -516,6 +517,7 @@ export const SOFTWARE_NAME_TO_ICON_MAP = {
postman: Postman,
"power automate": PowerAutomate,
"power bi": PowerBi,
powershell: Powershell,
powertoys: Powertoys,
prisma: Prisma,
privileges: Privileges,
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB