Allow multiple custom packages per software title#49157
Conversation
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #48396 # 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 ## Database migrations - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - shouldn't update timestamps unless the installer was a duplicate - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). ## 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: - [ ] 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 can now include multiple custom packages without duplicating the title in lists. * Added clearer package details when viewing software tied to a title or team. * **Bug Fixes** * Improved installer conflict handling so uploads now show more specific error messages. * Prevented duplicate package entries from creating extra title rows or incorrect counts. * Fixed package counting and selection so the first-added package is used consistently when multiple exist. * **Chores** * Added database changes to enforce the new package-deduplication behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
**Related issue:** Resolves #48397 ## Summary Adds the REST API layer for multiple custom packages per software title (backend only): - `GET /software/titles` and `GET /software/titles/:id` return a new `packages[]` array; `software_package` is retained as the first-added package for backwards compatibility. - `POST /software/package` adds a package to an existing title and returns the added package. - `PATCH /software/titles/:id/package` targets a specific `installer_id` and rejects a replacement whose hash matches a sibling package (409). - `DELETE /software/titles/:id/available_for_install?installer_id=` deletes one package; omitting `installer_id` deletes them all. Builds on the data-model foundation (#48396). Install-time precedence and the host-software endpoint are out of scope (#48398). # Checklist for submitter - [ ] Changes file added for user-visible changes in `changes/`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements). - [x] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually
**Related issue:** Resolves #48398 Applies first-added-wins install precedence everywhere a host can match more than one package of a software title: manual install, self-service, policy auto-install, setup experience, and the host-software read all resolve the first-added package the host is in label scope for. Policy automations can now target a specific package (defaulting to first-added), and deleting a package re-points its automations to the first-added surviving package. # Checklist for submitter - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements). ## Testing - [x] Added/updated automated tests - [x] 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
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #48399 Summary: - Allows multiple installers for the same title to be defined in a yaml file - `generate-gitops` generates a file like this if multiple installers are available per title - Allows labels, self_service, categories keys to be defined per package - Inherits fleet-level keys only if they are not set at the package-level - Repoints policies.software_installer_id for a deleted installer to either the first added installer for that title, or NULL if none are available # 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 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * GitOps output now supports software titles that contain multiple packages, generating a dedicated package file plus related assets. * Software imports and updates now preserve package order and handle multi-package titles more consistently. * **Bug Fixes** * Improved inheritance and validation for software fields so package-level settings are respected and conflicting settings are flagged. * Fixed installer batch updates to better handle added, removed, and reordered packages without disrupting related policies or pending installs. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…ctivity (#49085) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves # - Adds hash_sha256 field to host installer result and install software activity so they can be used in combination to cover past activities with existing installers + all new activities even if their installer will get deleted # Checklist for submitter ## 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 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Software installation results now include the installer’s SHA-256 hash when available. * Installed-software activity records now include the corresponding `hash_sha256` value. * **Bug Fixes** * Hash values are retained in activity history after an installer is deleted, while live results correctly show the value as unavailable. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…ience, install-details hash (#49079)
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis change adds support for up to 10 custom packages per software title. Packages can be independently targeted, edited, deleted, exported, and selected for installation, with first-added precedence when multiple packages match. APIs, datastore queries, policy automation, setup experience, GitOps YAML parsing/generation, and software management UI now expose package-level data. Install results and activities also preserve package SHA-256 hashes. Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Pull request overview
This PR adds end-to-end support for multiple custom software packages per software title (up to 10) within a team, while preserving the legacy software_package (first-added) field for backwards compatibility and applying “first-added-wins” precedence consistently.
Changes:
- Backend: extend software title/package models, list/detail query paths, policy automation pinning (
software_installer_id), and setup-experience dedupe to support multi-package titles. - GitOps: allow multi-package YAML parsing + stable generation/round-trip for multiple packages per title.
- Frontend: add multi-package Library UX (add/edit/delete per package), policy automation “select package” picker, setup experience tooltip copy, and install-details hash display.
Reviewed changes
Copilot reviewed 92 out of 94 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| server/service/testing_client_test.go | Adds installer_id multipart field support in test client update helper. |
| server/service/team_policies.go | Threads software_installer_id through policy create/modify and validates chosen installer against title packages. |
| server/service/team_policies_test.go | Extends policy automation population test to assert installer pin behavior. |
| server/service/software_titles.go | Populates packages[] and keeps software_package as first-added for detail responses; per-package categories/policies. |
| server/service/software_installers.go | Adds installer targeting on update and optional per-installer deletion on delete endpoint. |
| server/service/software_installers_test.go | Updates auth test to reflect new package lookup + delete signature. |
| server/service/orbit.go | Includes hash_sha256 in installed software activity details. |
| server/service/integration_vpp_install_test.go | Updates expected conflict messages for VPP vs installer conflicts. |
| server/service/integration_mdm_setup_experience_test.go | Extends setup experience activity assertions to include hash_sha256. |
| server/mock/service/service_mock.go | Updates mock service interface for delete signature including optional installer id. |
| server/mock/datastore_mock.go | Adds mocks for installer-scoped categories and multi-package fetch helpers. |
| server/fleet/software.go | Adds packages to title/list shapes; clarifies back-compat software_package semantics. |
| server/fleet/software_installer.go | Adds hash_sha256 to host installer results; introduces package list item type + validation helpers. |
| server/fleet/service.go | Updates service interface for installer-targeted delete. |
| server/fleet/policies.go | Adds software_installer_id to policy payloads and response shape for pinned package selection. |
| server/fleet/errors.go | Adds new conflict/validation messages for multi-package + GitOps validations. |
| server/fleet/datastore.go | Adds datastore APIs to fetch packages per title(s) and installer-scoped categories. |
| server/fleet/api_policies.go | Adds software_installer_id to team policy request payload. |
| server/fleet/activities.go | Adds hash_sha256 to installed-software activity detail. |
| server/datastore/mysql/software_titles.go | Prevents duplicate title rows via MIN(installer_id) join; adds packages[] fetch per title; adds GetSoftwarePackagesForTitles. |
| server/datastore/mysql/software_titles_test.go | Adds regression tests for multi-package title dedupe + per-installer policy dispatch. |
| server/datastore/mysql/software_test.go | Adds host software list precedence test for multi-package label scoping + first-added-wins. |
| server/datastore/mysql/setup_experience.go | Dedupes setup queue to first-added installer per title (no double-queue). |
| server/datastore/mysql/setup_experience_test.go | Adds test ensuring only first-added package is queued during setup. |
| server/datastore/mysql/policies.go | Makes GitOps policy resolution deterministic via first-added installer; adds installer join key. |
| server/datastore/mysql/policies_test.go | Adds tests for installer join key and GitOps first-added resolution behavior. |
| server/datastore/mysql/migrations/tables/20260702232839_MultipleCustomPackagesPerTitle.go | Migration adds dedup_token and new unique key; dedupes existing rows and repoints FKs before deletion. |
| server/datastore/mysql/in_house_apps.go | Removes installer-by-name existence helper (now multi-package aware paths elsewhere). |
| pkg/spec/gitops.go | Parses package YAML as list; validates fleet/package-level field placement; hydrates inheritance rules. |
| pkg/spec/gitops_test.go | Adds tests for multi-package field placement rules + inheritance behaviors. |
| frontend/test/handlers/software-handlers.ts | Adds MSW handler to return install results with hash_sha256. |
| frontend/services/entities/team_policies.ts | Threads software_installer_id through team policy create/update API calls. |
| frontend/services/entities/software.ts | Adds software_title_id to add-package flow; adds installer_id to edit/delete package calls. |
| frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/ViewYamlModal.tsx | Removes View YAML modal (flow removed). |
| frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/index.ts | Removes View YAML modal export. |
| frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/helpers.tsx | Removes YAML-generation helper. |
| frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/helpers.tests.ts | Removes YAML-generation helper tests. |
| frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/_styles.scss | Removes View YAML modal styles. |
| frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/SoftwareSummaryCard.tsx | Adjusts chips/actions for multi-package titles; pluralizes “Custom package(s)”. |
| frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/LibraryItemAccordion.tsx | Adds per-row self-service/auto-install indicator icons + GitOps delete lock for multi-package custom titles. |
| frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/LibraryItemAccordion.tests.tsx | Adds tests for multi-package row icon behavior and Latest badge gating. |
| frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/helpers.tests.ts | Updates helper tests for new installer_id and packages nullability. |
| frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tsx | Supports per-installer editing + “Edit package” title; uses shared navigation-block hook. |
| frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tests.tsx | Adds tests for “Edit package” title and installerId prop acceptance. |
| frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/DeleteSoftwareModal/DeleteSoftwareModal.tsx | Supports per-installer deletion + “Delete package” title changes. |
| frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/DeleteSoftwareModal/DeleteSoftwareModal.tests.tsx | Adds tests for multi-package delete title + warning suppression. |
| frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AddPackageModal/index.ts | Exports new AddPackageModal. |
| frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AddPackageModal/helpers.ts | Adds file-type restriction helper for adding packages to an existing title. |
| frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AddPackageModal/helpers.tests.ts | Tests file-type restriction helper. |
| frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AddPackageModal/AddPackageModal.tsx | New modal for adding a package to an existing multi-package title. |
| frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AddPackageModal/AddPackageModal.tests.tsx | Tests AddPackageModal behavior in standard and GitOps mode. |
| frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss | Adds layout styling for new library description row. |
| frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/SoftwareCustomPackage.tsx | Introduces shared GitOps banner component; switches to navigation-block hook. |
| frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/SoftwareCustomPackage.tests.tsx | Tests shared GitOps banner content/link. |
| frontend/pages/SoftwarePage/helpers.tsx | Makes target helpers accept undefined installer data. |
| frontend/pages/SoftwarePage/components/forms/PackageForm/PackageForm.tsx | Adds multi-package context banner + file accept restriction + GitOps banner reuse; adjusts save button text consistency. |
| frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx | Collapses Actions into single Edit button when multi-package gate is enabled. |
| frontend/pages/policies/hooks/useUpdatePolicyAutomations.ts | Includes software_installer_id in policy automation update type. |
| frontend/pages/policies/helpers.ts | Adds multi-package help text, package help text, and “first-added package” resolver. |
| frontend/pages/policies/helpers.tests.tsx | Adds tests for multi-package “N versions” help text behavior. |
| frontend/pages/policies/edit/components/PolicyForm/_styles.scss | Adds responsive layout tweaks for dual dropdowns when schema sidebar is open. |
| frontend/pages/policies/components/PolicyAutomationsFields/PolicyAutomationsFields.tsx | Adds per-package dropdown + auto-select/validation logic + payload includes installer id. |
| frontend/pages/policies/components/PolicyAutomationsFields/_styles.scss | Adds layout styling for dual dropdowns and tighter table layout. |
| frontend/pages/ManageControlsPage/SetupExperience/cards/InstallSoftware/components/InstallSoftwareTable/InstallSoftwareTableConfig.tsx | Adds tooltip to Version column describing first-added behavior for custom packages. |
| frontend/interfaces/software.ts | Adds installer_id, packages, max-packages constant, and install result hash field. |
| frontend/interfaces/policy.ts | Adds software_installer_id to policy software automation shape. |
| frontend/hooks/useBlockNavigation.ts | New shared beforeunload blocking hook for uploads/patch flows. |
| frontend/hooks/useBlockNavigation.tests.ts | Adds hook tests for add/remove handler behavior. |
| frontend/docs/patterns.md | Documents useBlockNavigation hook. |
| frontend/components/InfoBanner/InfoBanner.tsx | Adds optional leading icon support to InfoBanner component. |
| frontend/components/InfoBanner/_styles.scss | Styles InfoBanner layout when leading icon is present. |
| frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx | Adds ariaLabel prop and ensures combobox always has an accessible name. |
| frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx | Adds guarded “Package SHA-256 hash” row + copy button. |
| frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tests.tsx | Adds tests for hash row presence/absence based on payload. |
| frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/_styles.scss | Styles hash row to keep copy button visible with long hashes. |
| frontend/mocks/softwareMock.ts | Updates software mocks with installer_id and packages fields. |
| ee/server/service/software_installers_test.go | Updates EE install/uninstall tests for precedence resolver using packages list. |
| cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go | Adds enterprise integration test covering multi-package apply/generate/reapply stability. |
| cmd/fleetctl/fleetctl/testing_utils/testing_utils.go | Updates installer test server to serve distinct-hash variant package content. |
| cmd/fleetctl/fleetctl/get_test.go | Updates get output tests to include packages: null in shapes. |
| cmd/fleetctl/fleetctl/generate_gitops.go | Generates multi-package package YAML side file and references from fleet file; refactors label scoping helper. |
| cmd/fleetctl/fleetctl/generate_gitops_test.go | Adds unit test verifying multi-package generate output structure + side files. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ustom-packages # Conflicts: # frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/LibraryItemAccordion.tsx # frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx # pkg/spec/gitops.go # server/datastore/mysql/schema.sql # server/fleet/software_installer.go
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #49157 +/- ##
==========================================
+ Coverage 67.95% 68.15% +0.20%
==========================================
Files 3838 3849 +11
Lines 241433 243442 +2009
Branches 12802 12954 +152
==========================================
+ Hits 164068 165922 +1854
- Misses 62497 62524 +27
- Partials 14868 14996 +128
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/DeleteSoftwareModal/DeleteSoftwareModal.tsx (1)
97-127: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse a package-specific success message.
Line 105 still says “Successfully deleted software.” when multi-package mode deletes only one installer. Make the toast conditional, and include
canActivateMultiplePackagesin the callback dependencies.Proposed fix
- notify.success("Successfully deleted software."); + notify.success( + canActivateMultiplePackages + ? "Successfully deleted package." + : "Successfully deleted software." + ); @@ - }, [softwareId, teamId, installerId, onSuccess, onExit]); + }, [ + softwareId, + teamId, + installerId, + canActivateMultiplePackages, + onSuccess, + onExit, + ]);🤖 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/DeleteSoftwareModal/DeleteSoftwareModal.tsx` around lines 97 - 127, Update onDeleteSoftware to conditionally display a package-specific success toast when canActivateMultiplePackages is enabled, while retaining the existing message otherwise. Add canActivateMultiplePackages to the useCallback dependency array.cmd/fleetctl/fleetctl/generate_gitops.go (2)
2148-2161: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
generateMultiPackagedoesn't exportConfiguration(managed app config), unlike the single-package path.The single-package branch above unwraps and writes
softwareTitle.SoftwarePackage.Configurationto a side file (in-house app managed XML config), but the per-package loop ingenerateMultiPackagehas no equivalent — any in-house app package withConfigurationset that's part of a multi-package title will silently lose that config on GitOps export/round-trip.♻️ Suggested fix: mirror the single-package Configuration handling per item
if key, names := scopeLabels(pkg.LabelsIncludeAny, pkg.LabelsExcludeAny, pkg.LabelsIncludeAll); key != "" { item[key] = names } + if len(pkg.Configuration) > 0 { + var xmlStr string + if err := json.Unmarshal(pkg.Configuration, &xmlStr); err != nil { + return nil, err + } + if xmlStr != "" { + item["configuration"] = map[string]any{ + "path": writeSideFile("software", prefix+"-config.xml", []byte(xmlStr)), + } + } + } items = append(items, item)Also applies to: 2299-2336
🤖 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 2148 - 2161, Update generateMultiPackage to mirror the single-package Configuration handling for each package item: JSON-unmarshal the package Configuration into XML, report and return decoding errors, write non-empty XML to a package-specific config file under the appropriate lib path, and set the item’s softwareSpec["configuration"]["path"] accordingly. Ensure filenames are unique per package and apply the same logic to the additional code path around the referenced package loop.
1971-2015: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winUse the policy’s installer ID when exporting
install_software. Incmd/fleetctl/fleetctl/generate_gitops.go:1655,generatePolicieskeyscmd.SoftwareListbySoftwareTitleID, so a multi-package policy pinned to a non-first package is serialized with the title’s first-added hash. Fall back toSoftwareTitleIDonly whenSoftwareInstallerIDis nil.🤖 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 1971 - 2015, Use the policy’s SoftwareInstallerID when looking up software entries for install_software generation, since cmd.SoftwareList is populated by installer ID in the software export logic. Update generatePolicies to key the lookup by *SoftwareInstallerID when present, falling back to SoftwareTitleID only when it is nil, ensuring multi-package policies use the pinned installer’s hash and metadata.
🧹 Nitpick comments (3)
frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/DeleteSoftwareModal/DeleteSoftwareModal.tests.tsx (1)
63-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCover the installer-specific delete request.
Line 78 only verifies presentation; it neither supplies an
installerIdnor clicks Delete. Add a multi-package test that asserts the API receives the selected installer ID, since this is the destructive behavior introduced by the feature.🤖 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/DeleteSoftwareModal/DeleteSoftwareModal.tests.tsx` around lines 63 - 85, Extend the multi-package test coverage around DeleteSoftwareModal to exercise the destructive action, not just rendered text. Render with canActivateMultiplePackages true and a specific installerId, click the Delete control, and assert the delete API/mock is called with that selected installer ID. Reuse the existing modal render helper and API mock symbols to verify the request payload.frontend/pages/SoftwarePage/components/forms/PackageForm/PackageForm.tsx (1)
35-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the shared banner to break the import cycle.
Line 35 imports from
SoftwareCustomPackage, while that page importsPackageForm. MoveGitOpsCustomPackageBannerinto a standalone shared module and import it from both callers; the current cycle makes module initialization and testing more brittle.🤖 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/components/forms/PackageForm/PackageForm.tsx` at line 35, Move GitOpsCustomPackageBanner out of SoftwareCustomPackage into a standalone shared module, then update both SoftwareCustomPackage and PackageForm to import it from that module. Remove the direct PackageForm import from the page component to eliminate the circular dependency.server/service/software_titles.go (1)
229-234: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoffOptional: per-package status summary is an N+1.
GetSummaryHostSoftwareInstallsis called once per package inside the loop (up to 10 round-trips per title-detail request). If a batched variant keyed byinstallerIDis (or can be) available, prefer it to collapse these into a single query.🤖 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/service/software_titles.go` around lines 229 - 234, Replace the per-package GetSummaryHostSoftwareInstalls call in the package loop with a batched lookup keyed by installer ID, using or adding a data-store method that retrieves all summaries in one query; then assign each returned summary to its corresponding pkg.Status and preserve the existing error wrapping.
🤖 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 `@frontend/pages/policies/components/PolicyAutomationsFields/_styles.scss`:
- Around line 95-102: Update the comment above the `&__row-picker` styles to
reference the actual fixed width of 225px instead of 300px.
In
`@frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx`:
- Around line 289-320: Wrap the callback props in renderAppStoreRow with
zero-argument functions so the click event is not passed as the installer ID:
use closures for onTrashClick, onLabelCountClick, and onLabelsClick that invoke
openDeleteModal/openEditModal with the intended app-store identifier or no
argument, matching renderPackageRows and the expected callback signatures.
In `@pkg/spec/gitops.go`:
- Around line 2267-2304: Empty package files or lists currently pass validation
without registering a package. Before the per-package loop in the
package-loading logic, add a len(softwarePackageSpecs) == 0 check and append an
appropriate validation error referencing the package path, then continue without
hydration.
In `@server/datastore/mysql/software.go`:
- Around line 3794-3908: The label CTEs in ListHostSoftware currently aggregate
all installers before applying fleet, active-status, and title filters. Add a
candidate-installer CTE (or equivalent filtered seed) using global_or_team_id,
is_active, and title_ids, then join each of no_labels, include_any, exclude_any,
and include_all to that candidate set before aggregation; retain the final
result and ordering semantics.
---
Outside diff comments:
In `@cmd/fleetctl/fleetctl/generate_gitops.go`:
- Around line 2148-2161: Update generateMultiPackage to mirror the
single-package Configuration handling for each package item: JSON-unmarshal the
package Configuration into XML, report and return decoding errors, write
non-empty XML to a package-specific config file under the appropriate lib path,
and set the item’s softwareSpec["configuration"]["path"] accordingly. Ensure
filenames are unique per package and apply the same logic to the additional code
path around the referenced package loop.
- Around line 1971-2015: Use the policy’s SoftwareInstallerID when looking up
software entries for install_software generation, since cmd.SoftwareList is
populated by installer ID in the software export logic. Update generatePolicies
to key the lookup by *SoftwareInstallerID when present, falling back to
SoftwareTitleID only when it is nil, ensuring multi-package policies use the
pinned installer’s hash and metadata.
In
`@frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/DeleteSoftwareModal/DeleteSoftwareModal.tsx`:
- Around line 97-127: Update onDeleteSoftware to conditionally display a
package-specific success toast when canActivateMultiplePackages is enabled,
while retaining the existing message otherwise. Add canActivateMultiplePackages
to the useCallback dependency array.
---
Nitpick comments:
In `@frontend/pages/SoftwarePage/components/forms/PackageForm/PackageForm.tsx`:
- Line 35: Move GitOpsCustomPackageBanner out of SoftwareCustomPackage into a
standalone shared module, then update both SoftwareCustomPackage and PackageForm
to import it from that module. Remove the direct PackageForm import from the
page component to eliminate the circular dependency.
In
`@frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/DeleteSoftwareModal/DeleteSoftwareModal.tests.tsx`:
- Around line 63-85: Extend the multi-package test coverage around
DeleteSoftwareModal to exercise the destructive action, not just rendered text.
Render with canActivateMultiplePackages true and a specific installerId, click
the Delete control, and assert the delete API/mock is called with that selected
installer ID. Reuse the existing modal render helper and API mock symbols to
verify the request payload.
In `@server/service/software_titles.go`:
- Around line 229-234: Replace the per-package GetSummaryHostSoftwareInstalls
call in the package loop with a batched lookup keyed by installer ID, using or
adding a data-store method that retrieves all summaries in one query; then
assign each returned summary to its corresponding pkg.Status and preserve the
existing error wrapping.
🪄 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: 56ca2a3d-9e67-4f30-9fbc-e42eb5de7ef5
⛔ Files ignored due to path filters (2)
frontend/docs/patterns.mdis excluded by!**/*.mdserver/datastore/mysql/testdata/select_software_titles_sql_fixture.gzis excluded by!**/*.gz
📒 Files selected for processing (92)
changes/28108-multiple-custom-packagescmd/fleetctl/fleetctl/generate_gitops.gocmd/fleetctl/fleetctl/generate_gitops_test.gocmd/fleetctl/fleetctl/get_test.gocmd/fleetctl/fleetctl/testing_utils/testing_utils.gocmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.goee/server/service/software_installers.goee/server/service/software_installers_test.gofrontend/__mocks__/softwareMock.tsfrontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tests.tsxfrontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsxfrontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/_styles.scssfrontend/components/InfoBanner/InfoBanner.tsxfrontend/components/InfoBanner/_styles.scssfrontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsxfrontend/hooks/useBlockNavigation.tests.tsfrontend/hooks/useBlockNavigation.tsfrontend/interfaces/policy.tsfrontend/interfaces/software.tsfrontend/pages/ManageControlsPage/SetupExperience/cards/InstallSoftware/components/InstallSoftwareTable/InstallSoftwareTableConfig.tsxfrontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/SoftwareCustomPackage.tests.tsxfrontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/SoftwareCustomPackage.tsxfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AddPackageModal/AddPackageModal.tests.tsxfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AddPackageModal/AddPackageModal.tsxfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AddPackageModal/helpers.tests.tsfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AddPackageModal/helpers.tsfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AddPackageModal/index.tsfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/DeleteSoftwareModal/DeleteSoftwareModal.tests.tsxfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/DeleteSoftwareModal/DeleteSoftwareModal.tsxfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tests.tsxfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tsxfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/LibraryItemAccordion.tests.tsxfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/LibraryItemAccordion.tsxfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/SoftwareSummaryCard.tests.tsxfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/SoftwareSummaryCard.tsxfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsxfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/ViewYamlModal.tsxfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/_styles.scssfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/helpers.tests.tsfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/helpers.tsxfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/index.tsfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scssfrontend/pages/SoftwarePage/SoftwareTitleDetailsPage/helpers.tests.tsfrontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsxfrontend/pages/SoftwarePage/components/forms/PackageForm/PackageForm.tsxfrontend/pages/SoftwarePage/helpers.tsxfrontend/pages/policies/components/PolicyAutomationsFields/PolicyAutomationsFields.tests.tsxfrontend/pages/policies/components/PolicyAutomationsFields/PolicyAutomationsFields.tsxfrontend/pages/policies/components/PolicyAutomationsFields/_styles.scssfrontend/pages/policies/edit/components/PolicyForm/_styles.scssfrontend/pages/policies/helpers.tests.tsxfrontend/pages/policies/helpers.tsfrontend/pages/policies/hooks/useUpdatePolicyAutomations.tsfrontend/services/entities/software.tsfrontend/services/entities/team_policies.tsfrontend/test/handlers/software-handlers.tspkg/spec/gitops.gopkg/spec/gitops_test.goserver/datastore/mysql/in_house_apps.goserver/datastore/mysql/migrations/tables/20260702232839_MultipleCustomPackagesPerTitle.goserver/datastore/mysql/migrations/tables/20260702232839_MultipleCustomPackagesPerTitle_test.goserver/datastore/mysql/policies.goserver/datastore/mysql/policies_test.goserver/datastore/mysql/schema.sqlserver/datastore/mysql/setup_experience.goserver/datastore/mysql/setup_experience_test.goserver/datastore/mysql/software.goserver/datastore/mysql/software_installers.goserver/datastore/mysql/software_installers_test.goserver/datastore/mysql/software_test.goserver/datastore/mysql/software_titles.goserver/datastore/mysql/software_titles_test.goserver/fleet/activities.goserver/fleet/api_policies.goserver/fleet/datastore.goserver/fleet/errors.goserver/fleet/policies.goserver/fleet/service.goserver/fleet/software.goserver/fleet/software_installer.goserver/mock/datastore_mock.goserver/mock/service/service_mock.goserver/service/integration_enterprise_test.goserver/service/integration_mdm_setup_experience_test.goserver/service/integration_vpp_install_test.goserver/service/orbit.goserver/service/software_installers.goserver/service/software_installers_test.goserver/service/software_titles.goserver/service/team_policies.goserver/service/team_policies_test.goserver/service/testing_client_test.go
💤 Files with no reviewable changes (6)
- frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/_styles.scss
- frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/helpers.tsx
- frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/index.ts
- frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/helpers.tests.ts
- frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/ViewYamlModal.tsx
- server/datastore/mysql/in_house_apps.go
…splay-name save, observer download (#49244)
#49264) **Related issue:** Resolves #49209 Adding a package to an existing software title (`POST /software/package` with `software_title_id`) now validates that the uploaded installer actually belongs to that title. Previously the `software_title_id` sent by the title page's "Add package" flow was ignored, so uploading mismatched software (e.g. a 1Password package onto a Zoom title) silently created a new separate title instead of erroring. It now returns a 400 with a clear message and writes nothing. When `software_title_id` is omitted (general "Add software" flow, GitOps), behavior is unchanged. # 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 #49208 Host software details now show the status and install details for the installer scoped to the host, for titles with multiple packages. No `changes/` file: this is an unreleased bug fix, and the multiple-packages feature PR carries the changes entry. # 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 #49234 Editing a software installer to a new version failed with "The selected package is for different software" when the title's stored (osquery-reported) name differed from the installer's extracted name. The edit now validates by software identity (bundle identifier / upgrade code / name) instead of an exact name match. # 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 #28108
Adds support for uploading multiple custom packages (up to 10) for the same software title on a team — so IT admins can deploy different versions or architectures (for example, Arm vs. Intel builds or staged rollouts) to label-scoped hosts instead of splitting them across teams. The software title keeps a single first-added
software_packagefor backwards compatibility, and first-added-wins resolves overlaps consistently across self-service, manual install, policy automation, and setup experience.Feature branch combining the sub-PRs: migration (#48596), packages[] API and add/edit/delete-package endpoints (#48607), install-time precedence and setup experience (#48708), GitOps (#48710), Library and Add/Edit/Delete modals (#48520), secondary UI — policy automation, setup experience, and install-details hash (#49079), and the host install-result
hash_sha256field (#49085).Checklist for submitter
changes/,orbit/changes/oree/fleetd-chrome/changes.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
Database migrations
COLLATE utf8mb4_unicode_ci).Summary by CodeRabbit
aria-labelsupport for dropdowns.