Repository navigation
Only show Nudge once an update is prepared - #51546
Merged
Merged
Conversation
Nudge currently starts nagging as soon as a device is behind, which means the end user clicks Update and then waits through a download. Gate it on the OS having already staged the update, so the install is quick when they finally act. DDM activations only gate declarations, so the existing Nudge mobileconfig is wrapped in a com.apple.configuration.legacy declaration and paired with a com.apple.activation.simple activation whose predicate is @status(softwareupdate.install-state) == 'prepared'. The mobileconfig comes out of configuration_profiles and stays in the repo as the hosted payload. Apple only re-fetches ProfileURL when the declaration's ServerToken changes, so the URL carries a ?v=N cache-buster: bump it whenever the mobileconfig changes or devices keep the old profile.
fleet-release
approved these changes
Aug 19, 2026
allenhouchins
temporarily deployed
to
Docker Hub
August 19, 2026 14:58 — with
GitHub Actions
Inactive
fleet-release
approved these changes
Aug 19, 2026
fleet-release
approved these changes
Aug 19, 2026
fleet-release
approved these changes
Aug 24, 2026
allenhouchins
temporarily deployed
to
Docker Hub
August 24, 2026 17:31 — with
GitHub Actions
Inactive
fleet-release
approved these changes
Sep 2, 2026
allenhouchins
temporarily deployed
to
Docker Hub
September 2, 2026 01:58 — with
GitHub Actions
Inactive
fleet-release
approved these changes
Sep 3, 2026
allenhouchins
temporarily deployed
to
Docker Hub
September 3, 2026 01:44 — with
GitHub Actions
Inactive
fleet-release
previously approved these changes
Sep 3, 2026
allenhouchins
temporarily deployed
to
Docker Hub
September 3, 2026 02:39 — with
GitHub Actions
Inactive
…he hosted preferences profile gets its own identifier
fleet-release
approved these changes
Sep 5, 2026
allenhouchins
temporarily deployed
to
Docker Hub
September 5, 2026 03:44 — with
GitHub Actions
Inactive
fleet-release
previously approved these changes
Sep 5, 2026
Removed comments regarding profile delivery and updates.
fleet-release
previously approved these changes
Sep 6, 2026
allenhouchins
had a problem deploying
to
Docker Hub
September 6, 2026 02:57 — with
GitHub Actions
Error
Condensed comments regarding Nudge preferences and their activation process.
fleet-release
previously approved these changes
Sep 6, 2026
fleet-release
previously approved these changes
Sep 25, 2026
allenhouchins
temporarily deployed
to
Docker Hub
September 25, 2026 02:04 — with
GitHub Actions
Inactive
allenhouchins
marked this pull request as draft
September 25, 2026 02:06
1 task
The staged-update gate can't open on Macs that aren't supervised (no softwareupdate.install-state) and doesn't fit macOS 15, whose deadline is a major upgrade. Those Macs get an ungated copy of the preferences, with its own identifier, via two new labels; the declaration excludes them. Also ports the macOS 15 latest-supported rule and 2026-12-13 deadline from #54130 into nudge-preferences.mobileconfig. Co-Authored-By: Claude <noreply@anthropic.com>
The previous pin (57ffa20) predated the macOS 27 aboutUpdateURL fix and the macOS 15 deadline. Co-Authored-By: Claude <noreply@anthropic.com>
fleet-release
approved these changes
Oct 9, 2026
fleet-release
previously approved these changes
Oct 9, 2026
fleet-release
previously approved these changes
Oct 9, 2026
allenhouchins
had a problem deploying
to
Docker Hub
October 9, 2026 14:11 — with
GitHub Actions
Error
Co-Authored-By: Claude <noreply@anthropic.com>
fleet-release
previously approved these changes
Oct 9, 2026
allenhouchins
had a problem deploying
to
Docker Hub
October 9, 2026 14:13 — with
GitHub Actions
Error
Co-Authored-By: Claude <noreply@anthropic.com>
fleet-release
previously approved these changes
Oct 9, 2026
allenhouchins
temporarily deployed
to
Docker Hub
October 9, 2026 14:14 — with
GitHub Actions
Inactive
# Conflicts: # it-and-security/default.yml
fleet-release
approved these changes
Oct 19, 2026
allenhouchins
temporarily deployed
to
Docker Hub
October 19, 2026 20:59 — with
GitHub Actions
Inactive
allenhouchins
marked this pull request as ready for review
October 20, 2026 14:29
fleet-release
approved these changes
Oct 20, 2026
2 tasks done
This branch was previously deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Dogfood-only GitOps change. On supervised Macs running macOS 26 or later, Nudge now appears only after macOS has already downloaded and staged an update, so an end user who clicks Update gets a quick install instead of sitting through a download first. Macs that can't use that gate keep getting Nudge's preferences unconditionally, as they do today.
What changed
The old
nudge-configuration.mobileconfigcarried two payloads: Nudge's preferences and acom.apple.servicemanagementrule that keeps Nudge's launch agent a managed login item. Only the preferences should be gated on a staged update, so the profile is split in two.lib/macos/configuration-profiles/nudge-background-task.mobileconfig(new) — thecom.apple.servicemanagementrule forcom.github.macadmins.Nudge, on its own. Fleet delivers it unconditionally, same pattern asgoogle-updater-background-task.mobileconfigandfleet-desktop-launch-agent.mobileconfig.lib/macos/configuration-profiles/nudge-preferences.mobileconfig(renamed fromnudge-configuration.mobileconfig) — the Nudge preferences payload only, with a newPayloadIdentifier(com.fleetdm.profile.nudge-preferences) and fresh UUIDs. This file is no longer delivered by Fleet; it is the hosted payload the declaration below points at. Its settings matchmain, including the macOS 15latest-supportedrule and 2026-12-13 deadline from Nudge: add macOS 27 target, OS-specific update URLs, and macOS 15 upgrade deadline #54130.lib/macos/configuration-profiles/nudge-preferences-ungated.mobileconfig(new) — the same settings with its own identifier (com.fleetdm.profile.nudge-preferences-ungated), delivered directly by Fleet to the Macs that skip the gate.lib/macos/declaration-profiles/Nudge settings.json(new) — acom.apple.configuration.legacydeclaration whoseProfileURLpoints atnudge-preferences.mobileconfig, pinned to a commit.lib/macos/declaration-profiles/Nudge activation.json(new) — acom.apple.activation.simpleactivation gating that declaration on@status(softwareupdate.install-state) == 'prepared'.lib/all/labels/macos-15-and-older-hosts.ymlandlib/all/labels/macs-not-enrolled-through-ade.yml(new) — dynamic labels for the Macs that skip the gate, added todefault.yml.fleets/workstations.yml—nudge-configuration.mobileconfigcomes out ofconfiguration_profiles. In gonudge-background-task.mobileconfig, the declaration (withactivation:attached, excluding both labels), and the ungated profile (including both labels).Which Macs skip the gate
softwareupdate.install-stateis supervised-enrollments-only on macOS, and macOS only supervises ADE enrollments, so the predicate could never be true. The label uses the macadminsmdmtable (installed_from_dep = 'false'); a Mac reportingunknownstays on the gated path.prepared. Nudge's ownattemptToFetchMajorUpgradecan't help either, because Nudge only gets its settings once the gate opens.The ungated copy needs its own identifier for the same reason the hosted one does (below): a Mac that moves between the two paths, such as one upgrading from macOS 15 to 27, gets a
RemoveProfilefor one identifier and an install for the other, and they must not collide. The YAML comment inworkstations.ymlsays to keep both files' settings identical.Why the identifier changed
The same GitOps apply that adds the declaration also removes the old
com.fleetdm.nudge.managedprofile, which queues aRemoveProfilefor that identifier. MDM commands and the DDM channel are delivered independently, so if the device applied the legacy declaration before draining the command queue, a hosted profile with the same identifier would have been deleted by the removal — with the declaration still reporting Verified and nothing ever re-applying it. Giving the hosted profile its own identifier means the removal can only ever touch the old copy.Why the background-task rule is separate
If the servicemanagement payload were inside the gated profile, it would be installed and removed on every update cycle: a recurring "Background Items Added" notification for every user, and a window where they could disable Nudge's login item. Keeping it in an always-on profile means it does not matter when, or whether, the Nudge preferences activate.
Why it's shaped this way
DDM activations gate declarations, and Nudge's settings are a mobileconfig, so the profile has to be wrapped in
com.apple.configuration.legacy. That declaration'sProfileURLmust be anhttps://URL, and Fleet has no endpoint that serves a mobileconfig, so this points at the file's ownraw.githubusercontent.comURL.On hosting: Apple's v26 schema said the URL "must be hosted by the MDM server." The Release-v27.0 schema removed that and now says the request "uses MDM semantics, which includes the device-identity certificate." An Apple engineer on the developer forums describes
ProfileURLas expected to be on the MDM server, with the device presenting its identity certificate. GitHub doesn't request a client certificate, so the fetch should succeed, but Apple doesn't document this setup. On macOS 27,ProfileAssetReferencewith an asset usingNoneauthentication is Apple's supported way to host the profile elsewhere, and it verifies a hash. That's the path to move to once these Macs are all on 27.The
ProfileURLis pinned to a commit SHA, notmain. A device only re-downloadsProfileURLwhen the declaration's ServerToken changes, and editing the mobileconfig doesn't touch the declaration. Pointing atmainwould mean edits to Nudge's settings silently never reach devices. Pinning to the commit that last changednudge-preferences.mobileconfigmakes the URL immutable and gives every edit a new URL for a real reason: after changing the mobileconfig, commit it, then update the SHA inNudge settings.jsonto that commit. Forgetting the second step leaves devices on the old copy. This is documented in a YAML comment beside the entry inworkstations.yml, since the next person to edit Nudge settings will open the mobileconfig, not the declaration. Commits on a squash-merged PR branch keep resolving onraw.githubusercontent.comafter the branch is deleted (checked against #54130's head commit).What I verified
plutil -lintand Fleet's ownNewMDMAppleConfigProfileandValidateUserProvided(which runsScreenPayloads) checks.MDMAppleRawDeclaration.ValidateUserProvidedandMDMAppleRawActivation.ValidateUserProvidedagainst the declaration's identifier.default.ymlandworkstations.ymlparse throughspec.GitOpsFromFilewith no profile or label errors.nudge-preferences.mobileconfigandnudge-preferences-ungated.mobileconfigmatchmain'snudge-configuration.mobileconfigapart from identifiers, UUIDs, and the removed servicemanagement payload.nudge-preferences.mobileconfigon this branch, and a trial merge withmain(git merge-tree) is clean and leaves that file unchanged.'prepared'is the exact lowercase value in Apple'ssoftwareupdate.install-stateschema.Please don't merge before a device test
Three things I couldn't check without hardware, roughly in order of risk:
text/plainwithx-content-type-options: nosniff. Both will probably just work, but if not, the device should report the configuration invalid and Fleet shows it as Failed.install-stateactually reachpreparedhere?macos_updatesis empty inworkstations.yml, so nothing enforces updates via DDM; this relies onAutomaticActions.Download: AlwaysOnfromSoftware Update settings.json. Apple'sinstall-reasonschema listsauto-updateas a valid trigger, so it should — but if not, we'd need an enforcement declaration to drive the download. A false predicate looks like Verified in Fleet, so the test needs to confirm a Mac with a staged update actually receives the preferences profile.Checklist
No changes file: dogfood GitOps config, not a user-visible product change.
AI
AI: Claude Code (claude-opus-5-5)
Summary by CodeRabbit