<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** NA — caught from a failing `Update Fleet-maintained apps` ingest run. # What this does Changes `installer_arch` from `x86` to `x64` for the Gpg4win Windows FMA, and regenerates its output (5.0.2 → 5.1.0). ## Why the ingest was failing ``` {"level":"INFO","msg":"ingesting winget app","name":"Gpg4win"} panic: ingesting winget app: failed to find installer for app ``` This is **not** a removed winget package — Gpg4win is still published as `GnuPG.Gpg4win`. The manifest was fetched and parsed fine; the failure is the `selectedInstaller == nil` check in `ee/maintained-apps/ingesters/winget/ingester.go`, which means no installer entry matched all four selector fields from our input. Upstream flipped the architecture label between versions: | | 5.0.2 (last ingested) | 5.1.0 (new) | |---|---|---| | `Architecture` | **x86** | **x64** | | Manifest generator | `wingetcreate 1.10.3.0` | `YamlCreate.ps1 Dumplings Mod` | Our input pinned `x86` to match 5.0.2, so nothing matched once 5.1.0 landed in `winget-pkgs` (2026-08-03 18:11 UTC, microsoft/winget-pkgs#409397). The ingester only walks to an older version directory on a genuine 404 of the installer manifest — never because the newest version's installer failed to match — so it panics instead of falling back. ## The new x64 label is the correct one Verified against the real installer rather than trusting either manifest: - The NSIS stub's PE header is i386, which is very likely what `wingetcreate` guessed `x86` from. Installer stubs are almost always 32-bit, so stub arch says nothing about the payload. - The payload ships 64-bit binaries in `bin/` (PE machine `0x8664`) alongside a 32-bit `bin_32/` compatibility set. So 5.0.2's `x86` was the inaccurate manifest and the bot corrected it. Regenerated `sha256` also matches a fresh download of `gpg4win-5.1.0.exe` bit-for-bit (`9682f282…20a2e`). ## Blast radius Worth flagging: this one field blocked **the entire FMA ingest**, not just Gpg4win. `failed to find installer for app` isn't matched by `isTransientGitHubError`, so it returns up to `panic(err)` in `cmd/maintained-apps/main.go`, which aborts the process before `processOutput` writes *any* app's manifest. Because the `ingesters` map is iterated in random order, this could take out the homebrew side too. Any future upstream flip of `installer_arch` / `installer_scope` / `installer_type` / `installer_locale` on any single app will do the same thing — worth a follow-up to make non-transient per-app ingest errors skip-and-report instead of fatal. ## Why no changes file The net user-visible effect is a routine FMA version bump (Gpg4win 5.1.0), which the scheduled ingest PR delivers without changelog entries. Happy to add one if you'd rather it be called out. # 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. ## Testing - [x] QA'd all new/changed functionality manually Verification performed: - `go run ./cmd/maintained-apps -slug gpg4win/windows` completes with no panic (previously fatal). - Diff is scoped to the two expected files; `outputs/apps.json` is untouched, since name/description didn't change. - Regenerated `sha256` matches a fresh download of the upstream installer. - Install/uninstall scripts need no changes — both already enumerate the native *and* `Wow6432Node` registry views across `HKLM`/`HKCU`, so detection survives the 32→64-bit flip. - `installer_arch` is only a manifest selector plus CI runner routing in `.github/scripts/partition-fma-apps.sh`, where x64/x86/neutral all land on the same x64 runner. No routing change, and the field is never written into `outputs/`, so no user-visible arch claim changes. Still needs the FMA validator's install/uninstall run on a Windows runner to confirm 5.1.0 installs and is detected — that's what the draft is for.
Fleet-maintained apps (FMA)
Using Claude Code? The
new-fmaskill (.claude/skills/new-fma/SKILL.md) automates this workflow and bakes in the gotchas this README doesn't cover — verifying the installed app's identity withmsiinfo/PlistBuddyinstead of trusting winget/cask metadata, handling bootstrapper installers, parsing unquotedUninstallStrings, version-matching quirks, and more. Just ask it to "add X as a macOS/Windows FMA."
Adding a new app (macOS)
-
Find the app's metadata in its Homebrew formulae
-
Create a new manifest file called
$YOUR_APP_NAME.jsonin theinputs/homebrew/directory. For example, if you wanted to add Box Drive, create the fileinputs/homebrew/box-drive.json. -
Fill out the file according to the input schema below. For our example Box Drive app, it would look like this:
{ "name": "Box Drive", "slug": "box-drive/darwin", "unique_identifier": "com.box.desktop", "token": "box-drive", "installer_format": "pkg", "default_categories": ["Productivity"] } -
Run the following command from the root of the Fleet repo to generate the app's output data:
go run cmd/maintained-apps/main.go --slug="<slug-name>" --debug -
The contributor is responsible for adding the icon to Fleet (e.g. the TypeScript and website PNG components of #29175). These are generated using the generate-icons script. The script automatically adds the import statement and map entry to
frontend/pages/SoftwarePage/components/icons/index.ts, so you don't need to manually update the index file. -
Add a description for the app in
outputs/apps.jsonfile. You can use descriptions from Homebrew formulae. For consistency and presentation on the website, the description should follow sentence casing and the following format:<App Name>is a(n) (copy description from Homebrew)., making sure to end with a.. -
Open a PR to the
fleetrepository with the above changes. The #g-software Engineering Manager (EM) is automatically added reviewer. Also, @ mention the Fleet-maintained apps DRI. -
If the app passes automated tests, it is approved and merged. The EM reviews the PR within 3 business days. The app should appear shortly in the Fleet-maintained apps section when adding new software to Fleet. The app icon will not appear in Fleet until the following release.
macOS input file schema
| Name | Type | Description |
|---|---|---|
name |
string | Required. User-facing name of the application. |
unique_identifier |
string | Required. Platform-specific unique identifier (e.g., bundle identifier on macOS). |
token |
string | Required. Homebrew's unique identifier. It's the token field of the Homebrew API response. |
installer_format |
string | Required. File format of the installer (zip, dmg, pkg). Determine via the file extension in the Homebrew API url field or by downloading the installer if the extension isn’t present. |
slug |
string | Required. Identifies the app/platform combination (e.g., box-drive/darwin). Used to name manifest files and reference the app in Fleet's best practice GitOps. Format: <app-name>/<platform>, where app name is filesystem-friendly and platform is darwin. |
default_categories |
string | Required. Default categories for self-service if none are specified. Valid values: Browsers, Communication, Developer Tools, Productivity. |
pre_uninstall_scripts |
string | Command lines run before the generated uninstall script (e.g., for Box). |
post_uninstall_scripts |
string | Command lines run after the generated uninstall script (e.g., for Box). |
install_script_path |
string | Filepath to a custom install script (.sh). Overrides the generated install script. Script must be placed in inputs/homebrew/scripts/. |
uninstall_script_path |
string | Filepath to a custom uninstall script (.sh). Overrides the generated uninstall script. Cannot be used together with pre_uninstall_scripts or post_uninstall_scripts. Script must be placed in inputs/homebrew/scripts/. |
cask_path |
string | Path (relative to the repo root) to a local file containing the cask JSON in the same schema as https://formulae.brew.sh/api/cask/<token>.json. Used to commit cask metadata for third-party taps directly into this repo under inputs/homebrew/custom-tap/. See Ingesting apps from a custom tap below. |
Ingesting apps from a custom tap
Apps that live in a third-party Homebrew tap (not Homebrew/homebrew-cask) are not proxied by https://formulae.brew.sh/api/. To ingest them, commit both the .rb source and the generated .json into inputs/homebrew/custom-tap/, laid out like a Homebrew tap:
custom-tap/
├── Casks/<token>.rb # Cask DSL source
├── api/<token>.json # Generated with regenerate.sh
└── regenerate.sh # Rebuild api/*.json from Casks/*.rb
- Write the cask DSL in
inputs/homebrew/custom-tap/Casks/<token>.rb. - Run
./regenerate.shfrom insidecustom-tap/to produceapi/<token>.json. Requires macOS with Homebrew andjq. - In the app's input manifest (
inputs/homebrew/<token>.json), setcask_pathtoee/maintained-apps/inputs/homebrew/custom-tap/api/<token>.json. Seeinputs/homebrew/fleet-desktop.jsonfor an example.
See inputs/homebrew/custom-tap/README.md for the full contributor flow. Apps without cask_path continue to be fetched from formulae.brew.sh.
Adding a new app (Windows)
-
Find the Winget
PackageIdentifierin the relevant winget-pkgs repo manifest. -
Get the unique identifier that Fleet will use for matching the software with software inventory:
- On a test Windows host, install the app manually, then run the following PowerShell script that correlates to the defined
installer_scope:- Machine scope:
Get-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue | Where-Object {$_.DisplayName -like '*<App Name>*'} | Select-Object DisplayName, DisplayVersion, Publisher - User scope:
Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue | Where-Object {$_.DisplayName -like '*<App Name>*'} | Select-Object DisplayName, DisplayVersion, Publisher
- Machine scope:
If the unique_identifier doesn't match the DisplayName, then Fleet will incorrectly create two software titles when the Fleet-maintained app is added and later installed. One title for the Fleet-maintained app and a separate title for the inventoried software.
- Fill out the file according to the input schema. For example, Box Drive looks like this:
{
"name": "Box Drive",
"slug": "box-drive/windows",
"package_identifier": "Box.Box",
"unique_identifier": "Box",
"installer_arch": "x64",
"installer_type": "msi",
"installer_scope": "machine",
"default_categories": ["Productivity"]
}
- Run
go run cmd/maintained-apps/main.go --slug="<app-name>/windows" --debugfrom the root of the Fleet repo to generate the app's output data, replacing<app-name>with your app's name, for example:
go run cmd/maintained-apps/main.go --slug="box-drive/windows" --debug
-
The contributor is responsible for adding the icon to Fleet (e.g. the TypeScript and website PNG components of #29175). These are generated using the generate-icons script. The script automatically adds the import statement and map entry to
frontend/pages/SoftwarePage/components/icons/index.ts, so you don't need to manually update the index file. -
Add a description for the app in outputs/apps.json file. You can use descriptions from the wingest manifest.
-
Open a PR to the fleet repository with the above changes. The #g-software Engineering Manager (EM) is automatically added reviewer. Also, @ mention the #g-software Product Designer (PD) in a comment that points them to the new icon. This way, the icon change gets a second pair of eyes.
-
If the app passes automated tests, it is approved and merged. The EM reviews the PR within 3 business days. The app should appear shortly in the Fleet-maintained apps section when adding new software to Fleet. The app icon will not appear in Fleet until the following release.
Windows input file schema
| Name | Type | Description |
|---|---|---|
name |
string | Required. User-facing name of the application. |
unique_identifier |
string | Required. Platform-specific unique identifier. For Windows, this is the DisplayName. |
package_identifier |
string | Required. The PackageIdentifier from winget. Fleet uses this to pull the correct metadata for the app. |
slug |
string | Required. Identifies the app/platform combination (e.g., box-drive/windows). Used to name manifest files and reference the app in Fleet's best practice GitOps. Format: <app-name>/<platform>, where app name is filesystem-friendly and platform is darwin. |
installer_arch |
string | Required. x64 or x86 (most apps use x64). |
installer_type |
string | Required. exe, msi, or msix (file type, not vendor tech like "wix") |
installer_scope |
string | Required. machine or user (prefer machine for managed installs) |
default_categories |
string | Required. Default categories for self-service if none are specified. Valid values: Browsers, Communication, Developer Tools, Productivity. |
install_script_path |
string | Filepath to a custom install script (.ps1). Overrides the generated install script. Script must be placed in inputs/winget/scripts/. For .msi apps, the ingestor automatically generates install scripts. Do not add scripts unless you need to override the generated behavior. For .exe apps, you must provide PowerShell scripts that run the installer file directly. Fleet stores the installer and sends it to the host at install time; your script must execute it using the INSTALLER_PATH environment variable. |
uninstall_script_path |
string | Filepath to a custom uninstall script (.ps1). Overrides the generated uninstall script. Script must be placed in inputs/winget/scripts/. For .msi apps, the ingestor automatically generates uninstall scripts. Do not add scripts unless you need to override the generated behavior. For .exe apps, you must provide a script to uninstall the app. Scripts for .exe apps are vendor-specific. Use the vendor’s documented silent uninstall switch or the registered UninstallString (if available), ensuring the script runs silently and returns the installer’s exit code. |
fuzzy_match_name |
boolean | If the unique_identifier doesn't match the DisplayName, use fuzzy_match_name to specify that Fleet uses "fuzzy matching" to match the Fleet-maintained app and the inventoried software. For example, for Pritunl, the unique_identifier is "Pritunl" and the inventories software's DisplayName is "Pritunl Client". With fuzzy_match_name set to true, Pritunl app will be matched to the inventories software. |
Windows troubleshooting
- App not found in Fleet UI: ensure
apps.jsonwas updated by the generator and your override URL is correct - Install fails silently: confirm your
installer_type,installer_arch, andinstaller_scopematch the selected winget installer; run your PowerShell script manually on a test host - Uninstall doesn’t remove the app: prefer explicit uninstall scripts; otherwise, ensure the winget manifest exposes
ProductCodeorUpgradeCode - Hash mismatch errors: if the upstream manifest is in flux, you can set
ignore_hash: truein the input JSON (use sparingly)
Can I do this on macOS?
The instructions below are meant to be run on a Windows host. But, you can run most of this on a macOS host, as well:
- You can author Windows inputs and run the generator on macOS. The ingester is Go code that fetches data from winget/GitHub and works cross‑platform.
- To find the PackageName and Publisher, you can look in the locale and installer yaml files in the winget-pkgs repo.
- Validation and testing still require a Windows host (to verify programs.name and to run install/uninstall).
Updating existing Fleet-maintained apps
Fleet-maintained apps need to be updated as frequently as possible while maintaining reliability. This is currently a balancing act as both scenarios below result in customer workflow blocking bugs:
- App vendor updates to installers can break install/uninstall scripts
- App vendors will deprecate download links for older installers
A Github action periodically creates a PR that updates one or more apps in the catalog by:
- Bumping versions
- Regenerating install/uninstall scripts
Each app updated in the PR must be validated independently. Only merge the PR if all apps changed meet the following criteria:
- App can be downloaded using manifest URL
- App installs successfully on host using manifest install script
- App exists on host
- App uninstalls successfully on host using manifest uninstall script
If an app does not pass test criteria:
- Freeze the app
- File a bug for tracking
Freezing an existing Fleet-maintained app
If any app fails validation:
-
Do not merge the PR as-is.
-
Add
"frozen": true"to the failing app's input file (e.g.,inputs/homebrew/<app>.json). -
Revert its corresponding output manifest file (e.g.,
outputs/<slug>.json) to the version in themainbranch:git checkout origin/main -- ee/maintained-apps/outputs/<slug>.json -
Validate changes in the frozen input file by running the following. This should output no errors and generate no changes.
go run cmd/maintained-apps/main.go --slug="<slug>" --debug -
Commit both the input change and the output file revert to the same PR.