Skip to content

Add Microsoft .NET Runtime 8 & 10 as Windows FMAs#46877

Merged
allenhouchins merged 3 commits into
mainfrom
allenhouchins-net-runtime
Jun 5, 2026
Merged

Add Microsoft .NET Runtime 8 & 10 as Windows FMAs#46877
allenhouchins merged 3 commits into
mainfrom
allenhouchins-net-runtime

Conversation

@allenhouchins

@allenhouchins allenhouchins commented Jun 5, 2026

Copy link
Copy Markdown
Member

Add Winget input manifests for Microsoft .NET Runtime 8 and 10, plus a shared install script and separate uninstall scripts for each major version. Update outputs: register both runtimes in ee/maintained-apps/outputs/apps.json and add per-app windows.json files containing version metadata, installer URLs, sha256 checksums and script refs. These changes enable automated install/uninstall and version detection for .NET Runtime 8 and 10 in the maintained-apps catalog.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added support for installing and managing Microsoft .NET Runtime 10 and 8 on Windows
    • Added dedicated visual icon for .NET Runtime applications in the interface

Add Winget input manifests for Microsoft .NET Runtime 8 and 10, plus a shared install script and separate uninstall scripts for each major version. Update outputs: register both runtimes in ee/maintained-apps/outputs/apps.json and add per-app windows.json files containing version metadata, installer URLs, sha256 checksums and script refs. These changes enable automated install/uninstall and version detection for .NET Runtime 8 and 10 in the maintained-apps catalog.
fleet-release
fleet-release previously approved these changes Jun 5, 2026
Replace per-version uninstall scripts with a single generic microsoft_dotnet_runtime_uninstall.ps1 that targets the bundle by ProductCode (injected as PACKAGE_ID) and falls back to the Package Cache. Update winget input manifests for .NET Runtime 8 and 10 to reference the unified script, remove the old version-specific uninstall scripts, and update the output refs/hashes for both windows.json files to point to the new uninstall script implementations.
fleet-release
fleet-release previously approved these changes Jun 5, 2026
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/maintained-apps/outputs/microsoft-dotnet-runtime-10/windows.json

=== Install Script (no changes) ===
=== Uninstall // 68398bf2 -> b2eac15c ===

--- /tmp/old.ma5I1i	2026-06-05 04:58:52.324381424 +0000
+++ /tmp/new.2MAjft	2026-06-05 04:58:52.324381424 +0000
@@ -1,96 +1,73 @@
-# Locates the Microsoft .NET Runtime 10 (x64) WiX "burn" bundle in the registry
-# and runs its uninstaller silently.
+# Uninstalls the Microsoft .NET Runtime WiX "burn" bundle.
 #
