Skip to content

fix(table output): sort tags so table output is deterministic - #1207

Merged
dangrondahl merged 3 commits into
mainfrom
fix/deterministic-tag-rendering
Sep 18, 2026
Merged

dangrondahl merged 3 commits into
mainfrom
fix/deterministic-tag-rendering

Conversation

@dangrondahl

@dangrondahl dangrondahl commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

The flow and environment table printers rendered tags by iterating the tags map directly, so the order of the rendered pairs changed between runs.

Against a flow tagged app=api, env=prod, team=platform, the released CLI returned three distinct orderings across 20 runs:

$ for i in $(seq 20); do kosli get flow tag-demo --org <org> | grep '^Tags:'; done | sort -u
Tags:                [app=api], [env=prod], [team=platform]
Tags:                [env=prod], [team=platform], [app=api]
Tags:                [team=platform], [app=api], [env=prod]

With this change the same loop returns a single line.

What changed

Tag rendering goes through one sorted helper in cmd/kosli/tableHelpers.go, shared by the six places that each had their own copy: get flow, get environment, list flows, list environments, get repo and get control (list repos shares get repo's).

Both existing output formats are preserved, so ordering is the only user-visible change:

  • formatTags — bracketed pairs ([app=api], [env=prod]) for the flow and environment tables
  • sortedTagPairs — unbracketed pairs (app=api, env=prod) for repo and control

The helper additionally guards a missing or non-map tags value, which get environment asserted unchecked, and formats values with %v rather than %s. Neither is reachable through the API as it behaves today — it sends "tags": {} for an untagged resource, and tags set via kosli tag are always strings — but the sibling printers already guarded this way, so the shared helper does too.

Every existing assertion on tag output used a single tag or none, which is why the ordering was never caught. The new tests use three keys.

Verification

  • go build ./..., make vet, make lint (0 issues)
  • go test ./cmd/kosli/ -run 'TestSortedTagPairs|TestFormatTags|TestTagRendering|TestPrintEnvironmentAsTableWithoutTags'
  • Manually against app.kosli.com, released binary vs. this build, as above

Not yet run: the command suites (GetFlow, ListFlows, GetEnvironment, EnvironmentLs, GetRepo, GetControl, ListControls). GetRepo and GetControl have golden assertions on tag output, so these want a run before merge.

Follow-ups (not in this PR)

Two more clusters of duplication in the table printers:

  • Pagination footer — 5 sites printing the same line from 3 different response envelope shapes, with the noun drifting (items / controls / repos)
  • Empty-result message — 17 sites hand-rolling "No X were found", 8 with the page-number variant

Deciding whether [k=v] or k=v wins, and unifying all six sites on one format, is also left out — that's a deliberate output change.

Checklist

  • Docs are autogenerated from CLI help — no help text changes in this PR
  • Helm chart (charts/k8s-reporter/) — not needed
  • Terraform provider — not needed

@github-actions github-actions Bot added go Pull requests that update go code fix labels Sep 18, 2026
@claude

claude Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Claude finished @dangrondahl's task in 52s —— View job


Follow-up review of 1a1dbdea..3ffbec84

This commit only trims code comments in tableHelpers.go and tableHelpers_test.go (dropping historical narration, keeping the durable "why") — no production logic or test assertions changed.

Status of previously raised findings:

  1. getControl.go — duplicate unbracketed-tag formatting → resolved (unchanged by this commit; thread already resolved).
  2. tableHelpers.go — guard comment contradicted the PR description → still resolved. The comment is further trimmed but remains accurate (no longer claims the API omits the key, no longer over-explains — states the guard's effect only). Thread already marked resolved.
  3. getRepo.go — wrapper duplicated by two callers → resolved (unchanged by this commit; thread already resolved).
  4. tableHelpers_test.go — test name/assertions → still resolved. Test name and require.Equal assertions are unchanged; only the doc comments above TestTagRenderingIsSortedAcrossPrinters and TestPrintEnvironmentAsTableWithoutTags were shortened. Thread already marked resolved.

No new bugs, regressions, or inaccuracies found in the lines this commit touched — the trimmed comments still correctly describe the code's behavior. 0 new inline comments.

Comment thread cmd/kosli/getControl.go Outdated
Comment thread cmd/kosli/tableHelpers.go Outdated
Comment thread cmd/kosli/getRepo.go Outdated
Comment thread cmd/kosli/tableHelpers_test.go Outdated
The flow and environment table printers rendered tags by iterating the tags
map directly, so the order of the rendered pairs changed between runs. Against
a flow tagged app, env and team, `kosli get flow` returned three distinct
orderings across 20 runs.

Tag rendering now goes through one sorted helper, shared by the six places
that had their own copy: get flow, get environment, list flows,
list environments, get repo and get control. Both existing output formats are
preserved, so ordering is the only user-visible change: bracketed pairs for
the flow and environment tables, unbracketed for repo and control.

The helper also guards a missing or non-map tags value, which get environment
asserted unchecked, and formats values with %v rather than %s. Neither is
reachable through the API as it behaves today, which sends "tags": {} for an
untagged resource and string values for tags set via `kosli tag`; both are
kept because the sibling printers already guarded this way.

Every existing assertion on tag output used a single tag or none, which is why
the ordering was never caught. The new tests use three keys.
@dangrondahl
dangrondahl force-pushed the fix/deterministic-tag-rendering branch from 400f149 to d4d02be Compare September 18, 2026 07:15
Review follow-ups on #1207.

formatPlainTags replaces both formatRepoTags and the inline strings.Join in
get control, so the unbracketed format has one implementation instead of two
plus a wrapper named after one of its callers. sortedTagPairs is now reached
only through the two formatters.

The sortedTagPairs doc no longer claims the API omits "tags" when there are
none — it sends an empty object, as the PR description says — so the guard is
described as defensive instead. The same wrong claim is corrected in the test.

TestTagRenderingIsDeterministicAcrossPrinters becomes
TestTagRenderingIsSortedAcrossPrinters: each subtest renders once, so what it
asserts is sorted order, not determinism. All four now pin the full rendering,
tab padding included, so a column-width change cannot pass silently.

TestFormatRepoTags moves to tableHelpers_test.go as TestFormatPlainTags.
@claude

claude Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Follow-up review of d4d02be7..1a1dbdea

All four previous findings are resolved by this commit, and I've marked those review threads resolved:

  1. getControl.go — duplicate unbracketed-tag formatting → resolved. getControl.go now calls the new formatPlainTags helper instead of re-joining sortedTagPairs itself.
  2. tableHelpers.go — guard comment contradicted the PR description → resolved. The comment now says responses carry an empty tags object today and that the guard is defensive (matching the PR description), instead of claiming the key is omitted.
  3. getRepo.go — wrapper duplicated by two callers, comment referencing sibling code → resolved. formatRepoTags is deleted entirely; getRepo.go and listRepos.go both call the new formatPlainTags in tableHelpers.go, which now has a self-contained doc comment (no cross-reference to formatTags).
  4. tableHelpers_test.go — test name/assertions → resolved. Renamed to TestTagRenderingIsSortedAcrossPrinters, and all four subtests now assert the full rendered output with require.Equal instead of require.Contains on a fragment.

No new bugs or regressions found in the lines touched by this commit — sortedTagPairs is correctly reduced to two in-package callers (formatTags, formatPlainTags), and no dead code or unused imports remain from the formatRepoTags removal.

View job run

The comments narrated the bug instead of the constraint: that the released CLI
returned three distinct orderings across 20 runs, that get environment used to
assert the tags value unchecked, and that the other printers already tolerated
both shapes. None of that is useful to a reader a year from now, and it belongs
here in the history rather than in the source.

What is left states the constraint the code cannot express on its own: map
iteration order must not reach table output, an absent tags value must render
as "None" rather than panic, and the printer tests assert full renderings so a
column-width change cannot pass silently.
@dangrondahl
dangrondahl enabled auto-merge (squash) September 18, 2026 07:58
@dangrondahl
dangrondahl merged commit 1e119cb into main Sep 18, 2026
25 checks passed
@dangrondahl
dangrondahl deleted the fix/deterministic-tag-rendering branch September 18, 2026 09:42
social4hyq pushed a commit to social4hyq/homebrew-core that referenced this pull request Sep 20, 2026
kosli-cli 2.43.1

Created-by: HarmonybrewBot
Commit-by: HarmonybrewBot
Merged-by: HarmonybrewBot
Description: Created by `brew bump`

---

Created with `brew bump-formula-pr`.<details>
  <summary>release notes</summary>
  <pre>- Attestation commands now warn (instead of failing) when a CI-defaulted `--commit` cannot be resolved from the repository, allowing jobs without a checked-out repo to proceed without commit info.
- Commands that require commit info (`attest pullrequest`, `attest jira`) now emit a clear error when the commit cannot be resolved, rather than silently proceeding or panicking.
- Explicit `--commit` or `--repo-root` flags that cannot be resolved now produce a descriptive error pointing to the correct fix.

<!-- Release notes generated using configuration in .github/release.yml at v2.43.1 -->

## What's Changed
* fix(k8s): name artifacts by image reference when the runtime reports an image ID by @dangrondahl in kosli-dev/cli#1204
* chore: replace interface{} with any and enforce it via lint by @dangrondahl in kosli-dev/cli#1206
* fix(attest): don't fail when a CI-defaulted --commit has no repository by @mbevc1 in kosli-dev/cli#1202
* fix(table output): sort tags so table output is deterministic by @dangrondahl in kosli-dev/cli#1207


**Full Changelog**: kosli-dev/cli@v2.43.0...v2.43.1

</pre>
  <p>View the full release notes at <a href="https://github.com/kosli-dev/cli/releases/tag/v2.43.1">https://github.com/kosli-dev/cli/releases/tag/v2.43.1</a>.</p>
</details>
<hr>

See merge request: Harmonybrew/homebrew-core!20579
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix go Pull requests that update go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants