Skip to content

Patch policies: Prompt end users before app updates on macOS #39178

Description

@noahtalerman

Goal

User story
As an IT admin patching software on macOS,
I want to notify end users that app will close to update, 1hr and 5 minutes before update happens.
so that I give my end users time to save their work.

Changes

Product

  • UI changes: Figma
  • CLI (fleetctl) usage changes: No changes
  • YAML changes: [YAML] Patch policies: Notify end users before app updates #50678
  • REST API changes: [API] Patch policies: Notify end users before app updates #50677
  • Fleet's agent (fleetd) changes: No changes
  • Fleet server configuration changes: No changes
  • Exposed, public API endpoint changes: No changes
  • fleetdm.com changes: No changes
  • GitOps mode UI changes: Specified in Figma above.
  • GitOps generation changes: Include new field notify_before_patching in generated YAML files under Fleet-maintained apps.
  • Activity changes: [Activity] Patch policies: Notify end users before app updates #50679
  • Permissions changes: "Add, edit, and delete software" already defined in permissions table.
  • Changes to paid features or tiers: Fleet Premium
  • My device and fleetdm.com/better changes: My device changes specified in Figma.
  • Usage statistics: No changes
  • Other reference documentation changes: No changes.
  • First draft of test plan added
  • Once shipped, requester has been notified
  • Once shipped, dogfooding issue has been filed

Engineering

The delivery half of this story is built as a reusable platform, end_user_notifications, rather than as patch-specific code. Core handles enqueue, dispatch to Fleet Desktop, delivery confirmation, failure causes, and retry. A kind owns what to say, when, and what happens next. notify_before_patching is the first kind. #44672 and #39176 are both funded consumers.

Fleet Desktop already ships the notify command and the toast window from #49325 (PR #50211), so this story needs no Swift changes.

Open engineering questions

Marko answered most of these on 2026-08-11, in comments above and in Slack. What is left:

  1. How the device token reaches the notification script. Interpolating a host's device auth token into script_contents stores a live credential and breaks if the token rotates mid-countdown. Blocks Notify before patching: end-user notifications platform (delivery, device API, outcomes) #50910. Decides: Jonathan, Lucas.
  2. The deferred exit code number is not published. Marko added a notify error for "another notification is displayed" on 2026-08-11 but has not stated the value. Blocks Notify before patching: end-user notifications platform (delivery, device API, outcomes) #50910 and Notify before patching: activity feed, details modals, and automation runs #50915. Decides: Marko.
  3. Fleet Desktop version dependency. The catalog manifest is pinned at 1.4.0, Info.plist says 1.5.0, and the deferred exit code needs a release beyond that. Three versions in play against a 2026-09-11 milestone. Decides: Marko, release.
  4. Boolean is provisional. Marko chose notify_before_patching: bool pending Rachael Shaw. If it becomes an enum, Notify before patching: policy field, validation, and install-skip wiring #50911, Notify before patching: End user experience dropdown across software and policy forms #50914, and Notify before patching: GitOps and generate-gitops support #50917 all change field names.
  5. [Activity] Patch policies: Install new version when app is closed #49106 names two fields wrong, and it is merged. Answering Marko's Slack question: the payload field is install_skipped_when_app_open and the policy field is patch_when_closed. skipped_install did ship, but as the frontend display status derived from the flag, not as a payload field. patch_only_when_closed never existed in code. So it is a doc fix, not a code rename, and 4.91 has not shipped yet. Handled in Notify before patching: documentation and engineering QA #50918.

Notification outcomes in the UI

No reason field. The activity carries script_execution_id, and the details modal calls GET /api/v1/fleet/scripts/results/{execution_id} for the exit code and output, picking its copy from the exit code (Figma dev note 5582:46519).

A deferral, where another toast was already on screen, arrives two ways. The binary reports the new exit code Marko added on 2026-08-11, with a script_execution_id. When the dispatcher catches it first no script runs at all, so absence of script_execution_id identifies it. That is unambiguous only because this kind sets no expiry, making a dispatcher-caught deferral the only outcome that emits an activity without a script run. Both render the same sentence.

The app-open skip keeps using installed_software with the skip flag for both options, told apart by pre_install_query_output.

Validation rules

Marko specified the exact user-facing text in a dev note on #50678. All three apply to the API and to fleetctl gitops.

