Files
Allen Houchins 051d12718c Add JetBrains ReSharper as a Windows Fleet-maintained app (#50659)
**Related issue:** Resolves #50567

Adds JetBrains ReSharper as a Windows Fleet-maintained app (winget
`JetBrains.ReSharper`, version `2026.2.0.2`).

ReSharper is a Visual Studio extension rather than a standalone app, so
it does not follow the pattern of the other JetBrains FMAs (Rider,
PhpStorm, DataGrip, etc.), which are plain NSIS installers that take
`/S`. Reviewers should read the risks below before approving — a couple
of things can only be confirmed from a validator run.

## What's here

- `ee/maintained-apps/inputs/winget/resharper.json`
- `ee/maintained-apps/inputs/winget/scripts/resharper_install.ps1` /
`resharper_uninstall.ps1`
- Generated `ee/maintained-apps/outputs/resharper/windows.json` +
`apps.json` entry
- Icon (`Resharper.tsx`, website PNG, alphabetical `index.ts` entries),
generated from JetBrains' own brand asset

## Decisions that differ from the other JetBrains FMAs

**`use_display_version_for_patch` is omitted.** Every other JetBrains
winget input sets it, but the ReSharper manifest has no
`AppsAndFeaturesEntries`, so the ingester hard-errors with
`use_display_version_for_patch is set but no DisplayVersion found in
winget manifest`. The patch policy therefore compares against winget's
`2026.2.0.2`.

**Custom `exists_query` instead of `fuzzy_match_name`.** The prefix is
loose enough to match a possible per-VS-instance suffix, and excludes
the separate ReSharper C++ and ReSharper SDK products:

```sql
SELECT 1 FROM programs WHERE name LIKE 'JetBrains ReSharper%' AND name NOT LIKE 'JetBrains ReSharper C++%' AND name NOT LIKE 'JetBrains ReSharper SDK%' AND publisher = 'JetBrains s.r.o.';
```

**Install script detects Visual Studio.** It builds `/VsVersion` from
the instances `vswhere` reports and runs the installer with
`/Silent=True /PerMachine=True /SkipEtwService=True`:

- `/PerMachine=True` — the installer otherwise targets `%LocalAppData%`,
which under Fleet's SYSTEM context would land in the SYSTEM profile
instead of the developer's. The path is not configurable
([RSRP-428991](https://youtrack.jetbrains.com/issue/RSRP-428991)).
- `/SkipEtwService=True` — JetBrains documents that `EtwHostService.msi`
always raises a UAC prompt, so a fully silent install of every component
is not possible
([SUPPORT-A-3189](https://youtrack.jetbrains.com/articles/SUPPORT-A-3189)).
- It then waits for the uninstall registry entry (what osquery reads),
because the web bootstrapper can outlive its own exit code, and logs the
resulting ARP entries.

**Uninstall removes every matching entry**, since ReSharper registers
one per Visual Studio instance, using the defensive `UninstallString`
parser and appending `/Silent=True` rather than the NSIS `/S`.

Switches come from [JetBrains' silent install/uninstall
article](https://resharper-support.jetbrains.com/hc/en-us/articles/207241485-How-to-use-silent-install-and-silent-uninstall-of-ReSharper-via-Command-Line),
not guesswork.

## `unique_identifier` is provisional

`program_publisher` is verified — `JetBrains s.r.o.` is hard-coded next
to the ARP value names (`DisplayName`, `DisplayVersion`,
`UninstallString`, `Publisher`) in `JetBrains.Platform.Installer.exe`,
extracted from the installer.

The **DisplayName is not verifiable offline.** The winget URL is a
two-stage web bootstrapper: the 69 MB `.web.exe` contains
`JetBrains.Platform.Installer.Bootstrap.exe`, which downloads the
JetBrains dotUltimate installer, which downloads the product packages.
The ARP entry is written by that downloaded stage under
`Software\Microsoft\Windows\CurrentVersion\Uninstall\{GUID}`, with
`DisplayName` taken from a per-VS-host `PresentableName`. So `JetBrains
ReSharper` is a best-supported guess.

It can be confirmed from a validator run:
`cmd/maintained-apps/validate/windows.go` searches `programs` with a
loose `LOWER(name) LIKE '%…%'` on both the catalog name and
`unique_identifier`, and logs `Found app: '<DisplayName>' … Version:
<ver>` after running `MutateSoftwareOnIngestion`. That reveals both the
true DisplayName and the post-mutation version. The install script
prints the same information. **Expect a follow-up commit correcting
`unique_identifier` (and possibly the exists query) once that log
lands.**

## Risks

1. **Payload is not pinned.** The SHA covers only the 69 MB
bootstrapper; roughly 1.7 GB is fetched from `download.jetbrains.com` at
install time. JetBrains publishes only a `windowsWeb` download for
ReSharper, so there is no offline installer to point at. Install
duration may exceed script timeouts.
2. **Requires Visual Studio.** With no VS present the installer has
nothing to install, so the script exits 1 with a clear message.
`windows-latest` runners ship Visual Studio 2022 Enterprise, so
validation should be able to install.
3. **`/PerMachine=True` conflicts with pre-existing per-user installs.**
JetBrains states machine-wide mode "is not compatible with existing
installations in user profiles"; one must be removed first.
4. **`/SkipEtwService=True` omits the ETW host service**, so
dotTrace/dotMemory profiling integration is incomplete. This is the
documented tradeoff for an unattended install.
5. **Version reconciliation unconfirmed.** If the DisplayName ends in a
marketing version, the JetBrains name-based version mutation fires and
the validator's prefix check passes; a VS-suffixed name would instead
fall back to the registry `DisplayVersion`.

# Checklist for submitter

- [x] 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.
- [x] Timeouts are implemented and retries are limited to avoid infinite
loops

## Testing

- [x] `go test ./ee/maintained-apps/...` passes; both output JSON files
parse; generated SHA matches the winget manifest.
- [ ] QA'd all new/changed functionality manually — **not done.**
Install/uninstall need a Windows host with Visual Studio; relying on the
FMA Windows validator, which is also how `unique_identifier` gets
confirmed.


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

* **New Features**
  * Added ReSharper to the maintained Windows applications catalog.
* Added support for silent machine-wide installation and uninstallation.
* Added Visual Studio compatibility checks and installation failure
reporting.
  * Added a ReSharper icon for display in the software catalog.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-07 12:16:17 -05:00

98 lines
3.4 KiB
PowerShell

# Learn more about .exe install scripts:
# http://fleetdm.com/learn-more-about/exe-install-scripts
#
# ReSharper is a Visual Studio extension installed by a web bootstrapper.
# /PerMachine=True keeps it out of the SYSTEM profile, /SkipEtwService=True avoids
# an unavoidable UAC prompt, /VsVersion picks the VS instances to integrate into.
# https://resharper-support.jetbrains.com/hc/en-us/articles/207241485
$exeFilePath = "${env:INSTALLER_PATH}"
$registryTimeoutSeconds = 3300
$uninstallPaths = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
)
function Get-ReSharperEntries {
Get-ChildItem -Path $uninstallPaths -ErrorAction SilentlyContinue |
ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |
Where-Object {
$_.DisplayName -like 'JetBrains ReSharper*' -and
$_.DisplayName -notlike 'JetBrains ReSharper C++*' -and
$_.DisplayName -notlike 'JetBrains ReSharper SDK*' -and
$_.Publisher -like '*JetBrains*'
}
}
try {
$vsWhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
if (-not (Test-Path $vsWhere)) {
Write-Host "Visual Studio Installer not found at $vsWhere."
Write-Host "ReSharper is a Visual Studio extension and cannot be installed without Visual Studio."
Exit 1
}
$installationVersions = & $vsWhere -all -prerelease -products '*' -property installationVersion 2>$null
$vsMajors = @(
$installationVersions |
Where-Object { $_ -match '^\d+' } |
ForEach-Object { [int](($_ -split '\.')[0]) } |
Sort-Object -Unique -Descending
)
if ($vsMajors.Count -eq 0) {
Write-Host "No Visual Studio instances were reported by vswhere."
Write-Host "ReSharper is a Visual Studio extension and cannot be installed without Visual Studio."
Exit 1
}
$vsVersions = ($vsMajors | ForEach-Object { "$_.0" }) -join ';'
Write-Host "Visual Studio instances detected: $vsVersions"
$logFile = Join-Path $env:TEMP 'resharper-install.log'
$processOptions = @{
FilePath = "$exeFilePath"
ArgumentList = "/Silent=True /PerMachine=True /SkipEtwService=True /VsVersion=$vsVersions /LogFile=`"$logFile`""
PassThru = $true
Wait = $true
}
$process = Start-Process @processOptions
$exitCode = $process.ExitCode
Write-Host "Installer exit code: $exitCode"
$deadline = (Get-Date).AddSeconds($registryTimeoutSeconds)
$entries = @(Get-ReSharperEntries)
while ($entries.Count -eq 0 -and (Get-Date) -lt $deadline) {
$installerWasRunning = @(Get-Process -Name 'JetBrains.Platform.Installer*' -ErrorAction SilentlyContinue).Count -gt 0
Start-Sleep -Seconds 10
$entries = @(Get-ReSharperEntries)
$installerIsRunning = @(Get-Process -Name 'JetBrains.Platform.Installer*' -ErrorAction SilentlyContinue).Count -gt 0
if ($entries.Count -eq 0 -and -not $installerWasRunning -and -not $installerIsRunning) { break }
}
if ($entries.Count -eq 0) {
Write-Host "The ReSharper uninstall registry entry did not appear."
if (Test-Path $logFile) {
Write-Host "--- last 50 lines of $logFile ---"
Get-Content $logFile -Tail 50 | ForEach-Object { Write-Host $_ }
}
if ($exitCode -eq 0) { Exit 1 }
Exit $exitCode
}
foreach ($entry in $entries) {
Write-Host "Installed: DisplayName='$($entry.DisplayName)' DisplayVersion='$($entry.DisplayVersion)' Publisher='$($entry.Publisher)'"
}
Exit $exitCode
} catch {
Write-Host "Error: $_"
Exit 1
}