镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Quit 1Password with SIGTERM in the macOS FMA install - #56452

Merged
allenhouchins merged 2 commits into
mainfrom
allenhouchins/1password-quit-failure-d4044d
Oct 23, 2026
Merged

allenhouchins merged 2 commits into
mainfrom
allenhouchins/1password-quit-failure-d4044d

Conversation

@allenhouchins

@allenhouchins allenhouchins commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

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

  • QA'd all new/changed functionality manually

go test ./ee/maintained-apps/... ./cmd/maintained-apps/... passes.

I ran the script in bash with osascript, pkill, installer and open stubbed, in four modes:

Mode Result
Not running Installs, no relaunch
Quits on SIGTERM Quits, installs, relaunches
Ignores SIGTERM "did not quit", exit 1, nothing installed
Installer fails Exits with the installer's code, no relaunch

On a real Mac (1Password 8.12.42), I ran the script as the logged-in user with only installer stubbed. It quit the running 1Password with SIGTERM, then relaunched it. I haven't run a real root install on a host. The FMA validator on this PR runs the install.

AI

AI: Claude Code (claude-opus-5-5)

Summary

In dogfood, the 1Password install fails on almost every attempt:

Quitting application 'com.1password.1password'...
Application 'com.1password.1password' did not quit.

1Password's Electron main process cancels before-quit when Keep 1Password in the menu bar (keepInTray) is on, which is the default. It closes its windows and stays in the menu bar. So tell application id "com.1password.1password" to quit never stops it. The script retried for 10 seconds, then exited 1. That's been the case since #55342 made custom install scripts stop when the app won't quit. Before that, the pgrep check was a no-op and the pkg installed over the running app.

1Password handles SIGTERM (and SIGINT, power-off, and its menu bar Quit item) by setting a "really quit" flag, then quitting through its normal will-quit shutdown. On a real Mac, pkill -TERM -x 1Password closed it, and every process in the bundle, in under a second.

Changes:

  • 1password_install.sh:
    • A new quit_and_track_1password sends SIGTERM, then polls is running the same way the shared helper does. It returns 1 after 10 seconds, so the install still stops if 1Password won't exit.
    • It now relaunches 1Password after the install with a verbatim copy of relaunch_application. Before, a 1Password that quit stayed closed.
    • installer gets || exit $?, matching the generated pkg scripts.
  • outputs/1password/darwin.json: new install_script_ref and its ref only. No version bump, and the uninstall is unchanged.

Related PRs:

Summary by CodeRabbit

  • Bug Fixes
    • The 1Password installer now restores the app after installation if it was running beforehand.
    • Installation reports a failure if 1Password cannot quit or the installer does not complete successfully.

With "Keep 1Password in the menu bar" on (the default), 1Password cancels
quit events and stays in the menu bar, so the install script's quit loop
always timed out and the install stopped. 1Password treats SIGTERM like
Quit in its menu bar menu, so send that instead, then relaunch it after
the pkg installs.

Widen the custom install script quit-call check to every quit_* call so
the new function is covered.
fleet-release
fleet-release previously approved these changes Oct 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/maintained-apps/outputs/1password/darwin.json

=== Install // 070d64dd -> c0388cc8 ===

--- /tmp/old.FyosSu	2026-10-23 17:29:48.936724892 +0000
+++ /tmp/new.NkPJES	2026-10-23 17:29:48.936724892 +0000
@@ -1,30 +1,40 @@
 #!/bin/bash
 