Rule Message
Both flags true Only one of "patch_when_closed" or "notify_before_patching" can be set to true
Explicit continuous_automations_enabled: false If "notify_before_patching" is true, "continuous_automations_enabled" can't be set to false.
Windows Fleet-maintained app "notify_before_patching" is available for macOS Fleet-maintained apps. It's coming soon to Windows.

The second rejects an explicit false rather than overriding it silently. Omitting the field still auto-sets it. This mirrors patch_when_closed at server/service/global_policies.go:259 and pkg/spec/gitops.go:2082.

Dependency

Fleet Desktop 1.5.0 or newer is required on the host. The Fleet-maintained app manifest at ee/maintained-apps/outputs/fleet-desktop/darwin.json is still pinned at 1.4.0. The catalog has to pick up 1.5.0 before this feature works for anyone.

ℹ️  Please read this issue carefully and understand it. Pay special attention to UI wireframes, especially "dev notes".

Risk assessment

  • Requires testing in a hosted environment: No. Everything reproduces on a local Fleet with one enrolled Mac.
  • Requires load testing: Yes.
  • Risk level: High
  • Risk description: Three reasons. First, a new cron on a one-minute interval permanently changes the load profile. Second, the feature can quit an application the end user is working in, so a bug in either verify pass or in the per-app dedup guard closes an app without warning, or closes one the end user already updated. Third, it extends the app-open skip path added by Patch policies: Install new version when app is closed #39962, so a regression there affects patch_when_closed as well as this story. server/service/orbit_test.go:1368 is the guard test to watch.

Test plan

Make sure to go through the list and consider all events that might be related to this story, so we catch edge cases earlier.

Core flow

  • Add a Fleet-maintained app with notify before patching enabled. With the host online, the app open, and the policy failing, confirm the end user gets the 1 hour notification, then the 5 minute reminder, and the app closes and updates after that.
  • Same setup but with the app closed. Confirm it updates straight away and the end user is never notified.
  • Add three patch policies with notify before patching enabled and all three apps open. Confirm the end user gets one notification listing all three, not three separate ones.
  • Get the 1 hour notification, then go offline for a few hours. Confirm nothing updates while offline, and that a fresh 1 hour notification appears when the host comes back rather than the apps updating unannounced. Cycle is repeated 1hr notification, 5min reminder, install.
  • Get the notification, then update one of the listed apps yourself through My device > Self-service. Confirm it drops out of the 5 minute reminder and is not installed, while the others still update.
  • Get the notification, then update all the listed apps yourself. Confirm no 5 minute reminder appears, and nothing is installed.
  • While one notification's hour timer is still running, add a patch policy for another app and refetch the host. Confirm the new app gets its own separate notification and its own hour, and that the first apps still update on its original schedule.
  • Lock the screen, then refetch the host. Confirm no notification appears, nothing is installed, and the notification arrives on a later attempt once unlocked.
  • Log out entirely, then refetch the host. Confirm nothing is installed, and the notification arrives after logging back in.
  • Uninstall Fleet Desktop, then refetch the host. Confirm nothing is installed and Fleet reports it could not notify.
  • Confirm the countdown starts when the notification actually appears on screen, not when Fleet decided to send it.
  • Add a patch policy without notify before patching. Confirm it installs straight away with no notification, alongside policies that do notify.
  • With a notification on screen, click the Fleet Desktop icon in the Dock. Confirm the self-service window comes to the front and there is only one Fleet Desktop icon in the Dock.
  • Get 1 hour notification, click Update now, make sure that only Hide button is available in the toast. Install progress is visible. Timer is canceled, no 5 minute reminder.
  • Make sure the toast window notification UI respects the system theme set in macOS settings (dark / light mode).

Edge cases

  • TODO

Supplemental testing

Testing notes

Confirmation

  1. Engineer: Added comment to user story confirming successful completion of test plan (include any special setup, test data, or configuration used during development/testing if applicable).
  2. QA: Added comment to user story confirming successful completion of test plan.
  3. QA: Determined whether this story needs Playwright automation.
    • Needs automation: Yes / No
    • If yes, filed a follow-up issue in the :help-qa project with status "Needs automation":

Metadata

Metadata

Assignees

Type

No type

Projects

Status
No status
Status
🥚 Ready

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions