Skip to content

Add Azure Data Studio as a Windows FMA - #50027

Merged
allenhouchins merged 4 commits into
mainfrom
add-azure-data-studio-windows-fma
Jul 29, 2026
Merged

Add Azure Data Studio as a Windows FMA#50027
allenhouchins merged 4 commits into
mainfrom
add-azure-data-studio-windows-fma

Conversation

@kitzy

@kitzy kitzy commented Jul 28, 2026

Copy link
Copy Markdown
Member

Related issue: #50020

What this does

Adds Azure Data Studio as a Windows Fleet-maintained app. One of the 11 apps split out of #48501 that failed the FMA validator; #50016 shipped the 6 that passed.

Why it was failing

The install itself worked — the validator logged New application detected at: C:\Program Files\Azure Data Studio. The script never returned:

20:08:19  INFO  msg="Executing install script..." app="Azure Data Studio"
20:18:19  ERROR msg="Error executing install script: exit status 1"   # exactly 10:00 later
20:18:19  INFO  msg="New application detected at: C:\Program Files\Azure Data Studio"

Ten minutes on the nose is the validator's executeScript timeout. Azure Data Studio is a Visual Studio Code fork and ships the same Inno Setup script — including the runcode task, which launches the app when the install finishes. Because Start-Process -Wait waits for the process and all of its descendants, the launched app kept the script blocked forever.

The fix is the switch VS Code's own FMA already uses: /MERGETASKS=!runcode (see vscode_install.ps1 and vscodium_install.ps1, both of which pass validation). The script also waits on the installer process alone rather than its descendants, polls for the Add/Remove Programs entry, and stops a stray azuredatastudio process as a backstop in case a future build ignores the task suppression.

Notes

  • Machine-scope x64 installer, per the winget manifest — Azure Data Studio publishes both user and machine scope, and Fleet installs run as SYSTEM, so machine scope is required.
  • Clean ARP DisplayName (Azure Data Studio), so exact name matching. Publisher Microsoft Corporation.
  • Uninstall is unchanged: the Inno uninstaller doesn't leave anything resident, and -Wait waiting on descendants is the desired behavior there (Inno relaunches itself from %TEMP%).
  • Ships a new catalog icon and website asset.

Checklist for submitter

  • Input data is properly validated, SELECT * is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters.
  • Timeouts are implemented and retries are limited to avoid infinite loops

Testing

  • FMA CI validator (install → detect → uninstall) passes on the SYSTEM-context Windows runner — run 30384162280 (All checks passed)
  • Generated output verified locally: manifest SHA matches the winget manifest, exists/patched queries reviewed for name + publisher correctness, apps.json is valid JSON with a description filled in.
  • QA'd all new/changed functionality manually

Summary by CodeRabbit

  • New Features
    • Added Azure Data Studio to the available Windows software catalog.
    • Added support for installing and uninstalling Azure Data Studio (version 1.52.0) via silent installer and uninstaller flows with completion detection.
    • Added an Azure Data Studio icon to the software interface for better visual identification.

Azure Data Studio is a VS Code fork and ships the same Inno Setup script,
including the 'runcode' task that launches the app after install. Start-Process
-Wait waits for descendants as well as the process itself, so the launched app
kept the install script blocked and the validator killed it at its 10-minute cap.
Suppress the launch with /MERGETASKS=!runcode, as vscode_install.ps1 already
does, and wait on the installer process alone.
Copilot AI review requested due to automatic review settings July 28, 2026 00:11

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

Adds Azure Data Studio as a Windows Fleet-maintained app (winget input + generated output), including install/uninstall scripts tailored to avoid Inno Setup’s runcode auto-launch behavior on SYSTEM-context validation runners, and wires up a new catalog icon.

Changes:

  • Added Azure Data Studio Windows FMA input metadata and custom install/uninstall PowerShell scripts.
  • Generated the corresponding outputs/azure-data-studio/windows.json and added the app entry to outputs/apps.json.
  • Added a new software catalog icon component and mapped "azure data studio" to it.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
frontend/pages/SoftwarePage/components/icons/index.ts Imports and registers the Azure Data Studio icon in the software-name-to-icon map.
frontend/pages/SoftwarePage/components/icons/AzureDataStudio.tsx Adds a new icon component (embedded base64 PNG in an SVG wrapper).
ee/maintained-apps/outputs/azure-data-studio/windows.json New generated output manifest for Azure Data Studio Windows, including script refs and queries.
ee/maintained-apps/outputs/apps.json Adds Azure Data Studio to the maintained apps catalog list with description and slug.
ee/maintained-apps/inputs/winget/scripts/azure_data_studio_uninstall.ps1 New custom uninstall script (registry lookup + defensive parsing).
ee/maintained-apps/inputs/winget/scripts/azure_data_studio_install.ps1 New custom install script with task suppression and bounded wait + ARP polling.
ee/maintained-apps/inputs/winget/azure-data-studio.json New winget input definition (package id, scope/arch/type, script paths, categories).

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

Comment on lines +13 to +15
$installTimeoutSeconds = 600
$registrationTimeoutSeconds = 120

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch — this was a real bug, now fixed. The install cap is 420s and registration 120s, so the worst case stays under the 10-minute budget the caller enforces in cmd/maintained-apps/validate/windows.go.

As written it would have been killed part-way through the recovery path the script exists to provide, which is the worst possible place to lose the process. run 30372911136 passes.

Comment on lines +35 to +42
if (-not $process.WaitForExit($installTimeoutSeconds * 1000)) {
Write-Host "Installer process did not exit within ${installTimeoutSeconds}s, stopping it."
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
}

$exitCode = $process.ExitCode
Write-Host "Install exit code: $exitCode"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed. After Stop-Process the script now waits up to 30s for the kill to land and only reads .ExitCode when HasExited is true; otherwise it logs and falls through. Registration is treated as the authoritative signal in that case, since a killed process's exit code is meaningless — so a successful install can no longer be reported as a failure.

Comment on lines +17 to +22
$uninstallCommand = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString }
if ($uninstallCommand -match '^\s*"([^"]+)"\s*(.*)$') {
$uninstallCommand = $Matches[1]; if ($Matches[2]) { $uninstallArgs = "$($Matches[2]) $uninstallArgs".Trim() }
} elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
$uninstallCommand = $Matches[1]; if ($Matches[2]) { $uninstallArgs = "$($Matches[2]) $uninstallArgs".Trim() }
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Both added — the bare-token '^\s*(\S+)\s*(.*)$' fallback and -ErrorAction SilentlyContinue on the Get-ItemProperty call.

Separately, this script now also requires the publisher, mirroring the exists query. That is worth calling out because the validator's appExists looks up by name only, so a wrong exists-query publisher ships silently — the same class of bug found on Spyder in #50016. Requiring it in the uninstall puts the value under test, and it passes. Here it's Microsoft Corporation, verified against the installer's PE version resource (CompanyName).

Copilot AI review requested due to automatic review settings July 28, 2026 02:37

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

Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (3)

ee/maintained-apps/inputs/winget/scripts/azure_data_studio_install.ps1:14

  • The validator runs PowerShell scripts with a 10-minute context timeout (cmd/maintained-apps/validate/windows.go:291-299). With $installTimeoutSeconds=600 and $registrationTimeoutSeconds=120, this script can exceed the validator cap in the worst case (install wait hits 600s, then registration polling continues), which can reintroduce the original 10-minute timeout failure mode.
$installTimeoutSeconds = 600
$registrationTimeoutSeconds = 120

ee/maintained-apps/inputs/winget/scripts/azure_data_studio_install.ps1:39

  • After a timeout you attempt Stop-Process, but then immediately read $process.ExitCode. If Stop-Process fails (or the process hasn’t fully terminated yet), ExitCode can throw because the process is still running, causing the catch to return Exit 1 even if the app installed/registered. Safer to wait briefly after stopping and guard ExitCode behind HasExited, setting a fallback exit code if it’s still running.
if (-not $process.WaitForExit($installTimeoutSeconds * 1000)) {
  Write-Host "Installer process did not exit within ${installTimeoutSeconds}s, stopping it."
  Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
  Start-Sleep -Seconds 2
}

ee/maintained-apps/inputs/winget/scripts/azure_data_studio_uninstall.ps1:11

  • This registry enumeration uses Get-ItemProperty without -ErrorAction SilentlyContinue, so a single unreadable/unexpected key can throw and force the whole uninstall to Exit 1. Most other scripts suppress these per-key errors (e.g. windsurf_uninstall.ps1:15-19).
    [array]$uninstallKeys = Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue |
        ForEach-Object { Get-ItemProperty $_.PSPath }

$exitCode = $process.ExitCode; Write-Host "Uninstall exit code: $exitCode"; break
}
}
if (-not $foundUninstaller) { Write-Host "Uninstaller for '$softwareName' not found."; Exit 1 }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed and fixed — Exit 0 when no entry is found. I checked the convention across the directory rather than taking it on the two examples: 155 uninstall scripts exit 0 in that branch versus 21 that exit 1, so idempotent is clearly the house style. Applied to all the uninstall scripts in this batch.

Worth noting it doesn't weaken the test: if the app is still installed, the validator's own post-uninstall appExists check still fails the run.

],
"refs": {
"23fce27b": "$softwareName = \"Azure Data Studio\"\n$softwareNameLike = \"*$softwareName*\"\n$uninstallArgs = \"/VERYSILENT /SUPPRESSMSGBOXES /NORESTART\"\n\n$machineKey = 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$machineKey32on64 = 'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$exitCode = 0\n\ntry {\n [array]$uninstallKeys = Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath }\n\n $foundUninstaller = $false\n foreach ($key in $uninstallKeys) {\n if ($key.DisplayName -like $softwareNameLike) {\n $foundUninstaller = $true\n $uninstallCommand = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString }\n if ($uninstallCommand -match '^\\s*\"([^\"]+)\"\\s*(.*)$') {\n $uninstallCommand = $Matches[1]; if ($Matches[2]) { $uninstallArgs = \"$($Matches[2]) $uninstallArgs\".Trim() }\n } elseif ($uninstallCommand -match '(?i)^\\s*(.+?\\.exe)\\s*(.*)$') {\n $uninstallCommand = $Matches[1]; if ($Matches[2]) { $uninstallArgs = \"$($Matches[2]) $uninstallArgs\".Trim() }\n }\n Write-Host \"Uninstall command: $uninstallCommand\"; Write-Host \"Uninstall args: $uninstallArgs\"\n $processOptions = @{ FilePath = $uninstallCommand; PassThru = $true; Wait = $true }\n if ($uninstallArgs -ne '') { $processOptions.ArgumentList = $uninstallArgs }\n $process = Start-Process @processOptions\n $exitCode = $process.ExitCode; Write-Host \"Uninstall exit code: $exitCode\"; break\n }\n }\n if (-not $foundUninstaller) { Write-Host \"Uninstaller for '$softwareName' not found.\"; Exit 1 }\n} catch { Write-Host \"Error: $_\"; Exit 1 }\n\nExit $exitCode\n",
"29e6eb61": "# Learn more about .exe install scripts:\n# http://fleetdm.com/learn-more-about/exe-install-scripts\n\n$exeFilePath = \"${env:INSTALLER_PATH}\"\n\n# Azure Data Studio is a Visual Studio Code fork, so it ships the same Inno Setup\n# script -- including the \"runcode\" task, which launches the app once the install\n# finishes. PowerShell's \"Start-Process -Wait\" waits for the process *and all of\n# its descendants*, so the launched app kept the install script blocked\n# indefinitely. \"/MERGETASKS=!runcode\" suppresses the launch (the same switch\n# vscode_install.ps1 and vscodium_install.ps1 use); waiting on the installer\n# process alone, then stopping any stray app process, covers the rest.\n$installTimeoutSeconds = 600\n$registrationTimeoutSeconds = 120\n\n$machineKey = 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$machineKey32on64 = 'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n\nfunction Test-AzureDataStudioRegistered {\n $null -ne (Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |\n Where-Object { $_.DisplayName -like \"Azure Data Studio*\" } |\n Select-Object -First 1)\n}\n\ntry {\n\n$process = Start-Process -FilePath \"$exeFilePath\" `\n -ArgumentList \"/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /MERGETASKS=!runcode\" `\n -PassThru\n# Touch .Handle so the exit code is still readable after the process ends:\n# Start-Process -PassThru otherwise returns $null for .ExitCode.\n$null = $process.Handle\n\nif (-not $process.WaitForExit($installTimeoutSeconds * 1000)) {\n Write-Host \"Installer process did not exit within ${installTimeoutSeconds}s, stopping it.\"\n Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue\n Start-Sleep -Seconds 2\n}\n\n$exitCode = $process.ExitCode\nWrite-Host \"Install exit code: $exitCode\"\n\n# The installer can return before the Add/Remove Programs entry is written; wait\n# for it so software inventory sees a complete install.\n$elapsed = 0\nwhile (-not (Test-AzureDataStudioRegistered) -and ($elapsed -lt $registrationTimeoutSeconds)) {\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"Waiting for Azure Data Studio to register... ($elapsed seconds)\"\n}\n\n# Belt and braces in case a future build ignores !runcode.\nStop-Process -Name \"azuredatastudio\" -Force -ErrorAction SilentlyContinue\n\nif (-not (Test-AzureDataStudioRegistered)) {\n Write-Host \"Azure Data Studio did not register in Add/Remove Programs.\"\n Exit 1\n}\n\n# 3010 (reboot required) and 1641 (reboot initiated) are successful installs.\nif ($exitCode -eq 3010 -or $exitCode -eq 1641) { Exit 0 }\n\nExit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Regenerated — the ref now has the 420s/120s budget. run 30372911136 passes.

}
],
"refs": {
"23fce27b": "$softwareName = \"Azure Data Studio\"\n$softwareNameLike = \"*$softwareName*\"\n$uninstallArgs = \"/VERYSILENT /SUPPRESSMSGBOXES /NORESTART\"\n\n$machineKey = 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$machineKey32on64 = 'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$exitCode = 0\n\ntry {\n [array]$uninstallKeys = Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath }\n\n $foundUninstaller = $false\n foreach ($key in $uninstallKeys) {\n if ($key.DisplayName -like $softwareNameLike) {\n $foundUninstaller = $true\n $uninstallCommand = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString }\n if ($uninstallCommand -match '^\\s*\"([^\"]+)\"\\s*(.*)$') {\n $uninstallCommand = $Matches[1]; if ($Matches[2]) { $uninstallArgs = \"$($Matches[2]) $uninstallArgs\".Trim() }\n } elseif ($uninstallCommand -match '(?i)^\\s*(.+?\\.exe)\\s*(.*)$') {\n $uninstallCommand = $Matches[1]; if ($Matches[2]) { $uninstallArgs = \"$($Matches[2]) $uninstallArgs\".Trim() }\n }\n Write-Host \"Uninstall command: $uninstallCommand\"; Write-Host \"Uninstall args: $uninstallArgs\"\n $processOptions = @{ FilePath = $uninstallCommand; PassThru = $true; Wait = $true }\n if ($uninstallArgs -ne '') { $processOptions.ArgumentList = $uninstallArgs }\n $process = Start-Process @processOptions\n $exitCode = $process.ExitCode; Write-Host \"Uninstall exit code: $exitCode\"; break\n }\n }\n if (-not $foundUninstaller) { Write-Host \"Uninstaller for '$softwareName' not found.\"; Exit 1 }\n} catch { Write-Host \"Error: $_\"; Exit 1 }\n\nExit $exitCode\n",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Regenerated — the ref now exits 0 when no entry is found. run 30372911136 passes.

@codecov

codecov Bot commented Jul 28, 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 68.04%. Comparing base (bf3e1ba) to head (8f834a1).
⚠️ Report is 29 commits behind head on main.

Files with missing lines Patch % Lines
.../SoftwarePage/components/icons/AzureDataStudio.tsx 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #50027      +/-   ##
==========================================
+ Coverage   67.98%   68.04%   +0.06%     
==========================================
  Files        3922     3930       +8     
  Lines      250086   250300     +214     
  Branches    13315    13233      -82     
==========================================
+ Hits       170014   170319     +305     
+ Misses      64776    64684      -92     
- Partials    15296    15297       +1     
Flag Coverage Δ
frontend 60.88% <50.00%> (+0.46%) ⬆️

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.

Addresses Copilot's review, and this one was a real bug. The script allowed 600s
for the install plus 120s polling for registration, but the caller kills the whole
script at 10 minutes -- so the worst case would have been killed part-way through
the recovery path this script exists to provide. Now 420 + 120, with headroom.

Reading .ExitCode straight after Stop-Process could also throw if the process had
not died yet, failing the script even when the install succeeded. Now it waits for
the kill to land and treats registration as the authoritative signal, since a
killed process's exit code means nothing.

Uninstall gets the publisher predicate (verified 'Microsoft Corporation' from the
PE version resource), the bare-token UninstallString fallback,
-ErrorAction SilentlyContinue, and Exit 0 when no entry is found.
Copilot AI review requested due to automatic review settings July 28, 2026 15:20
@github-actions

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/maintained-apps/outputs/azure-data-studio/windows.json

=== Install // 29e6eb61 -> c9dbf666 ===

--- /tmp/old.nVl3pO	2026-07-28 15:22:14.199664496 +0000
+++ /tmp/new.R16Mqx	2026-07-28 15:22:14.199664496 +0000
@@ -10,7 +10,12 @@
 # indefinitely. "/MERGETASKS=!runcode" suppresses the launch (the same switch
 # vscode_install.ps1 and vscodium_install.ps1 use); waiting on the installer
 # process alone, then stopping any stray app process, covers the rest.
-$installTimeoutSeconds = 600
+# Budget: the caller kills the whole script at 10 minutes
+# (cmd/maintained-apps/validate/windows.go), so the worst case here -- full install
+# wait, then the registration wait, plus process overhead -- has to stay under
+# that. 420 + 120 leaves headroom; 600 + 120 would have been killed mid-recovery,
+# in exactly the case this script exists to handle.
+$installTimeoutSeconds = 420
 $registrationTimeoutSeconds = 120
 
 $machineKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
@@ -32,14 +37,23 @@
 # Start-Process -PassThru otherwise returns $null for .ExitCode.
 $null = $process.Handle
 
+$killed = $false
 if (-not $process.WaitForExit($installTimeoutSeconds * 1000)) {
   Write-Host "Installer process did not exit within ${installTimeoutSeconds}s, stopping it."
   Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
-  Start-Sleep -Seconds 2
+  # Wait for the kill to land: reading .ExitCode while the process is still alive
+  # throws, which would fail the script even though the install may have succeeded.
+  $null = $process.WaitForExit(30 * 1000)
+  $killed = $true
 }
 
-$exitCode = $process.ExitCode
-Write-Host "Install exit code: $exitCode"
+$exitCode = $null
+if ($process.HasExited) {
+  $exitCode = $process.ExitCode
+  Write-Host "Install exit code: $exitCode"
+} else {
+  Write-Host "Installer process could not be stopped; falling back to the registration check."
+}
 
 # The installer can return before the Add/Remove Programs entry is written; wait
 # for it so software inventory sees a complete install.
@@ -58,6 +72,10 @@
   Exit 1
 }
 
