Skip to content

feat(pro report): add --bundle-id and --path to software-installs - #348

Open
ktn-jamf wants to merge 3 commits into
mainfrom
claude/pro-report-software-bundle-id
Open

feat(pro report): add --bundle-id and --path to software-installs#348
ktn-jamf wants to merge 3 commits into
mainfrom
claude/pro-report-software-bundle-id

Conversation

@ktn-jamf

@ktn-jamf ktn-jamf commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Closes #325.

pro report software-installs reported three columns: title, version, device_count. This adds --bundle-id and --path, which report an application's bundle identifier and its install path.

Both fields were already on the wire. The ComputerApplication schema declares bundleId and path, and the function already read path to feed isSystemApp. No spec change and no regeneration are needed.

Each flag extends the grouping key

This is the correctness point of the change. One title and version can map to several bundle identifiers or several install paths — a repackaged application, or the same application installed in two locations. A display-only column would print one arbitrary member of the group and hide the rest, with nothing in the output to reveal it. So each flag joins the aggregation key:

$ jamf-cli pro report software-installs --title Zoom --bundle-id -o table
 BUNDLE_ID                DEVICE_COUNT  TITLE  VERSION
 com.example.zoom-repack  1             Zoom   5.0
 us.zoom.xos              2             Zoom   5.0

Without the flag those three installs are one row reading 3 for Zoom 5.0.

Column order is alphabetical. The table formatter reads its column set from sortedKeys(rows[0]), which floats only id and name, so every table in this CLI orders the rest that way. Use --select title,version,bundle_id,device_count for a different order.

softwareKey gains bundleID and path, left at the zero value when the flag is off, so the counting loop keys itself correctly in all four flag combinations with no branching. Sorting gained tie-breakers on bundle identifier then path, because a title and version can now span rows.

The default output is unchanged

The row builder gates each optional key on the flag, never on whether the value is empty. A table's columns are the keys of its first row, so an emptiness gate would make the column set depend on sort order, and an application with no bundleId on the wire would drop the column for every row. Both flags are off by default, so anything parsing the current three columns is unaffected. A test asserts that the default rows carry exactly those three keys and no more.

Flag names

Kebab-case, matching --include-system and every other flag in this CLI. The issue text asks for --bundle_id; snake_case would be the only such flag in the binary. --path covers the --file_path request in the issue comment.

Tests

Four cases beside the existing four:

  • TestRunReportSoftwareInstalls_BundleIDExtendsTheGroupingKey
  • TestRunReportSoftwareInstalls_PathExtendsTheGroupingKey
  • TestRunReportSoftwareInstalls_DefaultRowsCarryOnlyTheThreeOriginalKeys
  • TestRunReportSoftwareInstalls_BundleIDAndPathTogether

The first two are the ones that fail if the grouping is ever reduced to a display column. That was confirmed by mutation: discarding the two key assignments makes exactly the grouping tests fail, and no others.

make test passes in full with zero failures. go vet ./internal/commands/ is clean. make verify-site and make verify-site-output both pass, since the change touches docs/site.

Second commit, from review

A deep review found one confirmed defect and two real test gaps.

Both flags are boolean and both names invite a value, so --path /Applications/Foo.app set the flag and left the path as a positional argument, which the command discarded in silence and then reported the whole fleet. The operator got fleet-wide output for what they had typed as a filter. Args: cobra.NoArgs refuses it: cobra reports it as unknown command, which ClassifyError already maps to exit 2, and ValidateArgs runs ahead of PersistentPreRunE, so the refusal lands before any credential resolves or any request is sent.

The hole itself is repo-wide and pre-existing — pro report inventory-summary junkarg and pro categories list junkarg swallow a positional the same way — so the fix is scoped to the one command whose new flag names make a latent hole reachable. The general case deserves its own change.

The two test gaps: nothing in this file asserted row order at all, so the new tie-breakers were unobservable, and neither empty-value case was covered. TestRunReportSoftwareInstalls_RowOrderFollowsEveryComparatorLevel puts a tie at each of the four comparator levels and pins the full sequence; flipping any level in turn fails it. Two further tests cover an application the wire reports with no bundleId and one with no path, both of which still get a row carrying the column, because the row builder gates on the flag rather than on the value. Mutating either gate to test the value fails them.

The two flags also joined docs/site/examples.json. No test validates that file, so it was checked by parsing it, comparing its key set against every other entry, and running both documented commands.

The command needs a live Jamf Pro tenant, so the fixture-client tests are the runtime evidence for the aggregation. The rendered --help was checked on the built binary.