-quit_application() {
-  local bundle_id="$1"
+# With "Keep 1Password in the menu bar" on (the default), 1Password answers a
+# quit event by closing its windows and staying in the menu bar. SIGTERM quits
+# it fully, the same as Quit in its menu bar menu.
+quit_and_track_1password() {
+  local bundle_id="com.1password.1password"
+  local var_name="APP_WAS_RUNNING_${bundle_id//[^[:alnum:]_]/_}"
   local timeout_duration=10
 
   # check if the application is running
   local app_running
   app_running=$(osascript -e "application id \"$bundle_id\" is running" 2>/dev/null)
   if [[ "$app_running" != "true" ]]; then
+    eval "export $var_name=0"
     return
   fi
 
   local console_user
   console_user=$(stat -f "%Su" /dev/console)
-  if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then
+  if [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]; then
     echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'."
+    eval "export $var_name=0"
     return
   fi
 
+  # App was running, mark it for relaunch
+  eval "export $var_name=1"
+  echo "Application '$bundle_id' was running; will relaunch after installation."
+
   echo "Quitting application '$bundle_id'..."
+  pkill -TERM -x 1Password
 
-  # try to quit the application within the timeout period
+  # wait for the application to quit within the timeout period
   local quit_success=false still_running
   SECONDS=0
   while (( SECONDS < timeout_duration )); do
-    osascript -e "tell application id \"$bundle_id\" to quit" >/dev/null 2>&1
     sleep 1
     if still_running=$(osascript -e "application id \"$bundle_id\" is running" 2>/dev/null) && [[ "$still_running" == "false" ]]; then
       echo "Application '$bundle_id' quit successfully."
@@ -39,5 +49,47 @@
   fi
 }
 
-quit_application 'com.1password.1password' || exit 1
-installer -pkg "$INSTALLER_PATH" -target /
+relaunch_application() {
+  local bundle_id="$1"
+  local var_name="APP_WAS_RUNNING_${bundle_id//[^[:alnum:]_]/_}"
+  local was_running
+
+  # Check if the app was running before installation
+  eval "was_running=\$$var_name"
+  if [[ "$was_running" != "1" ]]; then
+    return
+  fi
+
+  local console_user
+  console_user=$(stat -f "%Su" /dev/console)
+  if [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]; then
+    echo "Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'."
+    return
+  fi
+
+  echo "Relaunching application '$bundle_id'..."
+
+  # Launch the app in the logged-in user's GUI session. Apps launched by root
+  # won't register with the user's Dock/GUI, so run 'open' as the console user.
+  # Use 'launchctl asuser' to bootstrap into the console user's Mach namespace
+  # and GUI session — 'sudo -u' alone doesn't do this, which can cause
+  # LSOpenURLsWithRole() failures even when 'open' exits 0.
+  local open_status=0
+  if [[ $EUID -eq 0 ]]; then
+    local console_uid
+    console_uid=$(id -u "$console_user")
+    /bin/launchctl asuser "$console_uid" sudo -u "$console_user" open -b "$bundle_id" >/dev/null 2>&1 || open_status=$?
+  else
+    open -b "$bundle_id" >/dev/null 2>&1 || open_status=$?
+  fi
+
+  if [[ $open_status -eq 0 ]]; then
+    echo "Application '$bundle_id' relaunched successfully."
+  else
+    echo "Failed to relaunch application '$bundle_id'."
+  fi
+}
+
+quit_and_track_1password || exit 1
+installer -pkg "$INSTALLER_PATH" -target / || exit $?
+relaunch_application 'com.1password.1password'

=== Uninstall Script (no changes) ===

@allenhouchins
allenhouchins marked this pull request as ready for review October 23, 2026 17:34
Copilot AI balanced review requested due to automatic review settings October 23, 2026 17:34
@github-actions

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/maintained-apps/outputs/1password/darwin.json

=== Install // 070d64dd -> c0388cc8 ===

--- /tmp/old.ul3eS0	2026-10-23 17:35:07.179274014 +0000
+++ /tmp/new.2oxxIz	2026-10-23 17:35:07.180274023 +0000
@@ -1,30 +1,40 @@
 #!/bin/bash
 
-quit_application() {
-  local bundle_id="$1"
+# With "Keep 1Password in the menu bar" on (the default), 1Password answers a
+# quit event by closing its windows and staying in the menu bar. SIGTERM quits
+# it fully, the same as Quit in its menu bar menu.
+quit_and_track_1password() {
+  local bundle_id="com.1password.1password"
+  local var_name="APP_WAS_RUNNING_${bundle_id//[^[:alnum:]_]/_}"
   local timeout_duration=10
 
   # check if the application is running
   local app_running
   app_running=$(osascript -e "application id \"$bundle_id\" is running" 2>/dev/null)
   if [[ "$app_running" != "true" ]]; then
+    eval "export $var_name=0"
     return
   fi
 
   local console_user
   console_user=$(stat -f "%Su" /dev/console)
-  if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then
+  if [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]; then
     echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'."
+    eval "export $var_name=0"
     return
   fi
 
+  # App was running, mark it for relaunch
+  eval "export $var_name=1"
+  echo "Application '$bundle_id' was running; will relaunch after installation."
+
   echo "Quitting application '$bundle_id'..."
+  pkill -TERM -x 1Password
 
-  # try to quit the application within the timeout period
+  # wait for the application to quit within the timeout period
   local quit_success=false still_running
   SECONDS=0
   while (( SECONDS < timeout_duration )); do
-    osascript -e "tell application id \"$bundle_id\" to quit" >/dev/null 2>&1
     sleep 1
     if still_running=$(osascript -e "application id \"$bundle_id\" is running" 2>/dev/null) && [[ "$still_running" == "false" ]]; then
       echo "Application '$bundle_id' quit successfully."
@@ -39,5 +49,47 @@
   fi
 }
 
-quit_application 'com.1password.1password' || exit 1
-installer -pkg "$INSTALLER_PATH" -target /
+relaunch_application() {
+  local bundle_id="$1"
+  local var_name="APP_WAS_RUNNING_${bundle_id//[^[:alnum:]_]/_}"
+  local was_running
+
+  # Check if the app was running before installation
+  eval "was_running=\$$var_name"
+  if [[ "$was_running" != "1" ]]; then
+    return
+  fi
+
+  local console_user
+  console_user=$(stat -f "%Su" /dev/console)
+  if [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]; then
+    echo "Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'."
+    return
+  fi
+
+  echo "Relaunching application '$bundle_id'..."
+
+  # Launch the app in the logged-in user's GUI session. Apps launched by root
+  # won't register with the user's Dock/GUI, so run 'open' as the console user.
+  # Use 'launchctl asuser' to bootstrap into the console user's Mach namespace
+  # and GUI session — 'sudo -u' alone doesn't do this, which can cause
+  # LSOpenURLsWithRole() failures even when 'open' exits 0.
+  local open_status=0
+  if [[ $EUID -eq 0 ]]; then
+    local console_uid
+    console_uid=$(id -u "$console_user")
+    /bin/launchctl asuser "$console_uid" sudo -u "$console_user" open -b "$bundle_id" >/dev/null 2>&1 || open_status=$?
+  else
+    open -b "$bundle_id" >/dev/null 2>&1 || open_status=$?
+  fi
+
+  if [[ $open_status -eq 0 ]]; then
+    echo "Application '$bundle_id' relaunched successfully."
+  else
+    echo "Failed to relaunch application '$bundle_id'."
+  fi
+}
+
+quit_and_track_1password || exit 1
+installer -pkg "$INSTALLER_PATH" -target / || exit $?
+relaunch_application 'com.1password.1password'

=== Uninstall Script (no changes) ===

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.

🟡 Changes recommended

The described regression-test update is missing, leaving the renamed quit helper outside the existing failure-guard check.

1 open issue
What changed in this PR

This PR fixes Fleet’s macOS 1Password maintained-app installation when the default menu-bar setting prevents AppleScript from fully quitting the app.

Changes:

  • Sends SIGTERM and waits up to 10 seconds for 1Password to exit before installing.
  • Relaunches a previously running app after successful installation and preserves installer failures.
  • Refreshes the generated install script and reference without changing the version or uninstall script.
File Description
ee/​maintained-apps/​outputs/​1password/​darwin.json Updates the embedded install script and its reference.
ee/​maintained-apps/​inputs/​homebrew/​scripts/​1password_install.sh Adds SIGTERM shutdown, conditional relaunch, and installer error handling.

Review effort: Balanced

@allenhouchins
allenhouchins merged commit e4f7c8b into main Oct 23, 2026
17 of 18 checks passed
@allenhouchins
allenhouchins deleted the allenhouchins/1password-quit-failure-d4044d branch October 23, 2026 17:41
allenhouchins added a commit that referenced this pull request Oct 23, 2026
Regenerated outputs/1password/darwin.json to keep the install script ref from #56452 alongside this branch's uninstall script ref.
allenhouchins added a commit that referenced this pull request Oct 23, 2026
…one is logged in (#56457)

**Related issue:** Combines #56016, #56314, #56333, #56336, #56337,
#56453 and #56454 into one PR. Those PRs are closed in favor of this
one. Follow-up to #55157 (#54170) and #54210. #56333 came up in review
of #56085 (Logitech G HUB).

## Summary

All seven change how macOS FMA scripts are generated or tested. Fleet
runs these scripts as root at any time, including at the login window.

### Quit apps by bundle ID when the cask lists names (#56016)

A few casks list app names in `uninstall quit:` instead of bundle IDs.
`tell application id "DeskTime"` fails with -1728, so `quit_application`
treated the app as not running and the script removed the bundle while
the app was open. A dotless `quit:` value now becomes the app's
`unique_identifier`, and repeated IDs in a stanza are quit once. A
dotless value with `*` (such as `*WaveLink*`) is still a pattern.
Affected: DeskTime, DisplayLink, NotesOllama and Fathom.

### Stop the uninstall when the app won't quit (#56314)

The uninstall `quit_application` helper had the bug that #55157 fixed in
the install helper. It confirmed the quit with `pgrep -f "$bundle_id"`,
which never matches, and only logged "did not quit" before `sudo rm -rf`
removed the bundle.
- `quit_application` now polls `osascript … is running` after a 1s
sleep, and returns 1 at the 10s timeout. `quit_matching_applications`
returns 1 if any match didn't quit.
- Generated scripts add `|| exit 1` only when the quit ID names the app
being uninstalled: its `unique_identifier`, case-insensitively, or a
wildcard that matches it. Helpers, shared updaters such as Microsoft
AutoUpdate, and other vendors' apps keep Homebrew's warn-and-continue. A
helper that launchd relaunches would otherwise block the uninstall for
good.
- With #56016, a name in `quit:` becomes the app's own ID first, so
DeskTime, DisplayLink, NotesOllama and Fathom stop too.
- The six custom uninstall scripts with stale copies of the helper now
carry the current one: Box Tools, CleanMyMac, ForkLift, GPG Suite,
Microsoft Word and P4V.

**Behavior change:** if the app won't quit, the uninstall fails with
"Application '<id>' did not quit." It no longer removes the app while
the app is running.

### Quit the app when the cask doesn't (#56337)

Generated uninstall scripts only quit the IDs in the cask's `uninstall
quit:`. Most casks list none, and Excel, Outlook, PowerPoint and Teams
quit only Microsoft AutoUpdate. So the app could be removed while it was
still running.
- The generator now writes `quit_application '<unique_identifier>' ||
exit 1` once, before anything removes the bundle. With an uninstall
stanza, it goes at the stanza's quit step: after `launchctl` and the
cask's own quits, and before `signal`, `early_script`, `script`,
`pkgutil` and `delete`. Without one, it goes right before the app's `rm
-rf`. A zap quit doesn't count, because zap runs after the app is
removed.
- It's left out when an uninstall stanza already quits or signals the
app. 17 casks signal apps that don't quit cleanly, such as DBeaver,
Parallels, Webex and Zoom for IT Admins. A quit that stops the script
mustn't run ahead of the signal. Perimeter 81 splits its stanza across
three artifacts, so every stanza is checked first.
- A cask quit that lists the app's name counts as quitting the app,
through #56016's mapping. So DeskTime, DisplayLink, Fathom and
NotesOllama aren't quit twice.
- Vendor `script` and `early_script` directives aren't exempt. A
directive doesn't say whether it stops the app, and the install script
already quits the same ID with `|| exit 1` on every update.
- Some apps' `unique_identifier` differs from the cask's quit ID, such
as KeePassXC, CleanShot X, Tower, JASP and Sequel Ace. They now also
quit the `unique_identifier`, which is the ID Fleet sees installed. If
LaunchServices doesn't know that ID, the helper returns 0.

The new-fma skill's guidance for custom uninstall scripts moved to
#56459.

### Trash cleanup with no GUI user (#56333)

At the login window, `LOGGED_IN_USER` is `loginwindow`, so `trash()`
tried to move every path into `/Users/loginwindow/.Trash` and failed.
When the user was empty, the unquoted variable disappeared and the
arguments shifted. Either way, system paths like `/Users/Shared/LGHUB`
were left behind.
- Generated calls quote the user: `trash "$LOGGED_IN_USER" '<path>'`.
- With no real console user (empty, `root` or `loginwindow`), `~` paths
are skipped with `Not logged into a non-root GUI; skipping <path>.`
Other paths are deleted with `rm -rf`, and globs still expand through
`compgen -G`. These are only the cask's app-specific `zap` entries. None
of the absolute paths in the outputs is a shared parent folder.
- Cleanup stays best-effort. A failed removal is logged, and `trash()`
always returns 0. Before, the exit status depended on which path
happened to be last.
- Nine custom uninstall scripts carry the current `trash()` verbatim,
with quoted call sites: Camtasia, CleanMyMac, Cycling '74 Max, ForkLift,
GPG Suite, Homebrew, KiCad, Microsoft Word and Teleport.

### Script directives with no GUI user (#56336)

Cask `script` and `early_script` directives were generated as `(cd
/Users/$LOGGED_IN_USER && …)`. At the login window the `cd` failed, so
vendor uninstallers never ran (Anka, Cloudflare WARP, Chrome Remote
Desktop Host, Microsoft Defender, Santa, Mullvad VPN and 11 more).
- Executable directives run as root, so they now start in the user's
home when there is one and in `/` otherwise: `(cd
"/Users/$LOGGED_IN_USER" 2>/dev/null || cd /; sudo '<exe>' …)`.
`must_succeed: false` still appends `|| true`.
- String directives run as the user, so they're skipped with the same
"Not logged into a non-root GUI" message when there's no real user. Box
Drive is the only output with one.
- Box Drive's `pre_uninstall_scripts` run their per-user steps only when
someone is logged in. With an empty user, the old sudoers line wasn't
valid syntax. GPG Suite's custom uninstaller call uses the same `cd … ||
cd /` form.

### Quit 1Password with SIGTERM in the uninstall (#56453)

With "Keep 1Password in the menu bar" on (the default), 1Password's
Electron `before-quit` handler cancels a quit event and drops to the
menu bar. Its `SIGTERM` handler quits it fully, the same as Quit in its
menu bar menu. Without a custom script, the bot would regenerate
1Password's uninstall with `quit_application 'com.1password.1password'
|| exit 1`, and every uninstall with 1Password running would fail with
"did not quit."
- `1password_uninstall.sh` is this branch's generated uninstall with one
change: `quit_application` is replaced by `quit_1password`. It sends
`pkill -TERM -x 1Password`, polls `is running` for up to 10s, and
returns 1 if 1Password is still running. The call is `quit_1password ||
exit 1`, right before `rm -rf`.
- `quit_1password` is the install script's `quit_and_track_1password`
(#56452) without the relaunch tracking.
- The script was built from this branch's generator, so it already has
the #56333 `trash()` and quoted call sites.

### Check every `quit_*` call in custom install scripts (#56454)

`TestCustomInstallScriptsStopWhenAppWontQuit` only matched
`quit_application` and `quit_and_track_application`. It now matches any
`quit_*` call, so a script with its own quit function, like
`quit_and_track_1password` (#56452), can't drop `|| exit 1` unnoticed.
`qgis-ltr-install.sh` is allowlisted: its `quit_app_at_path` sends
SIGTERM, then SIGKILL after 10 seconds.

## Merge order

- #56459 (new-fma skill guidance, split out of #56337) should merge
after this PR.
- #55865 (`rmdir` with `~`) also changes `scripts.go` and many of the
same outputs. Whichever merges second needs a rebuild of the overlapping
outputs.

## Outputs

975 of 980 macOS outputs change. In each one, only the uninstall script
and its ref change. Version, URL, sha256, install script and queries are
byte-identical to `main`.

- **Generated outputs:** they have the #56333 and #56336 changes,
applied to the uninstall script that's on `main` today. The 116 outputs
the bot bumped since #56333 was cut were rebuilt from `main`'s file the
same way. LM Studio and Logitech G HUB were added after #56333 was cut,
so they're new in this PR. The edit reproduces all 955 generated outputs
in #56333 and #56336 byte for byte.
- **#56314's quit change isn't applied to generated outputs.** That
follows #56314 and the precedent of #55157, #43842 and #42951: the FMA
bot picks up the new uninstall refs for the apps that quit something.
For 35 of those apps, the bot also adds #56337's quit, for example
Excel, Outlook, PowerPoint and Teams.
- **#56337's quit is applied to 589 outputs.** None of them quits
anything yet, and each already matched this branch's generator. So each
gains only `quit_application '<unique_identifier>' || exit 1` and the
helper. Eight are frozen apps, which the bot never rewrites: BrickLink
Studio, Cisco Jabber, Comet, Mobirise, Twobird, Usage, Visual Paradigm
and Whispering. Angry IP Scanner, InVesalius and KeyboardCleanTool
weren't changed by the trash or directive edits, so they're new in this
PR.
- **Frozen outputs (14):** hand-edited with the trash and directive
changes, since ingest never rewrites them. Logitech G HUB is newly
frozen since #56333. As in #56314, frozen apps keep the old quit helper
until they're unfrozen. Avast Secure Browser and Intune Company Portal
also don't get #56337's quit until then.
- **Custom-script outputs:** each uninstall script equals its input
file. Microsoft Word was rebuilt on `main`'s newer version. 1Password's
uninstall now comes from its new custom script (#56453). `go run
./cmd/maintained-apps --slug=1password/darwin` matches the committed
output, and its install script is unchanged from `main`.
- **DeskTime, DisplayLink, Fathom, NotesOllama:** #56016's regenerated
quit line, plus the trash change.

Generator cross-check: I ran the combined ingester for 20 apps,
including the four from #56016, LM Studio, Logitech G HUB, Box Drive,
Cloudflare WARP, Docker Desktop, the custom-script apps and a bot-bumped
app. Every uninstall script is identical to this PR's, except for
#56314's quit helpers and `|| exit 1` on the app's own quit, which the
bot adds.

#56337 cross-check: I generated all 964 non-custom macOS uninstall
scripts from today's cask API with this branch's generator, before and
after #56337. 624 change, each by one `quit_application
'<unique_identifier>' || exit 1` line, plus the helper in the 587 that
didn't have it. The next statement is always a removal: `sudo rm -rf`
(553), `remove_pkg_files` (60) or the cask's uninstall script (8).
DeskTime, DisplayLink, Fathom and NotesOllama don't change. For Asana,
Google Chrome, Microsoft Defender and Obsidian, `go run
./cmd/maintained-apps` produces the same uninstall script as the
committed output.

# Checklist for submitter

- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
  - None. The combined PRs' changes files were removed.

- [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.
- Cask-supplied paths, executables, args and string scripts still go
through `shellSingleQuote`. The only new interpolation is the quoted
`"$LOGGED_IN_USER"`.
- [x] Timeouts are implemented and retries are limited to avoid infinite
loops

## Testing

- [x] Added/updated automated tests
- `TestUninstallScriptStopsWhenAppWontQuit`, `TestQuitTargetsApp`,
`TestCustomUninstallScriptsUseCurrentQuitHelper` (#56314)
  - `TestUninstallScriptQuitsAppByBundleIDWhenCaskListsNames` (#56016)
- `TestUninstallScriptTrashWithoutGUIUser`,
`TestCustomUninstallScriptsUseCurrentTrash` (#56333)
  - `TestUninstallScriptScriptDirectiveWithoutGUIUser` (#56336)
- `TestCustomInstallScriptsStopWhenAppWontQuit` checks every `quit_*`
call (#56454)
- `TestUninstallScriptQuitsAppCaskDoesNotName`,
`TestUninstallScriptQuitsAppOnce` (#56337), and a quit-by-name case in
`TestUninstallScriptQuitsAppByBundleIDWhenCaskListsNames` that fails if
the app is quit twice
- 1Password's uninstall (#56453): I ran it with `/bin/bash` 3.2 and
`osascript`, `pkill`, `sudo`, `launchctl`, `stat`, `scutil`, `mv` and
`rm` stubbed. The `osascript` stub ignores quit events, as 1Password
does in menu-bar mode. Not running: no `pkill`, the app is removed, exit
0. Quits on SIGTERM: one `pkill -TERM -x 1Password`, the app is removed,
exit 0. Ignores SIGTERM: "did not quit", exit 1, nothing is removed.
shellcheck reports the same warnings as the generated script. The
SIGTERM behavior was checked on a real Mac for #56452.
- `go test ./ee/maintained-apps/... ./cmd/maintained-apps/...` passes
with Homebrew bash 5.3 and with `/bin/bash` 3.2 first on `PATH`.
golangci-lint (full and incremental) reports 0 issues on those packages.
Two incremental findings in #56314's tests now use `errors.AsType` and
`require.Contains`, like the other tests. A gosec G602 false positive in
#56337's order check is restructured.
- [ ] QA'd all new/changed functionality manually

## AI

**AI:** Claude Code (claude-opus-5-5)

---------

Co-authored-by: Claude <noreply@anthropic.com>

This branch was previously deployed

1 inactive deployment
Docker Hub — 56efff06 Deployed Oct 23, 2026 by allenhouchins via publish #110614
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