+# Registration is the authoritative success signal above. If the installer had to
+# be killed, or never reported a code, don't fail on a code that means nothing.
+if ($killed -or $null -eq $exitCode) { Exit 0 }
+
 # 3010 (reboot required) and 1641 (reboot initiated) are successful installs.
 if ($exitCode -eq 3010 -or $exitCode -eq 1641) { Exit 0 }
 

=== Uninstall // 23fce27b -> fc30d6c0 ===

--- /tmp/old.ATgIKK	2026-07-28 15:22:14.224664489 +0000
+++ /tmp/new.mYUtLL	2026-07-28 15:22:14.224664489 +0000
@@ -1,5 +1,11 @@
 $softwareName = "Azure Data Studio"
-$softwareNameLike = "*$softwareName*"
+$softwareNameLike = "$softwareName*"
+# Require the publisher too, mirroring the manifest's exists query. Verified as
+# "Microsoft Corporation" against the installer's PE version resource
+# (CompanyName). Requiring it here puts the value under test -- the validator's
+# appExists looks up by name only, so a wrong exists-query publisher would
+# otherwise ship undetected (cf. the Spyder finding in #50016).
+$softwarePublisher = "Microsoft Corporation"
 $uninstallArgs = "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART"
 
 $machineKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
@@ -8,17 +14,19 @@
 
 try {
     [array]$uninstallKeys = Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue |
-        ForEach-Object { Get-ItemProperty $_.PSPath }
+        ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue }
 
     $foundUninstaller = $false
     foreach ($key in $uninstallKeys) {
-        if ($key.DisplayName -like $softwareNameLike) {
+        if ($key.DisplayName -like $softwareNameLike -and $key.Publisher -eq $softwarePublisher) {
             $foundUninstaller = $true
             $uninstallCommand = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString }
             if ($uninstallCommand -match '^\s*"([^"]+)"\s*(.*)$') {
                 $uninstallCommand = $Matches[1]; if ($Matches[2]) { $uninstallArgs = "$($Matches[2]) $uninstallArgs".Trim() }
             } elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
                 $uninstallCommand = $Matches[1]; if ($Matches[2]) { $uninstallArgs = "$($Matches[2]) $uninstallArgs".Trim() }
+            } elseif ($uninstallCommand -match '^\s*(\S+)\s*(.*)$') {
+                $uninstallCommand = $Matches[1]; if ($Matches[2]) { $uninstallArgs = "$($Matches[2]) $uninstallArgs".Trim() }
             }
             Write-Host "Uninstall command: $uninstallCommand"; Write-Host "Uninstall args: $uninstallArgs"
             $processOptions = @{ FilePath = $uninstallCommand; PassThru = $true; Wait = $true }
@@ -27,7 +35,9 @@
             $exitCode = $process.ExitCode; Write-Host "Uninstall exit code: $exitCode"; break
         }
     }