-# Burn bundles register both UninstallString (e.g.
-#   "C:\ProgramData\Package Cache\{guid}\dotnet-runtime-10.0.x-win-x64.exe" /uninstall)
-# and, usually, QuietUninstallString (same, with /quiet appended). We prefer the
-# quiet form and otherwise append the silent switches ourselves.
-#
-# The DisplayName embeds the exact version ("Microsoft .NET Runtime - 10.0.8 (x64)"),
-# so we match on the major version + architecture.
-
-$displayNameLike = "Microsoft .NET Runtime - 10.* (x64)"
-$publisher = "Microsoft Corporation"
-
-$paths = @(
-  'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
-  'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+# The runtime installs as a burn bootstrapper that registers a *bundle* ARP entry
+# (keyed by the bundle ProductCode) alongside several MSI component entries that
+# share the same DisplayName. Only the bundle entry removes the whole runtime, and
+# it uninstalls by running its cached bootstrapper .exe with /uninstall -- never via
+# msiexec (see https://silentinstallhq.com/net-runtime-8-0-silent-uninstall-powershell/).
+# We target the bundle by its ProductCode (injected by the ingester) and fall back
+# to the cached bootstrapper in the Package Cache.
+
+$productCode = '{4443C461-ED45-4D3E-A6BB-3794C1B9FC6C}'
+
+function Invoke-Uninstaller {
+    param([string]$exe, [string]$exeArgs)
+    if ($exeArgs -notmatch '/uninstall') { $exeArgs = "/uninstall $exeArgs" }
+    if ($exeArgs -notmatch '/quiet')     { $exeArgs = "$exeArgs /quiet" }
+    if ($exeArgs -notmatch '/norestart') { $exeArgs = "$exeArgs /norestart" }
+    $exeArgs = $exeArgs.Trim()
+    Write-Host "Uninstall command: $exe"
+    Write-Host "Uninstall args: $exeArgs"
+    $process = Start-Process -FilePath $exe -ArgumentList $exeArgs -NoNewWindow -PassThru -Wait
+    return $process.ExitCode
+}
+
+$exitCode = $null
+
+# 1) Preferred: the bundle ARP entry, looked up by the bundle ProductCode. Its
+#    UninstallString/QuietUninstallString points to the cached bootstrapper .exe.
+$keys = @(
+  "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$productCode",
+  "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$productCode"
 )
 
-$entry = $null
-foreach ($p in $paths) {
-  $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
-    $_.DisplayName -like $displayNameLike -and $_.Publisher -like "$publisher*"
+foreach ($key in $keys) {
+  if (-not (Test-Path $key)) { continue }
+  $entry = Get-ItemProperty $key -ErrorAction SilentlyContinue
+  if (-not $entry) { continue }
+
+  $raw = $entry.QuietUninstallString
+  if (-not $raw) { $raw = $entry.UninstallString }
+  if (-not $raw) { continue }
+
+  # Parse into executable + args, handling quoted/unquoted/bare shapes.
+  if ($raw -match '^\s*"([^"]+)"\s*(.*)$') {
+      $exe = $matches[1]; $exeArgs = $matches[2].Trim()
+  } elseif ($raw -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
+      $exe = $matches[1]; $exeArgs = $matches[2].Trim()
+  } else {
+      $exe = $raw; $exeArgs = ""
   }
-  if ($items) { $entry = $items | Select-Object -First 1; break }
-}
 
-if (-not $entry) {
-  Write-Host "Uninstall entry not found"
-  Exit 0
+  $exitCode = Invoke-Uninstaller -exe $exe -exeArgs $exeArgs
+  break
 }
 
-# Prefer the vendor-provided quiet uninstall command when present.
-$raw = $entry.QuietUninstallString
-$needsSilentSwitches = $false
-if (-not $raw) {
-  $raw = $entry.UninstallString
-  $needsSilentSwitches = $true
+# 2) Fallback: run the cached bootstrapper directly from the Package Cache, which
+#    burn names after the bundle ProductCode.
+if ($null -eq $exitCode) {
+  $cached = Get-ChildItem -Path "C:\ProgramData\Package Cache\$productCode" -Filter *.exe -ErrorAction SilentlyContinue | Select-Object -First 1
+  if ($cached) {
+    $exitCode = Invoke-Uninstaller -exe $cached.FullName -exeArgs ""
+  }
 }
 
-if (-not $raw) {
-  Write-Host "No uninstall string found"
+if ($null -eq $exitCode) {
+  Write-Host "Uninstall entry not found for product code: $productCode"
   Exit 0
 }
 
-# Parse the command into an executable path and its arguments. Handle the three
-# common shapes: quoted path, unquoted path that may contain spaces, bare token.
-if ($raw -match '^\s*"([^"]+)"\s*(.*)$') {
-    $exe = $matches[1]
-    $exeArgs = $matches[2].Trim()
-} elseif ($raw -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
-    $exe = $matches[1]
-    $exeArgs = $matches[2].Trim()
-} else {
-    $exe = $raw
-    $exeArgs = ""
-}
-
-# Ensure silent + no-restart switches are present.
-if ($needsSilentSwitches -and $exeArgs -notmatch '/uninstall') {
-    $exeArgs = "/uninstall $exeArgs"
-}
-if ($exeArgs -notmatch '/quiet') {
-    $exeArgs = "$exeArgs /quiet"
-}
-if ($exeArgs -notmatch '/norestart') {
-    $exeArgs = "$exeArgs /norestart"
-}
-$exeArgs = $exeArgs.Trim()
-
-Write-Host "Uninstall command: $exe"
-Write-Host "Uninstall args: $exeArgs"
-
-try {
-    $processOptions = @{
-        FilePath = $exe
-        ArgumentList = $exeArgs
-        NoNewWindow = $true
-        PassThru = $true
-        Wait = $true
-    }
-
-    $process = Start-Process @processOptions
-    $exitCode = $process.ExitCode
-    Write-Host "Uninstall exit code: $exitCode"
-
-    # 0 = success, 3010 = success but reboot required, 1641 = reboot initiated
-    if ($exitCode -eq 3010 -or $exitCode -eq 1641) {
-        Exit 0
-    }
-
-    Exit $exitCode
-} catch {
-    Write-Host "Error running uninstaller: $_"
-    Exit 1
-}
+Write-Host "Uninstall exit code: $exitCode"
+# 0 = success, 3010 = success but reboot required, 1641 = reboot initiated
+if ($exitCode -eq 3010 -or $exitCode -eq 1641) { Exit 0 }
+Exit $exitCode

ee/maintained-apps/outputs/microsoft-dotnet-runtime-8/windows.json

=== Install Script (no changes) ===
=== Uninstall // b725f262 -> c507d96a ===

--- /tmp/old.RpPSfP	2026-06-05 04:58:52.362382245 +0000
+++ /tmp/new.A2ACry	2026-06-05 04:58:52.362382245 +0000
@@ -1,96 +1,73 @@
-# Locates the Microsoft .NET Runtime 8 (x64) WiX "burn" bundle in the registry
-# and runs its uninstaller silently.
+# Uninstalls the Microsoft .NET Runtime WiX "burn" bundle.
 #
-# Burn bundles register both UninstallString (e.g.
-#   "C:\ProgramData\Package Cache\{guid}\dotnet-runtime-8.0.x-win-x64.exe" /uninstall)
-# and, usually, QuietUninstallString (same, with /quiet appended). We prefer the
-# quiet form and otherwise append the silent switches ourselves.
-#
-# The DisplayName embeds the exact version ("Microsoft .NET Runtime - 8.0.27 (x64)"),
-# so we match on the major version + architecture.
-
-$displayNameLike = "Microsoft .NET Runtime - 8.* (x64)"
-$publisher = "Microsoft Corporation"
-
-$paths = @(
-  'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
-  'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+# The runtime installs as a burn bootstrapper that registers a *bundle* ARP entry
+# (keyed by the bundle ProductCode) alongside several MSI component entries that
+# share the same DisplayName. Only the bundle entry removes the whole runtime, and
+# it uninstalls by running its cached bootstrapper .exe with /uninstall -- never via
+# msiexec (see https://silentinstallhq.com/net-runtime-8-0-silent-uninstall-powershell/).
+# We target the bundle by its ProductCode (injected by the ingester) and fall back
+# to the cached bootstrapper in the Package Cache.
+
+$productCode = '{785c0558-c01e-44e3-8799-90ea5ea70ae6}'
+
+function Invoke-Uninstaller {
+    param([string]$exe, [string]$exeArgs)
+    if ($exeArgs -notmatch '/uninstall') { $exeArgs = "/uninstall $exeArgs" }
+    if ($exeArgs -notmatch '/quiet')     { $exeArgs = "$exeArgs /quiet" }
+    if ($exeArgs -notmatch '/norestart') { $exeArgs = "$exeArgs /norestart" }
+    $exeArgs = $exeArgs.Trim()
+    Write-Host "Uninstall command: $exe"
+    Write-Host "Uninstall args: $exeArgs"
+    $process = Start-Process -FilePath $exe -ArgumentList $exeArgs -NoNewWindow -PassThru -Wait
+    return $process.ExitCode
+}
+
+$exitCode = $null
+
+# 1) Preferred: the bundle ARP entry, looked up by the bundle ProductCode. Its
+#    UninstallString/QuietUninstallString points to the cached bootstrapper .exe.
+$keys = @(
+  "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$productCode",
+  "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$productCode"
 )
 
-$entry = $null
-foreach ($p in $paths) {
-  $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
-    $_.DisplayName -like $displayNameLike -and $_.Publisher -like "$publisher*"
+foreach ($key in $keys) {
+  if (-not (Test-Path $key)) { continue }
+  $entry = Get-ItemProperty $key -ErrorAction SilentlyContinue
+  if (-not $entry) { continue }
+
+  $raw = $entry.QuietUninstallString
+  if (-not $raw) { $raw = $entry.UninstallString }
+  if (-not $raw) { continue }
+
+  # Parse into executable + args, handling quoted/unquoted/bare shapes.
+  if ($raw -match '^\s*"([^"]+)"\s*(.*)$') {
+      $exe = $matches[1]; $exeArgs = $matches[2].Trim()
+  } elseif ($raw -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
+      $exe = $matches[1]; $exeArgs = $matches[2].Trim()
+  } else {
+      $exe = $raw; $exeArgs = ""
   }
-  if ($items) { $entry = $items | Select-Object -First 1; break }
-}
 
-if (-not $entry) {
-  Write-Host "Uninstall entry not found"
-  Exit 0
+  $exitCode = Invoke-Uninstaller -exe $exe -exeArgs $exeArgs
+  break
 }
 
-# Prefer the vendor-provided quiet uninstall command when present.
-$raw = $entry.QuietUninstallString
-$needsSilentSwitches = $false
-if (-not $raw) {
-  $raw = $entry.UninstallString
-  $needsSilentSwitches = $true
+# 2) Fallback: run the cached bootstrapper directly from the Package Cache, which
+#    burn names after the bundle ProductCode.
+if ($null -eq $exitCode) {
+  $cached = Get-ChildItem -Path "C:\ProgramData\Package Cache\$productCode" -Filter *.exe -ErrorAction SilentlyContinue | Select-Object -First 1
+  if ($cached) {
+    $exitCode = Invoke-Uninstaller -exe $cached.FullName -exeArgs ""
+  }
 }
 
-if (-not $raw) {
-  Write-Host "No uninstall string found"
+if ($null -eq $exitCode) {
+  Write-Host "Uninstall entry not found for product code: $productCode"
   Exit 0
 }
 
-# Parse the command into an executable path and its arguments. Handle the three
-# common shapes: quoted path, unquoted path that may contain spaces, bare token.
-if ($raw -match '^\s*"([^"]+)"\s*(.*)$') {
-    $exe = $matches[1]
-    $exeArgs = $matches[2].Trim()
-} elseif ($raw -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
-    $exe = $matches[1]
-    $exeArgs = $matches[2].Trim()
-} else {
-    $exe = $raw
-    $exeArgs = ""
-}
-
-# Ensure silent + no-restart switches are present.
-if ($needsSilentSwitches -and $exeArgs -notmatch '/uninstall') {
-    $exeArgs = "/uninstall $exeArgs"
-}
-if ($exeArgs -notmatch '/quiet') {
-    $exeArgs = "$exeArgs /quiet"
-}
-if ($exeArgs -notmatch '/norestart') {
-    $exeArgs = "$exeArgs /norestart"
-}
-$exeArgs = $exeArgs.Trim()
-
-Write-Host "Uninstall command: $exe"
-Write-Host "Uninstall args: $exeArgs"
-
-try {
-    $processOptions = @{
-        FilePath = $exe
-        ArgumentList = $exeArgs
-        NoNewWindow = $true
-        PassThru = $true
-        Wait = $true
-    }
-
-    $process = Start-Process @processOptions
-    $exitCode = $process.ExitCode
-    Write-Host "Uninstall exit code: $exitCode"
-
-    # 0 = success, 3010 = success but reboot required, 1641 = reboot initiated
-    if ($exitCode -eq 3010 -or $exitCode -eq 1641) {
-        Exit 0
-    }
-
-    Exit $exitCode
-} catch {
-    Write-Host "Error running uninstaller: $_"
-    Exit 1
-}
+Write-Host "Uninstall exit code: $exitCode"
+# 0 = success, 3010 = success but reboot required, 1641 = reboot initiated
+if ($exitCode -eq 3010 -or $exitCode -eq 1641) { Exit 0 }
+Exit $exitCode

Introduce a new MicrosoftDotnetRuntime React SVG component that inlines the runtime icon (base64 PNG) at frontend/pages/SoftwarePage/components/icons/MicrosoftDotnetRuntime.tsx. Update icons index to import the new component and map the software name "microsoft .net runtime" to it. Add corresponding high-resolution PNG assets to website/assets/images (app-icon-microsoft-dotnet-runtime-8/10-60x60@2x.png). This adds the runtime icon to the app's icon set for display on the Software page.
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/maintained-apps/outputs/microsoft-dotnet-runtime-10/windows.json

=== Install Script (no changes) ===
=== Uninstall // 68398bf2 -> b2eac15c ===

--- /tmp/old.WeiBNl	2026-06-05 05:15:43.965314727 +0000
+++ /tmp/new.nnxSmL	2026-06-05 05:15:43.965314727 +0000
@@ -1,96 +1,73 @@
-# Locates the Microsoft .NET Runtime 10 (x64) WiX "burn" bundle in the registry
-# and runs its uninstaller silently.
+# Uninstalls the Microsoft .NET Runtime WiX "burn" bundle.
 #
-# Burn bundles register both UninstallString (e.g.
-#   "C:\ProgramData\Package Cache\{guid}\dotnet-runtime-10.0.x-win-x64.exe" /uninstall)
-# and, usually, QuietUninstallString (same, with /quiet appended). We prefer the
-# quiet form and otherwise append the silent switches ourselves.
-#
-# The DisplayName embeds the exact version ("Microsoft .NET Runtime - 10.0.8 (x64)"),
-# so we match on the major version + architecture.
-
-$displayNameLike = "Microsoft .NET Runtime - 10.* (x64)"
-$publisher = "Microsoft Corporation"
-
-$paths = @(
-  'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
-  'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+# The runtime installs as a burn bootstrapper that registers a *bundle* ARP entry
+# (keyed by the bundle ProductCode) alongside several MSI component entries that
+# share the same DisplayName. Only the bundle entry removes the whole runtime, and
+# it uninstalls by running its cached bootstrapper .exe with /uninstall -- never via
+# msiexec (see https://silentinstallhq.com/net-runtime-8-0-silent-uninstall-powershell/).
+# We target the bundle by its ProductCode (injected by the ingester) and fall back
+# to the cached bootstrapper in the Package Cache.
+
+$productCode = '{4443C461-ED45-4D3E-A6BB-3794C1B9FC6C}'
+
+function Invoke-Uninstaller {
+    param([string]$exe, [string]$exeArgs)
+    if ($exeArgs -notmatch '/uninstall') { $exeArgs = "/uninstall $exeArgs" }
+    if ($exeArgs -notmatch '/quiet')     { $exeArgs = "$exeArgs /quiet" }
+    if ($exeArgs -notmatch '/norestart') { $exeArgs = "$exeArgs /norestart" }
+    $exeArgs = $exeArgs.Trim()
+    Write-Host "Uninstall command: $exe"
+    Write-Host "Uninstall args: $exeArgs"
+    $process = Start-Process -FilePath $exe -ArgumentList $exeArgs -NoNewWindow -PassThru -Wait
+    return $process.ExitCode
+}
+
+$exitCode = $null
+
+# 1) Preferred: the bundle ARP entry, looked up by the bundle ProductCode. Its
+#    UninstallString/QuietUninstallString points to the cached bootstrapper .exe.
+$keys = @(
+  "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$productCode",
+  "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$productCode"
 )
 
-$entry = $null
-foreach ($p in $paths) {
-  $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
-    $_.DisplayName -like $displayNameLike -and $_.Publisher -like "$publisher*"
+foreach ($key in $keys) {
+  if (-not (Test-Path $key)) { continue }
+  $entry = Get-ItemProperty $key -ErrorAction SilentlyContinue
+  if (-not $entry) { continue }
+
+  $raw = $entry.QuietUninstallString
+  if (-not $raw) { $raw = $entry.UninstallString }
+  if (-not $raw) { continue }
+
+  # Parse into executable + args, handling quoted/unquoted/bare shapes.
+  if ($raw -match '^\s*"([^"]+)"\s*(.*)$') {
+      $exe = $matches[1]; $exeArgs = $matches[2].Trim()
+  } elseif ($raw -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
+      $exe = $matches[1]; $exeArgs = $matches[2].Trim()
+  } else {
+      $exe = $raw; $exeArgs = ""
   }
-  if ($items) { $entry = $items | Select-Object -First 1; break }
-}
 
-if (-not $entry) {
-  Write-Host "Uninstall entry not found"
-  Exit 0
+  $exitCode = Invoke-Uninstaller -exe $exe -exeArgs $exeArgs
+  break
 }
 
-# Prefer the vendor-provided quiet uninstall command when present.
-$raw = $entry.QuietUninstallString
-$needsSilentSwitches = $false
-if (-not $raw) {
-  $raw = $entry.UninstallString
-  $needsSilentSwitches = $true
+# 2) Fallback: run the cached bootstrapper directly from the Package Cache, which
+#    burn names after the bundle ProductCode.
+if ($null -eq $exitCode) {
+  $cached = Get-ChildItem -Path "C:\ProgramData\Package Cache\$productCode" -Filter *.exe -ErrorAction SilentlyContinue | Select-Object -First 1
+  if ($cached) {
+    $exitCode = Invoke-Uninstaller -exe $cached.FullName -exeArgs ""
+  }
 }
 
-if (-not $raw) {
-  Write-Host "No uninstall string found"
+if ($null -eq $exitCode) {
+  Write-Host "Uninstall entry not found for product code: $productCode"
   Exit 0
 }
 
-# Parse the command into an executable path and its arguments. Handle the three
-# common shapes: quoted path, unquoted path that may contain spaces, bare token.
-if ($raw -match '^\s*"([^"]+)"\s*(.*)$') {
-    $exe = $matches[1]
-    $exeArgs = $matches[2].Trim()
-} elseif ($raw -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
-    $exe = $matches[1]
-    $exeArgs = $matches[2].Trim()
-} else {
-    $exe = $raw
-    $exeArgs = ""
-}
-
-# Ensure silent + no-restart switches are present.
-if ($needsSilentSwitches -and $exeArgs -notmatch '/uninstall') {
-    $exeArgs = "/uninstall $exeArgs"
-}
-if ($exeArgs -notmatch '/quiet') {
-    $exeArgs = "$exeArgs /quiet"
-}
-if ($exeArgs -notmatch '/norestart') {
-    $exeArgs = "$exeArgs /norestart"
-}
-$exeArgs = $exeArgs.Trim()
-
-Write-Host "Uninstall command: $exe"
-Write-Host "Uninstall args: $exeArgs"
-
-try {
-    $processOptions = @{
-        FilePath = $exe
-        ArgumentList = $exeArgs
-        NoNewWindow = $true
-        PassThru = $true
-        Wait = $true
-    }
-
-    $process = Start-Process @processOptions
-    $exitCode = $process.ExitCode
-    Write-Host "Uninstall exit code: $exitCode"
-
-    # 0 = success, 3010 = success but reboot required, 1641 = reboot initiated
-    if ($exitCode -eq 3010 -or $exitCode -eq 1641) {
-        Exit 0
-    }
-
-    Exit $exitCode
-} catch {
-    Write-Host "Error running uninstaller: $_"
-    Exit 1
-}
+Write-Host "Uninstall exit code: $exitCode"
+# 0 = success, 3010 = success but reboot required, 1641 = reboot initiated
+if ($exitCode -eq 3010 -or $exitCode -eq 1641) { Exit 0 }
+Exit $exitCode

ee/maintained-apps/outputs/microsoft-dotnet-runtime-8/windows.json

=== Install Script (no changes) ===
=== Uninstall // b725f262 -> c507d96a ===

--- /tmp/old.6Iqhnx	2026-06-05 05:15:44.022314373 +0000
+++ /tmp/new.vOTTaO	2026-06-05 05:15:44.022314373 +0000
@@ -1,96 +1,73 @@
-# Locates the Microsoft .NET Runtime 8 (x64) WiX "burn" bundle in the registry
-# and runs its uninstaller silently.
+# Uninstalls the Microsoft .NET Runtime WiX "burn" bundle.
 #
-# Burn bundles register both UninstallString (e.g.
-#   "C:\ProgramData\Package Cache\{guid}\dotnet-runtime-8.0.x-win-x64.exe" /uninstall)
-# and, usually, QuietUninstallString (same, with /quiet appended). We prefer the
-# quiet form and otherwise append the silent switches ourselves.
-#
-# The DisplayName embeds the exact version ("Microsoft .NET Runtime - 8.0.27 (x64)"),
-# so we match on the major version + architecture.
-
-$displayNameLike = "Microsoft .NET Runtime - 8.* (x64)"
-$publisher = "Microsoft Corporation"
-
-$paths = @(
-  'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
-  'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+# The runtime installs as a burn bootstrapper that registers a *bundle* ARP entry
+# (keyed by the bundle ProductCode) alongside several MSI component entries that
+# share the same DisplayName. Only the bundle entry removes the whole runtime, and
+# it uninstalls by running its cached bootstrapper .exe with /uninstall -- never via
+# msiexec (see https://silentinstallhq.com/net-runtime-8-0-silent-uninstall-powershell/).
+# We target the bundle by its ProductCode (injected by the ingester) and fall back
+# to the cached bootstrapper in the Package Cache.
+
+$productCode = '{785c0558-c01e-44e3-8799-90ea5ea70ae6}'
+
+function Invoke-Uninstaller {
+    param([string]$exe, [string]$exeArgs)
+    if ($exeArgs -notmatch '/uninstall') { $exeArgs = "/uninstall $exeArgs" }
+    if ($exeArgs -notmatch '/quiet')     { $exeArgs = "$exeArgs /quiet" }
+    if ($exeArgs -notmatch '/norestart') { $exeArgs = "$exeArgs /norestart" }
+    $exeArgs = $exeArgs.Trim()
+    Write-Host "Uninstall command: $exe"
+    Write-Host "Uninstall args: $exeArgs"
+    $process = Start-Process -FilePath $exe -ArgumentList $exeArgs -NoNewWindow -PassThru -Wait
+    return $process.ExitCode
+}
+
+$exitCode = $null
+
+# 1) Preferred: the bundle ARP entry, looked up by the bundle ProductCode. Its
+#    UninstallString/QuietUninstallString points to the cached bootstrapper .exe.
+$keys = @(
+  "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$productCode",
+  "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$productCode"
 )
 
-$entry = $null
-foreach ($p in $paths) {
-  $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
-    $_.DisplayName -like $displayNameLike -and $_.Publisher -like "$publisher*"
+foreach ($key in $keys) {
+  if (-not (Test-Path $key)) { continue }
+  $entry = Get-ItemProperty $key -ErrorAction SilentlyContinue
+  if (-not $entry) { continue }
+
+  $raw = $entry.QuietUninstallString
+  if (-not $raw) { $raw = $entry.UninstallString }
+  if (-not $raw) { continue }
+
+  # Parse into executable + args, handling quoted/unquoted/bare shapes.
+  if ($raw -match '^\s*"([^"]+)"\s*(.*)$') {
+      $exe = $matches[1]; $exeArgs = $matches[2].Trim()
+  } elseif ($raw -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
+      $exe = $matches[1]; $exeArgs = $matches[2].Trim()
+  } else {
+      $exe = $raw; $exeArgs = ""
   }
-  if ($items) { $entry = $items | Select-Object -First 1; break }
-}
 
-if (-not $entry) {
-  Write-Host "Uninstall entry not found"
-  Exit 0
+  $exitCode = Invoke-Uninstaller -exe $exe -exeArgs $exeArgs
+  break
 }
 
-# Prefer the vendor-provided quiet uninstall command when present.
-$raw = $entry.QuietUninstallString
-$needsSilentSwitches = $false
-if (-not $raw) {
-  $raw = $entry.UninstallString
-  $needsSilentSwitches = $true
+# 2) Fallback: run the cached bootstrapper directly from the Package Cache, which
+#    burn names after the bundle ProductCode.
+if ($null -eq $exitCode) {
+  $cached = Get-ChildItem -Path "C:\ProgramData\Package Cache\$productCode" -Filter *.exe -ErrorAction SilentlyContinue | Select-Object -First 1
+  if ($cached) {
+    $exitCode = Invoke-Uninstaller -exe $cached.FullName -exeArgs ""
+  }
 }
 
-if (-not $raw) {
-  Write-Host "No uninstall string found"
+if ($null -eq $exitCode) {
+  Write-Host "Uninstall entry not found for product code: $productCode"
   Exit 0
 }
 
-# Parse the command into an executable path and its arguments. Handle the three
-# common shapes: quoted path, unquoted path that may contain spaces, bare token.
-if ($raw -match '^\s*"([^"]+)"\s*(.*)$') {
-    $exe = $matches[1]
-    $exeArgs = $matches[2].Trim()
-} elseif ($raw -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
-    $exe = $matches[1]
-    $exeArgs = $matches[2].Trim()
-} else {
-    $exe = $raw
-    $exeArgs = ""
-}
-
-# Ensure silent + no-restart switches are present.
-if ($needsSilentSwitches -and $exeArgs -notmatch '/uninstall') {
-    $exeArgs = "/uninstall $exeArgs"
-}
-if ($exeArgs -notmatch '/quiet') {
-    $exeArgs = "$exeArgs /quiet"
-}
-if ($exeArgs -notmatch '/norestart') {
-    $exeArgs = "$exeArgs /norestart"
-}
-$exeArgs = $exeArgs.Trim()
-
-Write-Host "Uninstall command: $exe"
-Write-Host "Uninstall args: $exeArgs"
-
-try {
-    $processOptions = @{
-        FilePath = $exe
-        ArgumentList = $exeArgs
-        NoNewWindow = $true
-        PassThru = $true
-        Wait = $true
-    }
-
-    $process = Start-Process @processOptions
-    $exitCode = $process.ExitCode
-    Write-Host "Uninstall exit code: $exitCode"
-
-    # 0 = success, 3010 = success but reboot required, 1641 = reboot initiated
-    if ($exitCode -eq 3010 -or $exitCode -eq 1641) {
-        Exit 0
-    }
-
-    Exit $exitCode
-} catch {
-    Write-Host "Error running uninstaller: $_"
-    Exit 1
-}
+Write-Host "Uninstall exit code: $exitCode"
+# 0 = success, 3010 = success but reboot required, 1641 = reboot initiated
+if ($exitCode -eq 3010 -or $exitCode -eq 1641) { Exit 0 }
+Exit $exitCode

@codecov

codecov Bot commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 66.96%. Comparing base (7a78d05) to head (94b0de5).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...rePage/components/icons/MicrosoftDotnetRuntime.tsx 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #46877      +/-   ##
==========================================
- Coverage   66.96%   66.96%   -0.01%     
==========================================
  Files        2856     2857       +1     
  Lines      225672   225674       +2     
  Branches    11592    11592              
==========================================
+ Hits       151126   151127       +1     
- Misses      60840    60841       +1     
  Partials    13706    13706              
Flag Coverage Δ
frontend 56.96% <50.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@allenhouchins allenhouchins marked this pull request as ready for review June 5, 2026 05:22
@allenhouchins allenhouchins requested a review from a team as a code owner June 5, 2026 05:22
Copilot AI review requested due to automatic review settings June 5, 2026 05:22
@fleet-release fleet-release requested a review from eashaw June 5, 2026 05:22

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@allenhouchins allenhouchins merged commit 10a47c8 into main Jun 5, 2026
28 of 29 checks passed
@allenhouchins allenhouchins deleted the allenhouchins-net-runtime branch June 5, 2026 05:22
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6fdfb9ea-1c49-4d3e-b4bb-944519567cb4

📥 Commits

Reviewing files that changed from the base of the PR and between 7a78d05 and 94b0de5.

⛔ Files ignored due to path filters (2)
  • website/assets/images/app-icon-microsoft-dotnet-runtime-10-60x60@2x.png is excluded by !**/*.png
  • website/assets/images/app-icon-microsoft-dotnet-runtime-8-60x60@2x.png is excluded by !**/*.png
📒 Files selected for processing (9)
  • ee/maintained-apps/inputs/winget/microsoft-dotnet-runtime-10.json
  • ee/maintained-apps/inputs/winget/microsoft-dotnet-runtime-8.json
  • ee/maintained-apps/inputs/winget/scripts/microsoft_dotnet_runtime_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/microsoft_dotnet_runtime_uninstall.ps1
  • ee/maintained-apps/outputs/apps.json
  • ee/maintained-apps/outputs/microsoft-dotnet-runtime-10/windows.json
  • ee/maintained-apps/outputs/microsoft-dotnet-runtime-8/windows.json
  • frontend/pages/SoftwarePage/components/icons/MicrosoftDotnetRuntime.tsx
  • frontend/pages/SoftwarePage/components/icons/index.ts

Walkthrough

This PR adds managed deployment support for Microsoft .NET Runtime versions 8.0.27 and 10.0.8. It introduces Winget input definitions specifying package identifiers and detection queries, creates shared PowerShell scripts for silent WiX burn bundle installation and uninstallation with exit-code handling, generates output configurations with SQL predicates for version detection and installer metadata, registers both apps in the system registry, and adds a frontend icon component with UI integration for icon lookup by software name.

Possibly related PRs

  • fleetdm/fleet#42397: The new Winget input JSONs define per-app exists_query values that depend on the winget ingester schema support for ExistsQuery overrides.
  • fleetdm/fleet#46484: Both PRs add maintained-app Winget entries and update the shared SOFTWARE_NAME_TO_ICON_MAP in frontend/pages/SoftwarePage/components/icons/index.ts to register new software icons.
  • fleetdm/fleet#46497: Both PRs update the same SOFTWARE_NAME_TO_ICON_MAP icon registry by adding new software-name entries for icon resolution.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch allenhouchins-net-runtime

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds Windows Fleet-maintained apps (FMAs) for Microsoft .NET Runtime 8 and 10, including winget input manifests, install/uninstall PowerShell scripts, and generated output manifests/registry entries so Fleet can install/uninstall and detect versions of these runtimes.

Changes:

  • Add a new Software page icon for “Microsoft .NET Runtime” and map it for fuzzy name matching.
  • Add winget input manifests + shared install/uninstall scripts for .NET Runtime 8 and 10.
  • Add generated maintained-app outputs (apps.json registration + per-app windows.json manifests with versions, queries, URLs, hashes, and script refs).

Reviewed changes

Copilot reviewed 9 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
frontend/pages/SoftwarePage/components/icons/MicrosoftDotnetRuntime.tsx Adds a new icon component for .NET Runtime.
frontend/pages/SoftwarePage/components/icons/index.ts Registers the new icon and maps it to “microsoft .net runtime”.
ee/maintained-apps/outputs/microsoft-dotnet-runtime-8/windows.json Adds generated Windows manifest output for .NET Runtime 8 (version, queries, scripts, hashes).
ee/maintained-apps/outputs/microsoft-dotnet-runtime-10/windows.json Adds generated Windows manifest output for .NET Runtime 10 (version, queries, scripts, hashes).
ee/maintained-apps/outputs/apps.json Registers both runtimes in the maintained-app catalog.
ee/maintained-apps/inputs/winget/scripts/microsoft_dotnet_runtime_uninstall.ps1 Adds a shared uninstall script template for burn bundles using ProductCode/Package Cache.
ee/maintained-apps/inputs/winget/scripts/microsoft_dotnet_runtime_install.ps1 Adds a shared silent install script for the runtime .exe.
ee/maintained-apps/inputs/winget/microsoft-dotnet-runtime-8.json Adds winget input manifest for .NET Runtime 8.
ee/maintained-apps/inputs/winget/microsoft-dotnet-runtime-10.json Adds winget input manifest for .NET Runtime 10.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

"exists_query": "SELECT 1 FROM programs WHERE name LIKE 'Microsoft .NET Runtime - 8.%' AND name LIKE '%(x64)' AND publisher = 'Microsoft Corporation';",
"installer_arch": "x64",
"installer_type": "exe",
"installer_scope": "",
"exists_query": "SELECT 1 FROM programs WHERE name LIKE 'Microsoft .NET Runtime - 10.%' AND name LIKE '%(x64)' AND publisher = 'Microsoft Corporation';",
"installer_arch": "x64",
"installer_type": "exe",
"installer_scope": "",
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants