Skip to content

Auto-update, pin, and rollback Fleet-maintained apps via UI and GitOps - #48293

Merged
georgekarrv merged 15 commits into
mainfrom
feat/38504-auto-update-pin-rollback-fma
Jun 26, 2026
Merged

Auto-update, pin, and rollback Fleet-maintained apps via UI and GitOps#48293
georgekarrv merged 15 commits into
mainfrom
feat/38504-auto-update-pin-rollback-fma

Conversation

@cdcme

@cdcme cdcme commented Jun 25, 2026

Copy link
Copy Markdown
Member

Related issue: Resolves #38504

Constituent PRs (merged into this feature branch):

Checklist for submitter

  • Changes file added for user-visible changes in changes/, orbit/changes/ or
    ee/fleetd-chrome/changes. See Changes files for more information.

  • 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

  • If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes

Testing

Summary by CodeRabbit

  • New Features
    • Added Fleet-maintained app version pinning (Latest, exact, and major) via a new Versions modal.
    • Introduced premium auto-updates for maintained apps with pin-aware promotion and rollback-safe caching.
    • Added expandable library version rows and a Policies modal.
  • Bug Fixes
    • Improved pin handling, cache/manifest hydration, and safer update behavior on per-app failures and deduplication.
  • UI/UX
    • Refreshed the Software title details experience with new accordion/list patterns, redesigned details widget/tooltips, and updated installer presentation.
  • Documentation
    • Expanded Storybook component/page coverage and adjusted Storybook canvas padding.
  • Tests
    • Added/updated unit and integration tests for pinning, auto-update flows, and new modal/UI behavior.

RachelElysia and others added 9 commits June 23, 2026 08:46
…47808)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #46726 

Updates the `PATCH /api/v1/fleet/software/titles/:id/package` endpoint
to be able to set the pinned version in the `software_title_team_pins`
table and set the active installer if it changed.

`GET /software/titles/:id` returns the pinned version for a title from
that table now.

A few small fixes and updates that are related to this feature but not
strictly in the scope of the subtask are also included (see individual
comments).

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [ ] 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.

- [ ] 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
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually

## New Fleet configuration settings

This setting is already handled by gitops, this PR adds control of it
with the update software installer api

- [ ] Setting(s) is/are explicitly excluded from GitOps

If you didn't check the box above, follow this checklist for
GitOps-enabled settings:

- [ ] Verified that the setting is exported via `fleetctl
generate-gitops`
    - TODO in another PR
- [ ] Verified the setting is documented in a separate PR to [the GitOps
documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485)
- [ ] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
- [ ] Verified that any relevant UI is disabled when GitOps mode is
enabled


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

## Summary by CodeRabbit

* **New Features**
* Added version pinning for Fleet-maintained app installers, allowing
teams to lock to specific versions or clear pins to use "Latest"
* Policies automatically repoint to the newly active installer when a
version pin is changed
* Support for semantic version syntax including caret-style version
constraints

* **Tests**
* Added comprehensive integration test for Fleet-maintained app version
pinning workflows and validation rules

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #47679 
Adds a few things:
- exports pinned version in `generate-gitops` enclosed in double quotes
- adds `pinned_version` to the edited software activity. When set to a
full or major version it shows up in details, when set to latest or
unchanged it shows up as `pinned_version: null` (some other fields like
display_name also dont show up when unchanged)
- fixes a bug where some FMA's like google chrome couldn't be pinned to
major version because they couldn't be converted to semver (by just
splitting the version on periods instead of converting to semver)


# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [ ] 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.

- [ ] 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
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually

## New Fleet configuration settings

- [ ] Setting(s) is/are explicitly excluded from GitOps

If you didn't check the box above, follow this checklist for
GitOps-enabled settings:

- [x] Verified that the setting is exported via `fleetctl
generate-gitops`
- [ ] Verified the setting is documented in a separate PR to [the GitOps
documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485)
- [ ] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
- [ ] Verified that any relevant UI is disabled when GitOps mode is
enabled


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

## Summary by CodeRabbit

* **New Features**
* Software titles now support version pinning in GitOps exports for
fleet-managed applications, with pinned version values properly
formatted as quoted strings in the exported YAML configuration
* Activity logs now record when pinned version information is modified
during software editing operations

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
**Related issue:** Resolves #47215

  ## Summary

Pinning an FMA to a version via GitOps only resolved against versions
already cached on the instance. Bumping a pin to a freshly released
version failed with `specified version is not available` because Fleet
never downloaded a pinned version it hadn't cached.

`Hydrate` now falls back to the published app manifest on a cache miss.
When the requested version is currently published, Fleet downloads and
caches it, so an admin can bump a pin to a new version in a single
apply. A version that's neither cached nor published still returns the
same error.

  # Checklist for submitter

- [x] Timeouts are implemented and retries are limited to avoid infinite
loops

  ## Testing

  - [x] Added/updated automated tests
  - [x] QA'd all new/changed functionality manually
**Related issue:** Resolves #47623

  ## Summary

Wires up the Fleet-maintained app version-pinning UI on the software
title details page:

- **Versions modal** — opened from Actions ▾ → Versions or the active
row's version badge. Choose *Automatically update to latest*, pin to an
exact version, or pin to a major version; pre-selected from the current
pin, and Save is disabled with a tooltip in GitOps mode.
- **Multi-row Library** — one accordion row per cached version; the
active/pinned row is badged (Latest / Pinned / Major version) and older
versions are dimmed.
- Adds `pinned_version` to the software-package interface and a
`version` field to the installer `PATCH` request.

Frontend only; targets the `feat/38504-auto-update-pin-rollback-fma`
feature branch (backend, layout, and accordion already merged there).

  # Checklist for submitter

  ## Testing

  - [x] Added/updated automated tests
  - [x] QA'd all new/changed functionality manually


https://github.com/user-attachments/assets/f9822cfa-57d2-4f0c-9918-c5ee914f0431
**Related issue:** Resolves #47681

Adds an hourly, Premium-only cron (maintained_apps_auto_update) that
keeps Fleet-maintained apps current. For each FMA-backed active
installer (per team), it fetches the latest manifest, downloads and
caches a newly-published version when the pin allows, and advances the
team's active installer based on the pin state:

- Unpinned (Latest): download/cache the newest published version and
advance the active installer to it.
- Caret pin (^N): advance to the newest version within major N
(downloading it if newly published). Never cross into another major.
  - Literal pin: never advance and never download.

New versions are cached as additional software_installers rows (no
schema change; reuses the existing (global_or_team_id, title_id,
version) index), capped at the two most recent per team for rollback.

  # 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] Added/updated automated tests
  - [x] QA'd all new/changed functionality manually
@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.00858% with 177 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.47%. Comparing base (9ac9104) to head (ce1f85b).
⚠️ Report is 37 commits behind head on main.

Files with missing lines Patch % Lines
ee/server/service/maintained_apps_auto_update.go 66.84% 38 Missing and 25 partials ⚠️
server/datastore/mysql/software_installers.go 74.29% 31 Missing and 24 partials ⚠️
cmd/fleet/cron.go 0.00% 15 Missing ⚠️
ee/server/service/software_installers.go 79.10% 7 Missing and 7 partials ⚠️
...Page/LibraryItemAccordion/LibraryItemAccordion.tsx 94.95% 6 Missing ⚠️
...components/TruncatedTextList/TruncatedTextList.tsx 92.06% 5 Missing ⚠️
.../SoftwareDetailsSummary/SoftwareDetailsSummary.tsx 75.00% 4 Missing and 1 partial ⚠️
cmd/fleet/cron_registration.go 0.00% 3 Missing ⚠️
frontend/services/entities/software.ts 0.00% 2 Missing ⚠️
server/datastore/mysql/software_titles.go 80.00% 1 Missing and 1 partial ⚠️
... and 6 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #48293      +/-   ##
==========================================
+ Coverage   67.31%   67.47%   +0.16%     
==========================================
  Files        3661     3672      +11     
  Lines      231523   232687    +1164     
  Branches    12301    12256      -45     
==========================================
+ Hits       155849   157006    +1157     
+ Misses      61725    61592     -133     
- Partials    13949    14089     +140     
Flag Coverage Δ
backend 69.01% <71.92%> (+0.12%) ⬆️
frontend 59.03% <94.21%> (+0.37%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cdcme
cdcme marked this pull request as ready for review June 25, 2026 19:19
@cdcme
cdcme requested review from a team as code owners June 25, 2026 19:19
Copilot AI review requested due to automatic review settings June 25, 2026 19:19

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

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.

Warning

  • Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.

Pull request overview

This PR extends Fleet-maintained app management to support auto-updating, version pinning, and rollback via the UI and GitOps. It wires new backend pin state into GET /software/titles/:id, extends the installer update flow to accept a version pin, adds an hourly premium-only auto-update cron, and introduces/updates multiple frontend components (Library accordion, Versions modal, policies modal, and supporting UI primitives).

Changes:

  • Backend: add pinned-version retrieval to software title details, extend installer update payloads to support version pinning, and add FMA auto-update cron plumbing.
  • Maintained apps: update Hydrate behavior to fall back to remote manifests on cache misses; add test coverage for new hydration and auto-update behavior.
  • Frontend: add Versions modal + supporting helpers, refactor title details layout (summary/header pills, policies modal), and introduce new UI primitives (Chip, TruncatedTextList) plus Storybook updates.

Reviewed changes

Copilot reviewed 99 out of 104 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
changes/38504-auto-update-pin-rollback-fma Release note entry (content excluded by policy).
server/service/software_titles.go Populate pinned_version on software title detail responses.
server/service/software_installers.go Accept multipart version field and pass through as PinnedVersion.
server/service/integration_vpp_install_test.go Update edited_software activity assertions to include pinned_version.
server/service/integration_software_titles_test.go Update edited_software activity assertions to include pinned_version.
server/mock/datastore_mock.go Extend mock datastore interface with FMA auto-update + pin methods.
server/mdm/maintainedapps/sync.go Allow Hydrate to fall back to manifest on cache miss and select requested version.
server/mdm/maintainedapps/sync_test.go Add unit tests for Hydrate cache-hit/miss behavior and version availability.
server/fleet/software.go Add UploadedAt to cached versions and define FMAAutoUpdateCandidate.
server/fleet/software_installer.go Add PinnedVersion to installer structs/payloads and include in noop detection.
server/fleet/datastore.go Add datastore interface methods for pin management and auto-update cron support.
server/fleet/cron_schedules.go Add cron schedule name constant for maintained apps auto-update.
server/fleet/activities.go Add pinned_version to edited_software activity payload.
server/datastore/mysql/teams_test.go Ensure team deletion tests cover cleanup of software title pins.
server/datastore/mysql/software_titles.go Include uploaded_at in cached version rows; add semver-aware sorting when requested.
frontend/utilities/permissions/permissions.ts Add canWriteSoftware helper for admin/maintainer gating.
frontend/utilities/permissions/permissions.tests.ts Add unit tests for canWriteSoftware.
frontend/test/storybook-utils.tsx Add withFrame decorator helper for Storybook layout.
frontend/services/entities/software.ts Allow editSoftwarePackage to handle Versions modal pin payload (version).
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/VersionsModal.tsx New Versions modal UI and save flow for version pins.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/VersionsModal.tests.tsx Tests for Versions modal defaulting, selection, and save behavior.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/index.ts Export VersionsModal.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/helpers.ts Build version pin radio options and preselection helper.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/helpers.tests.ts Unit tests for Versions modal helpers.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/_styles.scss Styles for Versions modal layout.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/TitleVersionsTable/TitleVersionsTableConfig.tsx Fix VulnerabilitiesCell import path after table relocation.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/TitleVersionsTable/TitleVersionsTable.tsx New Versions section table component.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/TitleVersionsTable/TitleVersionsTable.tests.tsx Tests for Versions section table rendering and empty state.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/TitleVersionsTable/index.ts Export TitleVersionsTable.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/TitleVersionsTable/_styles.scss Styles for Versions section table.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/SoftwareSummaryCard.tsx Refactor summary card: header pills, policies modal trigger, Versions action gating.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareInstallerCard.tsx Remove old installer card implementation (replaced by new Library surfaces).
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx Remove old status table config (superseded by accordion/status UI).
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tsx Remove old status table component.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tests.tsx Remove old status table tests.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/index.ts Remove index export for deleted status table.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/_styles.scss Remove styles for deleted status table.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/InstallerPoliciesTable.tsx Add option to hide count (used by Policies modal).
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/InstallerPoliciesTable.tests.tsx Adjust tooltip hover tests after table changes.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/InstallerDetailsWidget.tsx Add tooltip suppression and installer-type hiding for embedding in accordion rows.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/InstallerDetailsWidget.tests.tsx Remove SHA-256 rendering assertions (hash moved to accordion).
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/_styles.scss Update truncation widths and remove sha256 styles.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/index.ts Remove index export for deleted installer card.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/helpers.ts Remove helpers for deleted installer card actions.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/_styles.scss Remove styles for deleted installer card.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/PoliciesModal/PoliciesModal.tsx New modal listing policies linked to software (from Auto install pill).
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/PoliciesModal/PoliciesModal.tests.tsx Tests for Policies modal rendering and exit behavior.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/PoliciesModal/index.ts Export PoliciesModal.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/PoliciesModal/_styles.scss Styles for Policies modal empty state.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/LibraryItemAccordionList.tsx New wrapper for composing accordion rows as a list.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/LibraryItemAccordion.stories.tsx Storybook stories for accordion row states and permissions.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/index.ts Export LibraryItemAccordion.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/helpers.ts Derive row badge/active state from active + pinned versions.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/helpers.tests.ts Unit tests for row state derivation.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/_styles.scss Accordion/list styling (active/inactive rows, panels, badges).
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/helpers.ts Add buildLibraryVersionRows helper for Library accordion rows.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/helpers.tests.ts Tests for buildLibraryVersionRows.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditIconModal/EditIconModal.tsx Update import for TitleVersionsLastUpdatedInfo after table relocation.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss Adjust section styling structure.
frontend/pages/SoftwarePage/helpers.tsx Make mergePolicies immutable (avoid mutating shared Sets).
frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx Add Versions action, header pills slot, and Apple VPP gitops gating refactor.
frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tests.tsx Extend tests for Versions option and headerPills rendering.
frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/_styles.scss Switch layout to grid and add pills slot styling.
frontend/interfaces/software.ts Export IFleetMaintainedVersion, add uploaded_at + pinned_version to package interface.
frontend/components/TruncatedTextList/TruncatedTextList.tsx New component for truncating a long list into a “+N more” tooltip UI.
frontend/components/TruncatedTextList/TruncatedTextList.stories.tsx Storybook stories for TruncatedTextList.
frontend/components/TruncatedTextList/index.ts Export TruncatedTextList.
frontend/components/TruncatedTextList/_styles.scss Styles for TruncatedTextList.
frontend/components/TooltipTruncatedText/TooltipTruncatedText.tsx Add disableTooltip prop to suppress nested tooltips when parent owns hover.
frontend/components/Tag/Tag.tsx Remove deprecated Tag component (replaced by Chip).
frontend/components/Tag/index.ts Remove Tag export.
frontend/components/Tag/_styles.scss Remove Tag styles.
frontend/components/icons/Tag.tsx Add Tag icon asset.
frontend/components/icons/Pin.tsx Add Pin icon asset.
frontend/components/icons/index.ts Register new Pin/Tag icons.
frontend/components/Icon/Icon.stories.tsx Improve Icon story controls to select from the full icon map.
frontend/components/Chip/index.ts Export Chip component.
frontend/components/Chip/Chip.tsx New Chip component (icons, tooltips, clickable variant).
frontend/components/Chip/Chip.tests.tsx Unit tests for Chip.
frontend/components/Chip/Chip.stories.tsx Storybook stories for Chip.
frontend/components/Chip/_styles.scss Styles for Chip and focus outline behavior.
frontend/components/buttons/Button/Button.tsx Clarify/deprecate text-icon variant and prefer inverse.
ee/server/service/software_installers.go Add pin-only update path, caret parsing helpers, and activity logging for pins.
ee/server/service/software_installers_test.go Update tests for caret parsing and add tests for major matching + pin parsing.
ee/server/service/maintained_apps_auto_update.go Add premium auto-update cron implementation for FMAs (download + promote).
ee/server/service/maintained_apps_auto_update_test.go Tests for auto-update promote logic and error isolation.
cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/fleets/team-a-thumbsup.yml Update GitOps expected output to include pinned version values.
cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamSoftware.yaml Update expected generateSoftware structure to include pinned version.
cmd/fleetctl/fleetctl/generate_gitops.go Ensure pinned versions are exported and quote version: in generated YAML.
cmd/fleetctl/fleetctl/generate_gitops_test.go Update mock software titles to include pinned versions.
cmd/fleet/cron.go Register and define maintained apps auto-update cron schedule.
cmd/fleet/cron_registration.go Register premium cron schedule for maintained apps auto-update.
.storybook/preview.scss Adjust Storybook canvas padding for more realistic layout.
.storybook/preview.js Import Storybook preview stylesheet.
.storybook/main.ts Include SoftwareTitleDetailsPage stories in Storybook build.
Files excluded by content exclusion policy (1)
  • changes/38504-auto-update-pin-rollback-fma

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ee/server/service/maintained_apps_auto_update_test.go
Comment thread ee/server/service/maintained_apps_auto_update_test.go
Comment thread ee/server/service/maintained_apps_auto_update_test.go
Comment thread server/mdm/maintainedapps/sync.go
Comment thread ee/server/service/software_installers.go
Comment thread cmd/fleetctl/fleetctl/generate_gitops.go
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1300af9d-3ae8-4345-844f-6cb9749dc0d6

📥 Commits

Reviewing files that changed from the base of the PR and between d5afa45 and ce1f85b.

📒 Files selected for processing (3)
  • ee/server/service/maintained_apps_auto_update.go
  • ee/server/service/maintained_apps_auto_update_download_test.go
  • server/service/integration_enterprise_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • server/service/integration_enterprise_test.go
  • ee/server/service/maintained_apps_auto_update.go
  • ee/server/service/maintained_apps_auto_update_download_test.go

Walkthrough

Adds Fleet-maintained app auto-update, pinning, rollback, and GitOps version export support across backend and frontend code. The software page now includes a Versions action, versions and policies modals, updated summary/header pills, and a new library accordion flow for version badges and install-state display. Storybook coverage and reusable UI primitives were expanded, while installer-card/status-table paths were removed or replaced.

Possibly related issues

Possibly related PRs

  • fleetdm/fleet#47808: Shares the same pinned-version parsing, active-installer switching, and datastore pin CRUD changes.
  • fleetdm/fleet#47944: Overlaps on the LibraryItemAccordion UI, helpers, stories, and tests.
  • fleetdm/fleet#48098: Touches the same edited-software activity payload and pinned-version GitOps handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: Fleet-maintained app auto-update, pinning, and rollback support.
Description check ✅ Passed The description includes the required related issue and a mostly complete checklist with the key implementation and testing confirmations.
Linked Issues check ✅ Passed The code implements the core requirements: UI/API version pinning, major pins, auto-update cron, GitOps handling, permissions, and pinned_version logging.
Out of Scope Changes check ✅ Passed The changes stay centered on the Fleet-maintained app workflow and supporting UI/backend pieces, with no clear unrelated scope creep.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/38504-auto-update-pin-rollback-fma

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.44.0)
server/service/integration_enterprise_test.go

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ee/server/service/software_installers.go (1)

2539-2547: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Select a cached version within the requested major before indexing.

When the remote latest does not match ^major, this path fetches versions[0] without checking that the slice is non-empty or that the first version matches the requested major. That can panic or hydrate the wrong major.

Proposed fix
 			versions, err := svc.ds.GetFleetMaintainedVersionsByTitleID(ctx, teamID, *app.TitleID, true)
 			if err != nil {
 				return fleet.NewUserMessageError(errMajorVersionNotFound, http.StatusNotFound)
 			}
+			var cachedVersion string
+			for _, v := range versions {
+				if versionMatchesMajor(v.Version, majorVersionString) {
+					cachedVersion = v.Version
+					break
+				}
+			}
+			if cachedVersion == "" {
+				return fleet.NewUserMessageError(errMajorVersionNotFound, http.StatusNotFound)
+			}
 
 			// This is a bit inefficient as we are duplicating strings for categories and install/uninstall scripts,
 			// but it can be optimized in softwareBatchUpload if it accepted only passing category and script content IDs.
-			installer, err := svc.ds.GetCachedFMAInstallerMetadata(ctx, teamID, app.ID, versions[0].Version)
+			installer, err := svc.ds.GetCachedFMAInstallerMetadata(ctx, teamID, app.ID, cachedVersion)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/server/service/software_installers.go` around lines 2539 - 2547, The
cached installer lookup in software_installers.go can panic or use the wrong
major because it indexes versions[0] without verifying the slice is non-empty
and that the selected version matches the requested major. Update the flow
around GetFleetMaintainedVersionsByTitleID and GetCachedFMAInstallerMetadata to
first find a version within the requested major (using the version list already
returned) and return the existing not-found error if none match, rather than
assuming the first entry is valid.
🧹 Nitpick comments (7)
.storybook/preview.scss (1)

6-8: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Scope this padding override to the affected stories.

.sb-main-padded.sb-show-main is Storybook-global, so this adds 90px padding to every padded story, not just the new Software page previews. A per-story decorator/parameter would avoid shifting unrelated stories and visual baselines.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.storybook/preview.scss around lines 6 - 8, The padding override in the
Storybook stylesheet is applied globally through the
`.sb-main-padded.sb-show-main` selector, so it affects every padded story
instead of only the new Software page previews. Move this spacing logic out of
the shared `preview.scss` and scope it to the specific stories using a per-story
decorator or parameters in the relevant Storybook story definition so only those
previews get the 90px padding.
frontend/components/Chip/Chip.tests.tsx (1)

11-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Scope the SVG assertions to the rendered chip.

These checks read document.querySelector(All)("svg"), so an unrelated SVG from a portal/decorator/tooltip can fail the test even when Chip still renders correctly. Query within the render container instead.

Example
-    render(<Chip text="Fleet-maintained" />);
+    const { container } = render(<Chip text="Fleet-maintained" />);

     expect(screen.getByText("Fleet-maintained")).toBeInTheDocument();
-    expect(document.querySelector("svg")).toBeNull();
+    expect(container.querySelector("svg")).toBeNull();

Also applies to: 19-20, 29-30

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/components/Chip/Chip.tests.tsx` around lines 11 - 13, The Chip tests
are using document-wide SVG queries, which can pick up unrelated icons outside
the component under test. Update the assertions in Chip.tests.tsx to scope SVG
lookups to the rendered Chip container returned by the test render helper, using
the existing Chip render/query setup rather than document.querySelector(All).
Apply the same change in the affected test cases so the SVG checks only validate
what Chip renders.
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/VersionsModal.tests.tsx (1)

111-137: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a save-path test for clearing the pin back to Latest.

The suite only verifies PATCH payloads for exact-version pins. The inverse path is the brittle one here: switching from a pinned version to “Automatically update to latest” must send the empty-string sentinel, and a regression there would currently pass this suite.

Suggested test shape
it("PATCHes an empty string when switching back to Latest", async () => {
  const editSpy = jest
    .spyOn(softwareAPI, "editSoftwarePackage")
    .mockResolvedValue({} as never);

  const { user } = renderModal({ pinned_version: "148.0.1" });

  await user.click(
    screen.getByRole("radio", { name: "Automatically update to latest" })
  );
  await user.click(screen.getByRole("button", { name: "Save" }));

  await waitFor(() => {
    expect(editSpy).toHaveBeenCalledWith({
      data: { pinnedVersion: "" },
      softwareId: 1,
      teamId: 1,
    });
  });
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/VersionsModal.tests.tsx`
around lines 111 - 137, The VersionsModal test coverage is missing the reverse
save path where a pinned version is cleared back to “Automatically update to
latest.” Add a test in VersionsModal.tests.tsx alongside the existing save-path
case that renders VersionsModal with an initial pinned_version, selects the
radio for “Automatically update to latest,” clicks Save, and asserts
softwareAPI.editSoftwarePackage is called with pinnedVersion set to the
empty-string sentinel, using renderModal, editSpy, and the existing radio/button
labels to locate the flow.
frontend/services/entities/software.ts (1)

41-41: 🩺 Stability & Availability | 🔵 Trivial

Resolve the circular dependency between services/entities/software.ts and VersionsModal.tsx.

The software service file at line 41 imports IVersionPinFormData from pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/VersionsModal, but that modal file imports softwareAPI from this service. This creates a runtime circular dependency and tightly couples the service layer to a specific page.

Convert this to a type-only import to prevent runtime resolution issues, and consider moving IVersionPinFormData to a shared type definition module (e.g., interfaces) to remove the dependency on the pages directory entirely.

Suggested change
- import { IVersionPinFormData } from "pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/VersionsModal";
+ import type { IVersionPinFormData } from "pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/VersionsModal";

Note: If possible, move IVersionPinFormData to interfaces/software.ts for cleaner layering.

```

Also applies to: 606-610

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/services/entities/software.ts` at line 41, The import of
IVersionPinFormData in software service layer creates a runtime circular
dependency with VersionsModal because that page also imports softwareAPI from
this module. Update the import in software.ts to be type-only so it is erased at
runtime, and if possible move IVersionPinFormData into a shared interfaces
module such as interfaces/software so the service no longer depends on a pages
component. Use the software.ts and VersionsModal.tsx symbols to verify the
dependency is broken without changing runtime behavior.
ee/server/service/maintained_apps_auto_update.go (1)

324-342: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider not memoizing fetch errors across teams.

manifestEntry is cached by slug and stores err as well as app. The first team encountered for a slug runs ds.GetMaintainedAppByID(ctx, c.FleetMaintainedAppID, c.TeamID), which is team-scoped. If that call fails transiently for that one team, the cached error short-circuits every other team sharing the slug for the rest of the run, skipping otherwise-healthy teams. The manifest itself is team-independent, so caching success is correct; caching the error is the part with collateral impact.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/server/service/maintained_apps_auto_update.go` around lines 324 - 342, The
slug-level cache in hydrateLatestManifest is memoizing team-scoped fetch
failures, causing one transient ds.GetMaintainedAppByID error to poison all
other teams sharing that slug. Keep caching successful hydrated manifests, but
do not store or replay fetch errors in manifestEntry for this path. Update
hydrateLatestManifest so only successful results are cached by slug, and let
failed lookups retry per team instead of short-circuiting on a cached err.
server/datastore/mysql/software_installers_test.go (1)

6202-6203: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a real team ID for the second pin scope.

otherTeam := new(uint(42)) only proves the row is keyed by an integer. It doesn't exercise the real team-backed path, so FK/cascade/lookup regressions tied to actual teams would still pass here.

♻️ Suggested fix
- noTeam := new(uint(0))
- otherTeam := new(uint(42))
+ noTeam := new(uint(0))
+ otherTeamRow, err := ds.NewTeam(ctx, &fleet.Team{Name: "pins-other-team"})
+ require.NoError(t, err)
+ otherTeam := &otherTeamRow.ID
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/datastore/mysql/software_installers_test.go` around lines 6202 - 6203,
The second pin-scope setup in the software installers test is using a fake team
identifier, which bypasses the real team-backed behavior. Update the test data
near noTeam and otherTeam so the second scope uses an actual team record/ID
created through the test fixtures or helpers, and then bind the pin scope to
that real team instead of hardcoding 42. Keep the existing assertions but ensure
the path exercises the same team lookup/foreign key behavior used by the
production team model.
cmd/fleetctl/fleetctl/generate_gitops.go (1)

650-676: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Normalize pinned-version quoting in both export paths.

This post-processing only runs in the file-writing branch. The --key branch returns at Line 646 before it executes, so partial and full GitOps exports already serialize the same pinned-version field differently. Please move this into a helper and run it on both paths.

♻️ Suggested direction
- // Keep software versions quoted so YAML treats them as strings (e.g. "10.0" must not become a float).
- b = softwareVersion.ReplaceAll(b, []byte(`${1}"${2}"`))
+ b = normalizeSoftwareVersionYAML(b)
// Also apply normalizeSoftwareVersionYAML to the bytes emitted by the --key path
// before writing them to stdout.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/fleetctl/fleetctl/generate_gitops.go` around lines 650 - 676, The
pinned-version quoting logic is only applied in the file-writing path, so the
`--key` export path in `generateGitOps` can emit a differently formatted
software version. Extract the post-processing in `generateGitOps` into a helper
(for example, around the existing `softwareVersion` replacement and related YAML
cleanup) and call it from both the stdout `--key` branch and the
`cmd.FilesToWrite` branch so both export modes normalize the version field the
same way.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ee/server/service/software_installers.go`:
- Around line 676-685: The caret-pin handling in software_installers.go
currently falls back to versions[0] when no cached version matches the requested
major, which can activate an unrelated major. In the usesCaret branch of the
installer selection logic, remove that fallback and return
errMajorVersionNotFound when versionMatchesMajor does not find a match; keep the
activeInstallerID assignment only for matching majors.
- Around line 821-823: The `edited_software` activity in
`software_installers.go` is skipping `pinned_version` when the user clears a pin
to Latest because `payload.PinnedVersion` is non-nil but empty. Update the
`activity.PinnedVersion` assignment logic in this software installer edit flow
so it records the field whenever the request explicitly includes it, even when
the value is an empty string, and keep the change localized to the relevant
activity-building code path that uses `payload.PinnedVersion` and
`activity.PinnedVersion`.

In `@frontend/components/TruncatedTextList/TruncatedTextList.tsx`:
- Around line 62-84: The truncated-list rendering in TruncatedTextList should
not show the separator or the “+0 more” badge when there is only one item.
Update the logic around truncatedFirstContent in TruncatedTextList.tsx so the
TooltipWrapper for the truncated first item is shown by itself for single-item
lists, and only render the separator plus the second TooltipWrapper when
items.length > 1. Use the existing items, truncatedFirst, and separator handling
to keep the multi-item behavior unchanged.

In
`@frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/helpers.ts`:
- Around line 36-40: The helper that determines row badge state is marking every
non-empty pinnedVersion as active, which makes unsatisfied pins look like the
current active row. Update the logic in the version-badge helper used by
buildLibraryVersionRows so it compares pinnedVersion against activeVersion and
only returns isActive true when the active version actually satisfies the pin;
otherwise keep the row labeled as Pinned or Major version until the promoted
version catches up.

In
`@frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/LibraryItemAccordion.tsx`:
- Around line 487-492: The LibraryItemAccordion header key handler is also
responding to keyboard activation from the nested badge buttons, which causes
unwanted row toggling. Update handleHeaderKeyDown in LibraryItemAccordion so it
ignores Enter/space events originating from the Latest, Pinned, and label-count
button elements, using the event target/currentTarget relationship or a similar
guard before calling toggleExpanded(). Keep the header toggle behavior unchanged
for genuine header keyboard interactions.

In
`@frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/_styles.scss`:
- Around line 8-13: The `&__title` SCSS block has a Stylelint spacing violation
because `max-width` immediately follows `@include ellipse-text` in
`InstallerDetailsWidget/_styles.scss`; add the required blank line in that rule
so the mixin and property are separated and the lint check passes.

In
`@frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/InstallerDetailsWidget.tsx`:
- Around line 171-179: The tooltip in InstallerDetailsWidget is pointing users
to the old Actions > Edit software flow, but pin/rollback now lives under
Actions > Versions. Update the tooltip copy in the isFma branch of
InstallerDetailsWidget so it matches the new Versions flow and uses the same
destination wording already used elsewhere in the UI.

In
`@frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/SoftwareSummaryCard.tsx`:
- Around line 237-242: The Versions action is still exposed without a concrete
team context because canManageVersions does not account for hasValidTeamId, even
though VersionsModal only renders when teamIdForApi is numeric. Update the
gating in SoftwareSummaryCard so the Versions dropdown item is only shown when
both canManageVersions and hasValidTeamId are true, using the existing
canManageVersions, hasValidTeamId, and VersionsModal/ teamIdForApi checks to
keep the menu actionable in the All teams view.

In
`@frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx`:
- Around line 225-345: The Library section in renderLibrarySection should not
expose edit/download/version/delete actions when viewing All teams, because
those handlers no-op and the edit modal gets an invalid team id. Gate the action
props and modal rendering on typeof teamIdForApi === "number", and pass
teamIdForApi through to EditSoftwareModal instead of currentTeamId; use the same
guard for the LibraryItemAccordion callbacks in both the package and appStore
paths.

In
`@frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/helpers.ts`:
- Around line 39-45: The major-pin option in helpers.ts only gets added for the
newest cached major, so older cached majors like 148 are missing a selectable
track. Update the logic around sorted and majorOf in the helper that builds the
versions modal options so it adds a pin-to-major entry for every distinct cached
major version, not just sorted[0], while keeping the label and value pattern
consistent for each major track.

In
`@frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/VersionsModal.tsx`:
- Around line 96-111: Add a programmatic label to the radio group rendered in
VersionsModal by updating the fieldset around the versionPin options to include
a legend or an aria-labelledby association. Keep the existing Radio mapping
intact, but ensure the group itself has a clear accessible name so screen
readers announce the set of options properly.

In `@server/service/integration_enterprise_test.go`:
- Around line 33318-33321: The multipart PATCH requests in the integration tests
are using team_id, but updateSoftwareInstallerRequest.DecodeRequest only reads
fleet_id, so the team-scoped path is not actually exercised. Update the
multipart body construction in the affected test cases to send fleet_id instead
of team_id, using the existing generateMultipartRequest helper and the same
request flow, so the SoftwareInstallerRequest.TeamID is populated through the
real decode path.

---

Outside diff comments:
In `@ee/server/service/software_installers.go`:
- Around line 2539-2547: The cached installer lookup in software_installers.go
can panic or use the wrong major because it indexes versions[0] without
verifying the slice is non-empty and that the selected version matches the
requested major. Update the flow around GetFleetMaintainedVersionsByTitleID and
GetCachedFMAInstallerMetadata to first find a version within the requested major
(using the version list already returned) and return the existing not-found
error if none match, rather than assuming the first entry is valid.

---

Nitpick comments:
In @.storybook/preview.scss:
- Around line 6-8: The padding override in the Storybook stylesheet is applied
globally through the `.sb-main-padded.sb-show-main` selector, so it affects
every padded story instead of only the new Software page previews. Move this
spacing logic out of the shared `preview.scss` and scope it to the specific
stories using a per-story decorator or parameters in the relevant Storybook
story definition so only those previews get the 90px padding.

In `@cmd/fleetctl/fleetctl/generate_gitops.go`:
- Around line 650-676: The pinned-version quoting logic is only applied in the
file-writing path, so the `--key` export path in `generateGitOps` can emit a
differently formatted software version. Extract the post-processing in
`generateGitOps` into a helper (for example, around the existing
`softwareVersion` replacement and related YAML cleanup) and call it from both
the stdout `--key` branch and the `cmd.FilesToWrite` branch so both export modes
normalize the version field the same way.

In `@ee/server/service/maintained_apps_auto_update.go`:
- Around line 324-342: The slug-level cache in hydrateLatestManifest is
memoizing team-scoped fetch failures, causing one transient
ds.GetMaintainedAppByID error to poison all other teams sharing that slug. Keep
caching successful hydrated manifests, but do not store or replay fetch errors
in manifestEntry for this path. Update hydrateLatestManifest so only successful
results are cached by slug, and let failed lookups retry per team instead of
short-circuiting on a cached err.

In `@frontend/components/Chip/Chip.tests.tsx`:
- Around line 11-13: The Chip tests are using document-wide SVG queries, which
can pick up unrelated icons outside the component under test. Update the
assertions in Chip.tests.tsx to scope SVG lookups to the rendered Chip container
returned by the test render helper, using the existing Chip render/query setup
rather than document.querySelector(All). Apply the same change in the affected
test cases so the SVG checks only validate what Chip renders.

In
`@frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/VersionsModal.tests.tsx`:
- Around line 111-137: The VersionsModal test coverage is missing the reverse
save path where a pinned version is cleared back to “Automatically update to
latest.” Add a test in VersionsModal.tests.tsx alongside the existing save-path
case that renders VersionsModal with an initial pinned_version, selects the
radio for “Automatically update to latest,” clicks Save, and asserts
softwareAPI.editSoftwarePackage is called with pinnedVersion set to the
empty-string sentinel, using renderModal, editSpy, and the existing radio/button
labels to locate the flow.

In `@frontend/services/entities/software.ts`:
- Line 41: The import of IVersionPinFormData in software service layer creates a
runtime circular dependency with VersionsModal because that page also imports
softwareAPI from this module. Update the import in software.ts to be type-only
so it is erased at runtime, and if possible move IVersionPinFormData into a
shared interfaces module such as interfaces/software so the service no longer
depends on a pages component. Use the software.ts and VersionsModal.tsx symbols
to verify the dependency is broken without changing runtime behavior.

In `@server/datastore/mysql/software_installers_test.go`:
- Around line 6202-6203: The second pin-scope setup in the software installers
test is using a fake team identifier, which bypasses the real team-backed
behavior. Update the test data near noTeam and otherTeam so the second scope
uses an actual team record/ID created through the test fixtures or helpers, and
then bind the pin scope to that real team instead of hardcoding 42. Keep the
existing assertions but ensure the path exercises the same team lookup/foreign
key behavior used by the production team model.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7072d9d9-2a2b-4211-a882-0ff31bb354c9

📥 Commits

Reviewing files that changed from the base of the PR and between 7013549 and b9f8c1d.

📒 Files selected for processing (104)
  • .storybook/main.ts
  • .storybook/preview.js
  • .storybook/preview.scss
  • changes/38504-auto-update-pin-rollback-fma
  • cmd/fleet/cron.go
  • cmd/fleet/cron_registration.go
  • cmd/fleetctl/fleetctl/generate_gitops.go
  • cmd/fleetctl/fleetctl/generate_gitops_test.go
  • cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamSoftware.yaml
  • cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/fleets/team-a-thumbsup.yml
  • ee/server/service/maintained_apps_auto_update.go
  • ee/server/service/maintained_apps_auto_update_download_test.go
  • ee/server/service/maintained_apps_auto_update_test.go
  • ee/server/service/software_installers.go
  • ee/server/service/software_installers_test.go
  • frontend/components/Chip/Chip.stories.tsx
  • frontend/components/Chip/Chip.tests.tsx
  • frontend/components/Chip/Chip.tsx
  • frontend/components/Chip/_styles.scss
  • frontend/components/Chip/index.ts
  • frontend/components/Icon/Icon.stories.tsx
  • frontend/components/Tag/Tag.tsx
  • frontend/components/Tag/_styles.scss
  • frontend/components/Tag/index.ts
  • frontend/components/TooltipTruncatedText/TooltipTruncatedText.tsx
  • frontend/components/TruncatedTextList/TruncatedTextList.stories.tsx
  • frontend/components/TruncatedTextList/TruncatedTextList.tsx
  • frontend/components/TruncatedTextList/_styles.scss
  • frontend/components/TruncatedTextList/index.ts
  • frontend/components/buttons/Button/Button.tsx
  • frontend/components/icons/Pin.tsx
  • frontend/components/icons/Tag.tsx
  • frontend/components/icons/index.ts
  • frontend/interfaces/software.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditIconModal/EditIconModal.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/LibraryItemAccordion.stories.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/LibraryItemAccordion.tests.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/LibraryItemAccordion.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/LibraryItemAccordionList.stories.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/LibraryItemAccordionList.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/_styles.scss
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/helpers.tests.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/helpers.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/index.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/PoliciesModal/PoliciesModal.tests.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/PoliciesModal/PoliciesModal.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/PoliciesModal/_styles.scss
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/PoliciesModal/index.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/InstallerDetailsWidget.tests.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/InstallerDetailsWidget.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/_styles.scss
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/InstallerPoliciesTable.tests.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerPoliciesTable/InstallerPoliciesTable.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tests.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/_styles.scss
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/index.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareInstallerCard.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/_styles.scss
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/helpers.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/index.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/SoftwareSummaryCard.tests.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/SoftwareSummaryCard.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/TitleVersionsTable/TitleVersionsTable.tests.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/TitleVersionsTable/TitleVersionsTable.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/TitleVersionsTable/TitleVersionsTableConfig.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/TitleVersionsTable/_styles.scss
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/TitleVersionsTable/index.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/VersionsModal.tests.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/VersionsModal.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/_styles.scss
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/helpers.tests.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/helpers.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/VersionsModal/index.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/helpers.tests.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/helpers.ts
  • frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tests.tsx
  • frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx
  • frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/_styles.scss
  • frontend/pages/SoftwarePage/helpers.tsx
  • frontend/services/entities/software.ts
  • frontend/test/storybook-utils.tsx
  • frontend/utilities/permissions/permissions.tests.ts
  • frontend/utilities/permissions/permissions.ts
  • server/datastore/mysql/software_installers.go
  • server/datastore/mysql/software_installers_test.go
  • server/datastore/mysql/software_titles.go
  • server/datastore/mysql/teams_test.go
  • server/fleet/activities.go
  • server/fleet/cron_schedules.go
  • server/fleet/datastore.go
  • server/fleet/software.go
  • server/fleet/software_installer.go
  • server/mdm/maintainedapps/sync.go
  • server/mdm/maintainedapps/sync_test.go
  • server/mock/datastore_mock.go
  • server/service/integration_enterprise_test.go
  • server/service/integration_software_titles_test.go
  • server/service/integration_vpp_install_test.go
  • server/service/software_installers.go
  • server/service/software_titles.go
💤 Files with no reviewable changes (13)
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/_styles.scss
  • frontend/components/Tag/_styles.scss
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTableConfig.tsx
  • frontend/components/Tag/index.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tests.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/index.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/index.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/InstallerStatusTable.tsx
  • frontend/components/Tag/Tag.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/helpers.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerStatusTable/_styles.scss
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareInstallerCard.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/InstallerDetailsWidget.tests.tsx

Comment thread ee/server/service/software_installers.go
Comment thread ee/server/service/software_installers.go
Comment thread frontend/components/TruncatedTextList/TruncatedTextList.tsx
Comment thread server/service/integration_enterprise_test.go
**Related issue:** Resolves #48334

The software title response now returns a per-version `filename` in
`fleet_maintained_versions`, and the Library version rows render each
version's own filename instead of the active installer's. Previously,
every cached-version row showed the active installer's filename because
the array didn't include one.

  # 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] Added/updated automated tests
  - [x] QA'd all new/changed functionality manually
**Related issue:** Resolves #48309

Deleting a Fleet-maintained app from a team now also deletes its
`software_title_team_pins` row. Previously the pin survived the delete
(the FK cascades only on title deletion, and the title row outlives the
installer rows), so re-adding the app resurfaced a stale pin pointing at
a version no longer cached.

  # 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] Added/updated automated tests
  - [x] QA'd all new/changed functionality manually
**Related issue:** Resolves #48301

When the auto-update cron downloads a new Fleet-maintained app version,
it now carries forward the previously-active install/uninstall scripts
when they were customized (e.g. via GitOps), instead of overwriting them
with the manifest defaults. Customization is detected per-script by
comparing the active scripts against the manifest (the uninstall script
is compared against the manifest template substituted with the active
version's package IDs, since it's version-specific). When the active
scripts match the manifest, the new version's manifest scripts are used
as before.

  # 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] Added/updated automated tests
  - [x] QA'd all new/changed functionality manually
@georgekarrv
georgekarrv merged commit 0f439f9 into main Jun 26, 2026
50 checks passed
@georgekarrv
georgekarrv deleted the feat/38504-auto-update-pin-rollback-fma branch June 26, 2026 19:42
cdcme added a commit that referenced this pull request Jun 30, 2026
**Related issue:** Resolves #46728

Docs for auto-update, version pinning, and rollback of Fleet-maintained
apps (#38504), shipped in #48293.

---------

Co-authored-by: Marko Lisica <83164494+marko-lisica@users.noreply.github.com>
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.

Auto update, pin, and rollback Fleet-maintained apps via UI/API

5 participants