Skip to content

Allow multiple custom packages per software title#49157

Merged
georgekarrv merged 17 commits into
mainfrom
feat/28108-multiple-custom-packages
Jul 15, 2026
Merged

Allow multiple custom packages per software title#49157
georgekarrv merged 17 commits into
mainfrom
feat/28108-multiple-custom-packages

Conversation

@cdcme

@cdcme cdcme commented Jul 10, 2026

Copy link
Copy Markdown
Member

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_package for 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_sha256 field (#49085).

Checklist for submitter

  • Changes file added for user-visible changes in changes/, orbit/changes/ or ee/fleetd-chrome/changes.
  • 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

  • Added/updated automated tests
  • QA'd all new/changed functionality manually

Database migrations

  • Checked schema for all modified table for columns that will auto-update timestamps during migration.
  • Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects.
  • Ensured the correct collation is explicitly set for character columns (COLLATE utf8mb4_unicode_ci).

Summary by CodeRabbit

  • New Features
    • Support up to 10 custom packages per software title, with package-scoped deploy/install settings and package-specific edit/delete.
    • Policy automation can pin “Install software” to a specific package.
    • GitOps generation now outputs repeatable multi-package YAML per title.
    • Software install details now display the package SHA-256 hash (when available) with a copy-to-clipboard action.
  • Bug Fixes
    • Improved deterministic “first-added” precedence for installs, listings, setup experience, and policy dispatch when multiple packages match.
    • Fixed installer targeting and per-package metadata persistence (including installer-specific activity details).
  • Accessibility/UX
    • Added optional aria-label support for dropdowns.
    • Info banners can now render a leading icon with configurable color.

jkatz01 and others added 7 commits July 3, 2026 10:02
<!-- 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 -->
Copilot AI review requested due to automatic review settings July 10, 2026 18:36
@cdcme cdcme requested review from a team as code owners July 10, 2026 18:36
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

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

Walkthrough

This 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

  • fleetdm/fleet#48596 — Implements related multi-package installer precedence, deduplication, and update behavior.
  • fleetdm/fleet#48710 — Shares the multi-package GitOps YAML generation path and tests.
  • fleetdm/fleet#49085 — Shares SHA-256 propagation from installer results to activities and UI.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.17% 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 clearly and concisely summarizes the main change: supporting multiple custom packages per software title.
Description check ✅ Passed The description matches the repository template and includes the required issue, testing, and migration sections with relevant details.
Linked Issues check ✅ Passed The changes align with #28108: multi-package support, first-added precedence, validation, GitOps/UI/API updates, and related tests are all represented.
Out of Scope Changes check ✅ Passed No clearly unrelated code changes stand out; the added UI, API, datastore, and test updates all support the multi-package software-title feature.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/28108-multiple-custom-packages

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.

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.

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.

Comment thread server/service/team_policies.go
Comment thread frontend/interfaces/software.ts
…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

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.36415% with 209 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.15%. Comparing base (e6eb379) to head (4ce133c).
⚠️ Report is 18 commits behind head on main.

Files with missing lines Patch % Lines
server/datastore/mysql/software_installers.go 84.63% 29 Missing and 22 partials ⚠️
ee/server/service/software_installers.go 80.30% 14 Missing and 12 partials ⚠️
server/datastore/mysql/software.go 92.09% 14 Missing and 12 partials ⚠️
...tleDetailsPage/AddPackageModal/AddPackageModal.tsx 48.71% 20 Missing ⚠️
...s/20260715214831_MultipleCustomPackagesPerTitle.go 77.77% 8 Missing and 6 partials ⚠️
server/service/software_titles.go 68.18% 8 Missing and 6 partials ⚠️
server/datastore/mysql/software_titles.go 84.81% 6 Missing and 6 partials ⚠️
server/service/team_policies.go 67.74% 7 Missing and 3 partials ⚠️
...olicyAutomationsFields/PolicyAutomationsFields.tsx 80.43% 8 Missing and 1 partial ⚠️
cmd/fleetctl/fleetctl/generate_gitops.go 91.30% 3 Missing and 3 partials ⚠️
... and 9 more
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     
Flag Coverage Δ
backend 69.66% <86.20%> (+0.13%) ⬆️
frontend 60.07% <81.17%> (+0.57%) ⬆️

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.

@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: 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 win

Use 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 canActivateMultiplePackages in 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

generateMultiPackage doesn't export Configuration (managed app config), unlike the single-package path.

The single-package branch above unwraps and writes softwareTitle.SoftwarePackage.Configuration to a side file (in-house app managed XML config), but the per-package loop in generateMultiPackage has no equivalent — any in-house app package with Configuration set 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 win

Use the policy’s installer ID when exporting install_software. In cmd/fleetctl/fleetctl/generate_gitops.go:1655, generatePolicies keys cmd.SoftwareList by SoftwareTitleID, so a multi-package policy pinned to a non-first package is serialized with the title’s first-added hash. Fall back to SoftwareTitleID only when SoftwareInstallerID is 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 win

Cover the installer-specific delete request.

Line 78 only verifies presentation; it neither supplies an installerId nor 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 win

Extract the shared banner to break the import cycle.

Line 35 imports from SoftwareCustomPackage, while that page imports PackageForm. Move GitOpsCustomPackageBanner into 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 tradeoff

Optional: per-package status summary is an N+1.

GetSummaryHostSoftwareInstalls is called once per package inside the loop (up to 10 round-trips per title-detail request). If a batched variant keyed by installerID is (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

📥 Commits

Reviewing files that changed from the base of the PR and between e0c6411 and 78b9176.

⛔ Files ignored due to path filters (2)
  • frontend/docs/patterns.md is excluded by !**/*.md
  • server/datastore/mysql/testdata/select_software_titles_sql_fixture.gz is excluded by !**/*.gz
📒 Files selected for processing (92)
  • changes/28108-multiple-custom-packages
  • cmd/fleetctl/fleetctl/generate_gitops.go
  • cmd/fleetctl/fleetctl/generate_gitops_test.go
  • cmd/fleetctl/fleetctl/get_test.go
  • cmd/fleetctl/fleetctl/testing_utils/testing_utils.go
  • cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go
  • ee/server/service/software_installers.go
  • ee/server/service/software_installers_test.go
  • frontend/__mocks__/softwareMock.ts
  • frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tests.tsx
  • frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx
  • frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/_styles.scss
  • frontend/components/InfoBanner/InfoBanner.tsx
  • frontend/components/InfoBanner/_styles.scss
  • frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx
  • frontend/hooks/useBlockNavigation.tests.ts
  • frontend/hooks/useBlockNavigation.ts
  • frontend/interfaces/policy.ts
  • frontend/interfaces/software.ts
  • frontend/pages/ManageControlsPage/SetupExperience/cards/InstallSoftware/components/InstallSoftwareTable/InstallSoftwareTableConfig.tsx
  • frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/SoftwareCustomPackage.tests.tsx
  • frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/SoftwareCustomPackage.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AddPackageModal/AddPackageModal.tests.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AddPackageModal/AddPackageModal.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AddPackageModal/helpers.tests.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AddPackageModal/helpers.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AddPackageModal/index.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/DeleteSoftwareModal/DeleteSoftwareModal.tests.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/DeleteSoftwareModal/DeleteSoftwareModal.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tests.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/LibraryItemAccordion.tests.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/LibraryItemAccordion/LibraryItemAccordion.tsx
  • 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/ViewYamlModal/ViewYamlModal.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/_styles.scss
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/helpers.tests.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/helpers.tsx
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/index.ts
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss
  • frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/helpers.tests.ts
  • frontend/pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx
  • frontend/pages/SoftwarePage/components/forms/PackageForm/PackageForm.tsx
  • frontend/pages/SoftwarePage/helpers.tsx
  • frontend/pages/policies/components/PolicyAutomationsFields/PolicyAutomationsFields.tests.tsx
  • frontend/pages/policies/components/PolicyAutomationsFields/PolicyAutomationsFields.tsx
  • frontend/pages/policies/components/PolicyAutomationsFields/_styles.scss
  • frontend/pages/policies/edit/components/PolicyForm/_styles.scss
  • frontend/pages/policies/helpers.tests.tsx
  • frontend/pages/policies/helpers.ts
  • frontend/pages/policies/hooks/useUpdatePolicyAutomations.ts
  • frontend/services/entities/software.ts
  • frontend/services/entities/team_policies.ts
  • frontend/test/handlers/software-handlers.ts
  • pkg/spec/gitops.go
  • pkg/spec/gitops_test.go
  • server/datastore/mysql/in_house_apps.go
  • server/datastore/mysql/migrations/tables/20260702232839_MultipleCustomPackagesPerTitle.go
  • server/datastore/mysql/migrations/tables/20260702232839_MultipleCustomPackagesPerTitle_test.go
  • server/datastore/mysql/policies.go
  • server/datastore/mysql/policies_test.go
  • server/datastore/mysql/schema.sql
  • server/datastore/mysql/setup_experience.go
  • server/datastore/mysql/setup_experience_test.go
  • server/datastore/mysql/software.go
  • server/datastore/mysql/software_installers.go
  • server/datastore/mysql/software_installers_test.go
  • server/datastore/mysql/software_test.go
  • server/datastore/mysql/software_titles.go
  • server/datastore/mysql/software_titles_test.go
  • server/fleet/activities.go
  • server/fleet/api_policies.go
  • server/fleet/datastore.go
  • server/fleet/errors.go
  • server/fleet/policies.go
  • server/fleet/service.go
  • server/fleet/software.go
  • server/fleet/software_installer.go
  • server/mock/datastore_mock.go
  • server/mock/service/service_mock.go
  • server/service/integration_enterprise_test.go
  • server/service/integration_mdm_setup_experience_test.go
  • server/service/integration_vpp_install_test.go
  • server/service/orbit.go
  • server/service/software_installers.go
  • server/service/software_installers_test.go
  • server/service/software_titles.go
  • server/service/team_policies.go
  • server/service/team_policies_test.go
  • server/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

Comment thread pkg/spec/gitops.go
Comment thread server/datastore/mysql/software.go
RachelElysia and others added 6 commits July 14, 2026 09:51
#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
RachelElysia
RachelElysia previously approved these changes Jul 15, 2026
@georgekarrv georgekarrv merged commit f8438b1 into main Jul 15, 2026
48 checks passed
@georgekarrv georgekarrv deleted the feat/28108-multiple-custom-packages branch July 15, 2026 23:36
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.

Add multiple custom packages for the same software title in the same fleet

5 participants