make lint was not run. The golangci-lint on the development machine is built with go1.26.2 while go.mod targets 1.27.0, so it aborts at config load before it reads any file. CI installs its own version and is unaffected.

🤖 Generated with Claude Code

`software-installs` reported title, version and device_count, aggregating over
/v4/computers-inventory?section=APPLICATIONS. Both new fields were already on
the wire: the ComputerApplication schema declares bundleId and path, and the
function already read path to feed isSystemApp.

Each flag extends the grouping key rather than adding a display column. One
title and version can map to several bundle IDs or several install paths — a
repackaged app, or the same app installed in two locations — so a column alone
would print one arbitrary member of the group and silently hide the rest. The
two grouping tests are the ones that fail if that is ever undone.

softwareKey gains bundleID and path, left at the zero value when the flag is
off, so the counting loop keys itself correctly in all four flag combinations
with no branching. Sorting gained tie-breakers on bundle ID then path, because a
title and version can now span rows.

The row builder gates each optional key on the flag, never on whether the value
is empty. A table's columns are the keys of its first row, so an emptiness gate
would make the column set depend on sort order and an application with no
bundleId on the wire would drop the column for every row. Both flags are off by
default, so the default output keeps exactly its three columns, which a test
asserts.

Flags are kebab-case, matching --include-system and every other flag in this
CLI; the issue text asked for --bundle_id.

Verified: `make test` green in full. The four new tests cover each flag alone,
both together, and the unchanged default. The command needs a live tenant, so
the fixture-client tests are the runtime evidence for the aggregation; the
rendered --help was checked on the built binary.

Closes #325

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ktn-jamf

This comment was marked as outdated.

Both flags are boolean, and both names invite a value. `pro report
software-installs --path /Applications/Foo.app` set --path true and left the
path as a positional argument, which the command discarded without a word. It
then ran the full fleet report, so the operator got fleet-wide output for what
they had typed as a filter. Verified on the built binary before the fix: that
invocation, and a bare `software-installs junkarg`, produced output identical
to a clean run and exited 1 on the network rather than 2 on usage.

The hole is repo-wide and pre-existing. `pro report inventory-summary junkarg`
and `pro categories list junkarg` swallow a positional the same way, so this
change is scoped to the one command whose new flag names make a latent hole
reachable.

`Args: cobra.NoArgs` is the mechanism. Cobra reports the refusal as `unknown
command`, ClassifyError already maps that prefix to exitcode.Usage, and
ValidateArgs runs ahead of PersistentPreRunE, so the refusal lands before any
credential resolves or any request is sent. Confirmed on the binary: exit 2
and an exitCodeName of "usage".

Three tests come with it. The sort tie-breakers had no test that could observe
them, because nothing in this file asserted row order at all. The new order
test puts a tie at each of the four comparator levels and pins the full row
sequence. Both flags are on, because the path level is unreachable otherwise:
with --path off every key holds the same empty path, so any two keys reaching
that comparison are the same key. Every level was mutation-proved. Flipping
title, version, bundle ID or path in turn fails the test, and so does dropping
Args: cobra.NoArgs.

The last two tests cover an application the wire reports with no bundleId, and
one with no path. The row builder gates each optional key on the flag and never
on whether the value is empty, so both still get a row and still carry the
column. An emptiness gate would drop the column for every row, because a
table's columns are the keys of its first row. Both mutations were proved as
well. The path case is not a duplicate of the bundle ID case: isSystemApp("")
is false, so an application with no path is never filtered as a system app and
reaches even the default output.

The two new flags also join the site examples.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… env

This commit closes two test-quality gaps in the --bundle-id and --path work.

The default-output test asserted the row key set. Its fixture gave each title
and version one bundle ID and one path. An unconditional assignment to
key.bundleID or key.path therefore changed no row count, and both mutations
kept the whole internal/commands package green. The default output then
rendered one title and version as two identical rows. Each row held half of
the device count, and no column explained the split. The fixture now gives
Zoom 5.0 two bundle IDs and two paths across two devices, and the test
asserts the merged device count.

The stray-positional test is the only test in the file that runs the real
root command, and it read the ambient environment. With the Args guard
removed and Jamf Pro credentials exported, the test sent the paginated
computers-inventory request and took 12 seconds. A developer with live tenant
credentials therefore risked a fleet inventory sweep from `go test ./...`.
The test now calls clearAuthEnv, which root_test.go already uses. The spec
version argument is now "unknown", because checkTenantVersion returns early
only for that value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add flag to include app bundle id to jamf-cli pro report software-installs

1 participant