-    if (-not $foundUninstaller) { Write-Host "Uninstaller for '$softwareName' not found."; Exit 1 }
+    # Nothing to remove is not a failure: uninstall scripts are idempotent here, as
+    # in nordpass_uninstall.ps1 and windsurf_uninstall.ps1.
+    if (-not $foundUninstaller) { Write-Host "Uninstall entry not found for '$softwareName'."; Exit 0 }
 } catch { Write-Host "Error: $_"; Exit 1 }
 
 Exit $exitCode

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

Copilot reviewed 7 out of 8 changed files in this pull request and generated 1 comment.

function Test-AzureDataStudioRegistered {
$null -ne (Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue |
ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |
Where-Object { $_.DisplayName -like "Azure Data Studio*" } |

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Deliberately leaving these different, but the underlying risk is covered.

The registration poll answers a different question from the exists query: "did the installer actually finish, or did it die part-way?" Tightening it to an exact name + publisher match would make it fail if the installer writes DisplayName and Publisher in separate registry operations, which would reintroduce exactly the false-failure this script was written to eliminate.

On the identity risk itself — both halves are verified rather than assumed:

  • DisplayName: the validator's own osquery output reports Found app: 'Azure Data Studio' at C:\Program Files\Azure Data Studio\, Version: 1.52.0 — exactly the string the exists query matches.
  • Publisher: Microsoft Corporation, verified from the installer's PE version resource, and now enforced by the uninstall script's predicate — so if it were wrong, the uninstall would fail in CI rather than shipping.

So a publisher mismatch can no longer ship undetected, which was the substance of the concern.

@kitzy
kitzy marked this pull request as ready for review July 28, 2026 16:16
@kitzy
kitzy requested a review from a team as a code owner July 28, 2026 16:16
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Added Azure Data Studio as a maintained Windows application at version 1.52.0, including catalog metadata, download integrity data, detection queries, and embedded PowerShell install/uninstall scripts. The installer handles silent execution, timeouts, registration polling, process cleanup, and exit codes. The uninstaller discovers matching registry entries and runs silently. A frontend SVG icon component and software-name mapping were also added.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed Clear and specific; it accurately summarizes adding Azure Data Studio as a Windows Fleet-maintained app.
Description check ✅ Passed The description is detailed and includes the issue, summary, rationale, notes, checklist, and testing; only non-applicable template sections are omitted.
✨ 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 add-azure-data-studio-windows-fma

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.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ee/maintained-apps/inputs/winget/scripts/azure_data_studio_install.ps1`:
- Around line 24-28: Use the manifest’s exact registry identity throughout the
Azure Data Studio scripts: update Test-AzureDataStudioRegistered in
ee/maintained-apps/inputs/winget/scripts/azure_data_studio_install.ps1 to
require DisplayName equal to $softwareName and Publisher equal to
$softwarePublisher; remove the wildcard identity variable at line 2 of
ee/maintained-apps/inputs/winget/scripts/azure_data_studio_uninstall.ps1 and
make its uninstall filter require DisplayName equal to $softwareName at line 21.
Regenerate the embedded scripts and ref IDs in
ee/maintained-apps/outputs/azure-data-studio/windows.json lines 19-20.
- Around line 67-68: Update azure_data_studio_install.ps1 to snapshot existing
Azure Data Studio process IDs before installation and restrict fallback cleanup
to only processes created during the current install; never force-close
pre-existing user sessions. Regenerate the embedded install script and ref ID in
ee/maintained-apps/outputs/azure-data-studio/windows.json at line 19 to reflect
the script change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f538766e-d9e0-4144-b33a-2385a22193a2

📥 Commits

Reviewing files that changed from the base of the PR and between bf3e1ba and cb1758e.

⛔ Files ignored due to path filters (1)
  • website/assets/images/app-icon-azure-data-studio-60x60@2x.png is excluded by !**/*.png
📒 Files selected for processing (7)
  • ee/maintained-apps/inputs/winget/azure-data-studio.json
  • ee/maintained-apps/inputs/winget/scripts/azure_data_studio_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/azure_data_studio_uninstall.ps1
  • ee/maintained-apps/outputs/apps.json
  • ee/maintained-apps/outputs/azure-data-studio/windows.json
  • frontend/pages/SoftwarePage/components/icons/AzureDataStudio.tsx
  • frontend/pages/SoftwarePage/components/icons/index.ts

Comment on lines +24 to +28
function Test-AzureDataStudioRegistered {
$null -ne (Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue |
ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |
Where-Object { $_.DisplayName -like "Azure Data Studio*" } |
Select-Object -First 1)

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the manifest’s exact registry identity everywhere.

The manifest requires exact Azure Data Studio / Microsoft Corporation, but install registration accepts any Azure Data Studio* entry and uninstall can remove any publisher-matching prefix match. A timeout fallback can therefore report success without the manifest-detected product, or uninstall a different product.

  • ee/maintained-apps/inputs/winget/scripts/azure_data_studio_install.ps1#L24-L28: require DisplayName -eq $softwareName and Publisher -eq $softwarePublisher.
  • ee/maintained-apps/inputs/winget/scripts/azure_data_studio_uninstall.ps1#L2-L2: remove the wildcard identity variable.
  • ee/maintained-apps/inputs/winget/scripts/azure_data_studio_uninstall.ps1#L21-L21: use DisplayName -eq $softwareName.
  • ee/maintained-apps/outputs/azure-data-studio/windows.json#L19-L20: regenerate the embedded scripts and ref IDs after updating the sources.
📍 Affects 3 files
  • ee/maintained-apps/inputs/winget/scripts/azure_data_studio_install.ps1#L24-L28 (this comment)
  • ee/maintained-apps/inputs/winget/scripts/azure_data_studio_uninstall.ps1#L2-L2
  • ee/maintained-apps/inputs/winget/scripts/azure_data_studio_uninstall.ps1#L21-L21
  • ee/maintained-apps/outputs/azure-data-studio/windows.json#L19-L20
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/maintained-apps/inputs/winget/scripts/azure_data_studio_install.ps1`
around lines 24 - 28, Use the manifest’s exact registry identity throughout the
Azure Data Studio scripts: update Test-AzureDataStudioRegistered in
ee/maintained-apps/inputs/winget/scripts/azure_data_studio_install.ps1 to
require DisplayName equal to $softwareName and Publisher equal to
$softwarePublisher; remove the wildcard identity variable at line 2 of
ee/maintained-apps/inputs/winget/scripts/azure_data_studio_uninstall.ps1 and
make its uninstall filter require DisplayName equal to $softwareName at line 21.
Regenerate the embedded scripts and ref IDs in
ee/maintained-apps/outputs/azure-data-studio/windows.json lines 19-20.

Comment on lines +67 to +68
# Belt and braces in case a future build ignores !runcode.
Stop-Process -Name "azuredatastudio" -Force -ErrorAction SilentlyContinue

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not force-close an existing user session.

This runs after every install, not only after detecting an installer-launched process. It can forcibly terminate an already-running Azure Data Studio instance and discard unsaved work. Snapshot existing PIDs before installation and only stop newly created fallback processes.

  • ee/maintained-apps/inputs/winget/scripts/azure_data_studio_install.ps1#L67-L68: limit cleanup to processes created during this install.
  • ee/maintained-apps/outputs/azure-data-studio/windows.json#L19-L19: regenerate the embedded install script and ref ID.
📍 Affects 2 files
  • ee/maintained-apps/inputs/winget/scripts/azure_data_studio_install.ps1#L67-L68 (this comment)
  • ee/maintained-apps/outputs/azure-data-studio/windows.json#L19-L19
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/maintained-apps/inputs/winget/scripts/azure_data_studio_install.ps1`
around lines 67 - 68, Update azure_data_studio_install.ps1 to snapshot existing
Azure Data Studio process IDs before installation and restrict fallback cleanup
to only processes created during the current install; never force-close
pre-existing user sessions. Regenerate the embedded install script and ref ID in
ee/maintained-apps/outputs/azure-data-studio/windows.json at line 19 to reflect
the script change.

@github-actions

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/maintained-apps/outputs/azure-data-studio/windows.json

=== Install // c9dbf666 -> 8c1c97a1 ===

--- /tmp/old.1BH0mP	2026-07-28 17:44:37.481627991 +0000
+++ /tmp/new.Ann0cX	2026-07-28 17:44:37.482627992 +0000
@@ -3,18 +3,10 @@
 
 $exeFilePath = "${env:INSTALLER_PATH}"
 
-# Azure Data Studio is a Visual Studio Code fork, so it ships the same Inno Setup
-# script -- including the "runcode" task, which launches the app once the install
-# finishes. PowerShell's "Start-Process -Wait" waits for the process *and all of
-# its descendants*, so the launched app kept the install script blocked
-# indefinitely. "/MERGETASKS=!runcode" suppresses the launch (the same switch
-# vscode_install.ps1 and vscodium_install.ps1 use); waiting on the installer
-# process alone, then stopping any stray app process, covers the rest.
-# Budget: the caller kills the whole script at 10 minutes
-# (cmd/maintained-apps/validate/windows.go), so the worst case here -- full install
-# wait, then the registration wait, plus process overhead -- has to stay under
-# that. 420 + 120 leaves headroom; 600 + 120 would have been killed mid-recovery,
-# in exactly the case this script exists to handle.
+# ADS is a VS Code fork with the same Inno script, including the "runcode" task
+# that launches the app after install. -Wait waits on descendants, so that would
+# block forever; "/MERGETASKS=!runcode" suppresses the launch, as in
+# vscode_install.ps1. Timeouts are sized to stay under the caller's 10-minute cap.
 $installTimeoutSeconds = 420
 $registrationTimeoutSeconds = 120
 
@@ -33,16 +25,14 @@
 $process = Start-Process -FilePath "$exeFilePath" `
   -ArgumentList "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /MERGETASKS=!runcode" `
   -PassThru
-# Touch .Handle so the exit code is still readable after the process ends:
-# Start-Process -PassThru otherwise returns $null for .ExitCode.
+# Keeps .ExitCode readable after the process ends.
 $null = $process.Handle
 
 $killed = $false
 if (-not $process.WaitForExit($installTimeoutSeconds * 1000)) {
   Write-Host "Installer process did not exit within ${installTimeoutSeconds}s, stopping it."
   Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
-  # Wait for the kill to land: reading .ExitCode while the process is still alive
-  # throws, which would fail the script even though the install may have succeeded.
+  # Reading .ExitCode while the process is alive would throw.
   $null = $process.WaitForExit(30 * 1000)
   $killed = $true
 }
@@ -55,8 +45,7 @@
   Write-Host "Installer process could not be stopped; falling back to the registration check."
 }
 
-# The installer can return before the Add/Remove Programs entry is written; wait
-# for it so software inventory sees a complete install.
+# The installer can return before the ARP entry is written.
 $elapsed = 0
 while (-not (Test-AzureDataStudioRegistered) -and ($elapsed -lt $registrationTimeoutSeconds)) {
   Start-Sleep -Seconds 5
@@ -64,7 +53,7 @@
   Write-Host "Waiting for Azure Data Studio to register... ($elapsed seconds)"
 }
 
-# Belt and braces in case a future build ignores !runcode.
+# In case a future build ignores !runcode.
 Stop-Process -Name "azuredatastudio" -Force -ErrorAction SilentlyContinue
 
 if (-not (Test-AzureDataStudioRegistered)) {
@@ -72,8 +61,7 @@
   Exit 1
 }
 
-# Registration is the authoritative success signal above. If the installer had to
-# be killed, or never reported a code, don't fail on a code that means nothing.
+# Registration above is the success signal; a killed process's code means nothing.
 if ($killed -or $null -eq $exitCode) { Exit 0 }
 
 # 3010 (reboot required) and 1641 (reboot initiated) are successful installs.

=== Uninstall // fc30d6c0 -> 573526d3 ===

--- /tmp/old.ko4dRc	2026-07-28 17:44:37.508628011 +0000
+++ /tmp/new.hGE8fk	2026-07-28 17:44:37.508628011 +0000
@@ -1,10 +1,5 @@
 $softwareName = "Azure Data Studio"
 $softwareNameLike = "$softwareName*"
-# Require the publisher too, mirroring the manifest's exists query. Verified as
-# "Microsoft Corporation" against the installer's PE version resource
-# (CompanyName). Requiring it here puts the value under test -- the validator's
-# appExists looks up by name only, so a wrong exists-query publisher would
-# otherwise ship undetected (cf. the Spyder finding in #50016).
 $softwarePublisher = "Microsoft Corporation"
 $uninstallArgs = "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART"
 
@@ -35,8 +30,6 @@
             $exitCode = $process.ExitCode; Write-Host "Uninstall exit code: $exitCode"; break
         }
     }
-    # Nothing to remove is not a failure: uninstall scripts are idempotent here, as
-    # in nordpass_uninstall.ps1 and windsurf_uninstall.ps1.
     if (-not $foundUninstaller) { Write-Host "Uninstall entry not found for '$softwareName'."; Exit 0 }
 } catch { Write-Host "Error: $_"; Exit 1 }

@allenhouchins allenhouchins changed the title Add Azure Data Studio Windows FMA Add Azure Data Studio as a Windows FMA Jul 29, 2026
@allenhouchins
allenhouchins merged commit 2fc41c7 into main Jul 29, 2026
36 checks passed
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.

4 participants