From 8e54468f0e8bf04a72bc346b9ebd38f98c4377ab Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Fri, 13 Nov 2026 15:48:57 +0000 Subject: [PATCH] feat: list only unread product updates, 10 at most, with --dismiss kosli list product-updates answered 'what is new for me?' with a list that was mostly updates already read. It now lists only unread updates; --all lists the read ones too. The Kosli app now shows at most 10 product updates, so the CLI lists the same 10 unless --limit says otherwise; --limit 0 means no limit. --all and --limit apply to the table and to JSON, and the table says when more exist. --dismiss marks the unread updates listed as seen. Listing does not do that by default, as the app does on opening its dropdown: the command also runs in scripts and pipes, where it would use up a user's unread updates unseen. The help also said a first listing marks every update as seen. The server now does that only for users who signed up less than 7 days before, so existing users see the last 12 months as unread. --- cmd/kosli/dismissProductUpdates.go | 15 +- cmd/kosli/listProductUpdates.go | 174 ++++++++++++++-- cmd/kosli/listProductUpdates_test.go | 186 +++++++++++++++++- cmd/kosli/root.go | 3 + .../testdata/empty-flag-audit-coverage.json | 3 + hack/empty-flag-audit/categories.json | 2 + hack/empty-flag-audit/spec.json | 6 + 7 files changed, 364 insertions(+), 25 deletions(-) diff --git a/cmd/kosli/dismissProductUpdates.go b/cmd/kosli/dismissProductUpdates.go index c7fb15e41..f1cce52da 100644 --- a/cmd/kosli/dismissProductUpdates.go +++ b/cmd/kosli/dismissProductUpdates.go @@ -103,7 +103,18 @@ func (o *dismissProductUpdatesOptions) run(ids []string) error { } ids = uniqueNonEmpty(ids) + if err := markProductUpdatesSeen(ids); err != nil { + return err + } + if !global.DryRun { + logger.Info("%d product update(s) dismissed", len(ids)) + } + return nil +} +// markProductUpdatesSeen sends ids in requests of at most +// maxProductUpdateIDsPerRequest. +func markProductUpdatesSeen(ids []string) error { reqURL, err := url.JoinPath(global.Host, "api/v2/product-updates", global.Org, "seen") if err != nil { return err @@ -125,10 +136,6 @@ func (o *dismissProductUpdatesOptions) run(ids []string) error { return err } } - - if !global.DryRun { - logger.Info("%d product update(s) dismissed", len(ids)) - } return nil } diff --git a/cmd/kosli/listProductUpdates.go b/cmd/kosli/listProductUpdates.go index 93e1a6904..ad1e3760c 100644 --- a/cmd/kosli/listProductUpdates.go +++ b/cmd/kosli/listProductUpdates.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "encoding/json" "fmt" "io" @@ -16,21 +17,40 @@ const listProductUpdatesShortDesc = `List Kosli product updates.` const listProductUpdatesLongDesc = listProductUpdatesShortDesc + ` Product updates are the new features and improvements published in Kosli's changelog. -Unread updates are listed first, then the newest. An update about a feature that is not -enabled for the org is left out. The first time you list them, in the CLI or in the Kosli app, -every update is marked as seen, so none is shown as unread; only updates published after that -start as unread. Dismiss those with ^kosli dismiss product-updates^. -The update's summary is included in the JSON output. +Only unread updates are listed; use ^--all^ to list the ones you have seen too, unread first, +then the newest. An update about a feature that is not enabled for the org is left out. +The first 10 are listed, as in the Kosli app; use ^--limit^ to list more, or ^--limit 0^ for +no limit. ^--all^ and ^--limit^ apply to the JSON output as well, which also holds each +update's summary. Its ^unread_count^ is always the total number of unread updates, however +many are listed. + +An update is unread when you have not seen it and it was published in the last 12 months. +A user who signed up less than 7 days before first listing product updates, in the CLI or in +the Kosli app, starts with every update seen. Listing leaves updates unread: use ^--dismiss^ +to mark the unread updates listed as seen, or ^kosli dismiss product-updates^ later. Product updates are tracked per user, so the API token must belong to a member of the org.` const listProductUpdatesExample = ` -# list product updates: +# list unread product updates: kosli list product-updates \ --api-token yourAPIToken \ --org yourOrgName -# list product updates in JSON: +# list unread product updates and mark them as seen: +kosli list product-updates \ + --dismiss \ + --api-token yourAPIToken \ + --org yourOrgName + +# list every product update, read ones included: +kosli list product-updates \ + --all \ + --limit 0 \ + --api-token yourAPIToken \ + --org yourOrgName + +# list unread product updates in JSON: kosli list product-updates \ --output json \ --api-token yourAPIToken \ @@ -38,7 +58,10 @@ kosli list product-updates \ ` type listProductUpdatesOptions struct { - output string + output string + limit int + all bool + dismiss bool } type listProductUpdatesResponse struct { @@ -69,7 +92,7 @@ func newListProductUpdatesCmd(out io.Writer) *cobra.Command { if err != nil { return ErrorBeforePrintingUsage(cmd, err.Error()) } - return nil + return o.validate(cmd) }, RunE: func(cmd *cobra.Command, args []string) error { return o.run(out) @@ -77,10 +100,20 @@ func newListProductUpdatesCmd(out io.Writer) *cobra.Command { } cmd.Flags().StringVarP(&o.output, "output", "o", "table", outputFlag) + cmd.Flags().IntVar(&o.limit, "limit", 10, productUpdatesLimitFlag) + cmd.Flags().BoolVar(&o.all, "all", false, productUpdatesAllFlag) + cmd.Flags().BoolVar(&o.dismiss, "dismiss", false, productUpdatesDismissFlag) return cmd } +func (o *listProductUpdatesOptions) validate(cmd *cobra.Command) error { + if o.limit < 0 { + return ErrorBeforePrintingUsage(cmd, "--limit must be 0 or more") + } + return nil +} + func (o *listProductUpdatesOptions) run(out io.Writer) error { reqURL, err := url.JoinPath(global.Host, "api/v2/product-updates", global.Org) if err != nil { @@ -96,21 +129,115 @@ func (o *listProductUpdatesOptions) run(out io.Writer) error { return err } - return output.FormattedPrint(response.Body, o.output, out, 0, + selected, err := selectProductUpdates(response.Body, o.all, o.limit) + if err != nil { + return err + } + + err = output.FormattedPrint(selected.body, o.output, out, 0, map[string]output.FormatOutputFunc{ - "table": printProductUpdatesListAsTable, - "json": output.PrintJson, + "table": func(raw string, out io.Writer, page int) error { + return printProductUpdatesListAsTable(raw, out, selected.matching, o.all) + }, + "json": output.PrintJson, }) + if err != nil || !o.dismiss || len(selected.unreadIDs) == 0 { + return err + } + + if err := markProductUpdatesSeen(selected.unreadIDs); err != nil { + return err + } + // A count after the JSON would make the output unparseable. + if !global.DryRun && o.output != "json" { + logger.Info("%d product update(s) dismissed", len(selected.unreadIDs)) + } + return nil } -func printProductUpdatesListAsTable(raw string, out io.Writer, page int) error { +type selectedProductUpdates struct { + // body is the list response holding only the updates selected. + body string + // matching counts the updates that qualified before the limit cut them. + matching int + // unreadIDs are the ids of the unread updates in body. + unreadIDs []string +} + +// selectProductUpdates keeps the unread updates of a list response, or all of +// them with includeRead, and of those the first limit, or all when limit is 0. +func selectProductUpdates(raw string, includeRead bool, limit int) (*selectedProductUpdates, error) { + // Only product_updates is decoded, so the rest of the server's answer + // reaches the JSON output as it was sent. + response := map[string]json.RawMessage{} + if err := json.Unmarshal([]byte(raw), &response); err != nil { + return nil, err + } + updates := []json.RawMessage{} + if listed, ok := response["product_updates"]; ok { + if err := json.Unmarshal(listed, &updates); err != nil { + return nil, err + } + } + + selected := &selectedProductUpdates{unreadIDs: []string{}} + kept := []json.RawMessage{} + for _, rawUpdate := range updates { + update := productUpdate{} + if err := json.Unmarshal(rawUpdate, &update); err != nil { + return nil, err + } + if !includeRead && !update.Unread { + continue + } + selected.matching++ + if limit != 0 && len(kept) == limit { + continue + } + kept = append(kept, rawUpdate) + if update.Unread { + selected.unreadIDs = append(selected.unreadIDs, update.ID) + } + } + + keptJSON, err := marshalKeepingHTMLCharacters(kept) + if err != nil { + return nil, err + } + response["product_updates"] = keptJSON + body, err := marshalKeepingHTMLCharacters(response) + if err != nil { + return nil, err + } + selected.body = string(body) + return selected, nil +} + +// marshalKeepingHTMLCharacters encodes v as json.Marshal does, but leaves the +// &, < and > of a title or summary as they are instead of as \u escapes. +func marshalKeepingHTMLCharacters(v any) ([]byte, error) { + encoded := &bytes.Buffer{} + encoder := json.NewEncoder(encoded) + encoder.SetEscapeHTML(false) + if err := encoder.Encode(v); err != nil { + return nil, err + } + return bytes.TrimSpace(encoded.Bytes()), nil +} + +func printProductUpdatesListAsTable(raw string, out io.Writer, matching int, includeRead bool) error { response := &listProductUpdatesResponse{} if err := json.Unmarshal([]byte(raw), response); err != nil { return err } - if len(response.ProductUpdates) == 0 { - logger.Info("No product updates were found.") + shown := len(response.ProductUpdates) + if shown == 0 { + if includeRead { + logger.Info("No product updates were found.") + } else { + logger.Info("No unread product updates. Use --all to list the ones you have seen.") + } return nil } @@ -125,10 +252,23 @@ func printProductUpdatesListAsTable(raw string, out io.Writer, page int) error { update.ID, update.Title, publishedAt, update.Unread, update.URL)) } - rows = append(rows, fmt.Sprintf("\n%d unread of %d product updates", - response.UnreadCount, len(response.ProductUpdates))) + rows = append(rows, "\n"+productUpdatesFooter(shown, matching, response.UnreadCount, includeRead)) tabFormattedPrint(out, header, rows) return nil } + +func productUpdatesFooter(shown, matching, unreadCount int, includeRead bool) string { + const more = " Use --limit to list more." + switch { + case includeRead && shown < matching: + return fmt.Sprintf("Showing %d of %d product updates, %d unread in total.", shown, matching, unreadCount) + more + case includeRead: + return fmt.Sprintf("%d unread of %d product updates", unreadCount, matching) + case shown < matching: + return fmt.Sprintf("Showing %d of %d unread product updates.", shown, matching) + more + default: + return fmt.Sprintf("%d unread product update(s)", matching) + } +} diff --git a/cmd/kosli/listProductUpdates_test.go b/cmd/kosli/listProductUpdates_test.go index d82b565de..48452dbac 100644 --- a/cmd/kosli/listProductUpdates_test.go +++ b/cmd/kosli/listProductUpdates_test.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "net/http" + "strings" "testing" "github.com/stretchr/testify/require" @@ -95,7 +96,7 @@ func (suite *ListProductUpdatesCommandTestSuite) TestItReadsTheOrgsProductUpdate func (suite *ListProductUpdatesCommandTestSuite) TestItPrintsATable() { server, _ := newFakeProductUpdates(suite.T(), http.StatusOK, productUpdatesList) - _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "")) + _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "--all")) require.NoError(suite.T(), err) require.Equal(suite.T(), @@ -112,16 +113,183 @@ func (suite *ListProductUpdatesCommandTestSuite) TestItPrintsNAForAMissingPublis `{"unread_count":0,"product_updates":[{"id":"controls","title":"Controls",`+ `"url":"https://docs.kosli.com/changelog#controls","summary":"","unread":false}]}`) - _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "")) + _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "--all")) require.NoError(suite.T(), err) require.Contains(suite.T(), combined, "controls Controls N/A false") } +// manyProductUpdates is a list of count updates, u0 first, with the first unread of them unread. +func manyProductUpdates(count, unread int) string { + updates := make([]string, count) + for i := range updates { + updates[i] = fmt.Sprintf(`{"id":"u%d","title":"Update %d","url":"https://docs.kosli.com/changelog#u%d",`+ + `"summary":"","published_at":1757000000.5,"unread":%t}`, i, i, i, i < unread) + } + return fmt.Sprintf(`{"unread_count":%d,"product_updates":[%s]}`, unread, strings.Join(updates, ",")) +} + +func (suite *ListProductUpdatesCommandTestSuite) TestItListsOnlyUnreadUpdatesByDefault() { + server, _ := newFakeProductUpdates(suite.T(), http.StatusOK, productUpdatesList) + + _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "")) + + require.NoError(suite.T(), err) + require.Equal(suite.T(), + "ID TITLE PUBLISHED UNREAD URL\n"+ + "rego-policies Rego policies Sat, 16 Jan 2016 00:00:00 UTC true https://docs.kosli.com/changelog#rego-policies\n"+ + "\n"+ + "1 unread product update(s)\n", + combined) +} + +func (suite *ListProductUpdatesCommandTestSuite) TestJSONHoldsOnlyUnreadUpdatesByDefault() { + server, _ := newFakeProductUpdates(suite.T(), http.StatusOK, productUpdatesList) + + _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "--output json")) + + require.NoError(suite.T(), err) + require.JSONEq(suite.T(), `{"unread_count":1,"product_updates":[`+ + `{"id":"rego-policies","title":"Rego policies","url":"https://docs.kosli.com/changelog#rego-policies",`+ + `"summary":"Store and version Rego policies in Kosli.","published_at":1757000000.5,"unread":true}]}`, combined) +} + +func (suite *ListProductUpdatesCommandTestSuite) TestJSONKeepsUnknownFieldsAndHTMLCharacters() { + server, _ := newFakeProductUpdates(suite.T(), http.StatusOK, + `{"unread_count":1,"feed_url":"https://docs.kosli.com/changelog","product_updates":[`+ + `{"id":"a","summary":"Rego & ","unread":true},{"id":"b","unread":false}]}`) + + _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "--output json")) + + require.NoError(suite.T(), err) + require.JSONEq(suite.T(), `{"unread_count":1,"feed_url":"https://docs.kosli.com/changelog","product_updates":[`+ + `{"id":"a","summary":"Rego & ","unread":true}]}`, combined) + require.Contains(suite.T(), combined, "Rego & ") +} + +func (suite *ListProductUpdatesCommandTestSuite) TestItSaysWhenNothingIsUnread() { + server, _ := newFakeProductUpdates(suite.T(), http.StatusOK, manyProductUpdates(3, 0)) + + _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "")) + + require.NoError(suite.T(), err) + require.Equal(suite.T(), "No unread product updates. Use --all to list the ones you have seen.\n", combined) +} + +func (suite *ListProductUpdatesCommandTestSuite) TestDismissMarksTheUnreadUpdatesListedAsSeen() { + server, fake := newFakeProductUpdates(suite.T(), http.StatusCreated, "null") + fake.listBody = productUpdatesList + + _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "--all --dismiss")) + + require.NoError(suite.T(), err) + require.Equal(suite.T(), "/api/v2/product-updates/test-org/seen", fake.escapedPath) + require.Len(suite.T(), fake.posted, 1) + require.JSONEq(suite.T(), `{"ids":["rego-policies"]}`, fake.posted[0]) + require.Contains(suite.T(), combined, "1 unread of 2 product updates\n1 product update(s) dismissed\n") +} + +func (suite *ListProductUpdatesCommandTestSuite) TestDismissLeavesUpdatesBeyondTheLimitUnread() { + server, fake := newFakeProductUpdates(suite.T(), http.StatusCreated, "null") + fake.listBody = manyProductUpdates(12, 11) + + _, _, _, _, err := executeCommandC(suite.listCmd(server.URL, "--dismiss")) + + require.NoError(suite.T(), err) + require.Len(suite.T(), fake.posted, 1) + require.Equal(suite.T(), 10, strings.Count(fake.posted[0], `"u`)) + require.NotContains(suite.T(), fake.posted[0], `"u10"`) +} + +func (suite *ListProductUpdatesCommandTestSuite) TestDismissSendsNothingWhenNothingIsUnread() { + server, fake := newFakeProductUpdates(suite.T(), http.StatusCreated, "null") + fake.listBody = manyProductUpdates(3, 0) + + _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "--dismiss")) + + require.NoError(suite.T(), err) + require.Empty(suite.T(), fake.posted) + require.NotContains(suite.T(), combined, "dismissed") +} + +func (suite *ListProductUpdatesCommandTestSuite) TestDismissKeepsJSONOutputClean() { + server, fake := newFakeProductUpdates(suite.T(), http.StatusCreated, "null") + fake.listBody = manyProductUpdates(2, 2) + + _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "--dismiss --output json")) + + require.NoError(suite.T(), err) + require.Len(suite.T(), fake.posted, 1) + require.JSONEq(suite.T(), manyProductUpdates(2, 2), combined) +} + +func (suite *ListProductUpdatesCommandTestSuite) TestAFailedDismissIsAnError() { + server, fake := newFakeProductUpdates(suite.T(), http.StatusCreated, "null") + fake.listBody = manyProductUpdates(2, 2) + fake.failOnPost = 1 + + _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "--dismiss")) + + require.Error(suite.T(), err) + require.Contains(suite.T(), combined, "boom") +} + +func (suite *ListProductUpdatesCommandTestSuite) TestItListsTheFirst10ByDefault() { + server, _ := newFakeProductUpdates(suite.T(), http.StatusOK, manyProductUpdates(12, 11)) + + _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "")) + + require.NoError(suite.T(), err) + require.Contains(suite.T(), combined, "\nu9 ") + require.NotContains(suite.T(), combined, "\nu10 ") + require.Contains(suite.T(), combined, + "\nShowing 10 of 11 unread product updates. Use --limit to list more.\n") +} + +func (suite *ListProductUpdatesCommandTestSuite) TestLimitSetsHowManyAreListed() { + server, _ := newFakeProductUpdates(suite.T(), http.StatusOK, manyProductUpdates(12, 1)) + + _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "--all --limit 2")) + + require.NoError(suite.T(), err) + require.Contains(suite.T(), combined, "\nu1 ") + require.NotContains(suite.T(), combined, "\nu2 ") + require.Contains(suite.T(), combined, "\nShowing 2 of 12 product updates, 1 unread in total.") +} + +func (suite *ListProductUpdatesCommandTestSuite) TestLimit0ListsAll() { + server, _ := newFakeProductUpdates(suite.T(), http.StatusOK, manyProductUpdates(12, 1)) + + _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "--all --limit 0")) + + require.NoError(suite.T(), err) + require.Contains(suite.T(), combined, "\nu11 ") + require.Contains(suite.T(), combined, "\n1 unread of 12 product updates\n") +} + +func (suite *ListProductUpdatesCommandTestSuite) TestLimitAlsoAppliesToJSON() { + server, _ := newFakeProductUpdates(suite.T(), http.StatusOK, manyProductUpdates(12, 11)) + + _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "--limit 3 --output json")) + + require.NoError(suite.T(), err) + require.JSONEq(suite.T(), strings.Replace(manyProductUpdates(3, 3), `"unread_count":3`, `"unread_count":11`, 1), combined) +} + +func (suite *ListProductUpdatesCommandTestSuite) TestItRefusesANegativeLimit() { + server, fake := newFakeProductUpdates(suite.T(), http.StatusOK, productUpdatesList) + + _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "--limit -1")) + + require.Error(suite.T(), err) + require.Contains(suite.T(), combined, "illegal") + require.Equal(suite.T(), 0, fake.calls) +} + func (suite *ListProductUpdatesCommandTestSuite) TestItSaysWhenThereAreNoProductUpdates() { server, _ := newFakeProductUpdates(suite.T(), http.StatusOK, productUpdatesEmptyList) - _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "")) + _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "--all")) require.NoError(suite.T(), err) require.Equal(suite.T(), "No product updates were found.\n", combined) @@ -130,7 +298,7 @@ func (suite *ListProductUpdatesCommandTestSuite) TestItSaysWhenThereAreNoProduct func (suite *ListProductUpdatesCommandTestSuite) TestItPrintsTheServerAnswerAsJSON() { server, _ := newFakeProductUpdates(suite.T(), http.StatusOK, productUpdatesList) - _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "--output json")) + _, combined, _, _, err := executeCommandC(suite.listCmd(server.URL, "--all --output json")) require.NoError(suite.T(), err) require.JSONEq(suite.T(), productUpdatesList, combined) @@ -146,6 +314,16 @@ func (suite *ListProductUpdatesCommandTestSuite) TestItReturnsAServerError() { require.Contains(suite.T(), combined, "not enabled for organization 'test-org'") } +// The root command already refuses a flag value starting with "-"; this pins +// the command's own rule, which must hold without that guard. +func TestListProductUpdatesRefusesANegativeLimitItself(t *testing.T) { + o := &listProductUpdatesOptions{limit: -1} + + err := o.validate(newListProductUpdatesCmd(io.Discard)) + + require.ErrorContains(t, err, "--limit must be 0 or more") +} + func TestListProductUpdatesCommandTestSuite(t *testing.T) { suite.Run(t, new(ListProductUpdatesCommandTestSuite)) } diff --git a/cmd/kosli/root.go b/cmd/kosli/root.go index 496907730..8d3051c86 100644 --- a/cmd/kosli/root.go +++ b/cmd/kosli/root.go @@ -167,6 +167,9 @@ Paths the list already matches stay excluded whatever is later added there, so k evaluationShowInputFlag = "[optional] Add the document the policy read as input under .input. Needs --output json." evaluationFixtureFlag = "[optional] Write the evaluation's input, params and policy files to --dir, to replay it with OPA." evaluationFixtureDirFlag = "[conditional] The directory --fixture writes to. It must be empty or not exist yet. Only needed with --fixture." + productUpdatesLimitFlag = "[defaulted] The maximum number of product updates to list. 0 means no limit." + productUpdatesAllFlag = "[optional] List the product updates you have seen too, not only the unread ones." + productUpdatesDismissFlag = "[optional] Mark the unread product updates listed as seen." dismissAllProductUpdatesFlag = "[optional] Dismiss every unread product update, instead of giving their ids." regoPolicySearchFlag = "[optional] Only list rego policies whose name or identifier contains this substring (case-insensitive)." regoPolicySortFlag = "[optional] The field to sort rego policies by. Valid values are: [updated_at, identifier]. (defaults to updated_at)" diff --git a/cmd/kosli/testdata/empty-flag-audit-coverage.json b/cmd/kosli/testdata/empty-flag-audit-coverage.json index 5484e54b0..c38eb9774 100644 --- a/cmd/kosli/testdata/empty-flag-audit-coverage.json +++ b/cmd/kosli/testdata/empty-flag-audit-coverage.json @@ -809,6 +809,9 @@ "output": "string" }, "list product-updates": { + "all": "bool", + "dismiss": "bool", + "limit": "int", "output": "string" }, "list rego-policies": { diff --git a/hack/empty-flag-audit/categories.json b/hack/empty-flag-audit/categories.json index 0e2cab91d..3ccde9d44 100644 --- a/hack/empty-flag-audit/categories.json +++ b/hack/empty-flag-audit/categories.json @@ -37,6 +37,7 @@ "description": "metadata", "digests-source": "identity", "dir": "location", + "dismiss": "switch", "display-name": "metadata", "dry-run": "switch", "e": "identity", @@ -87,6 +88,7 @@ "jira-username": "credentials", "jq": "location", "kubeconfig": "credentials", + "limit": "output", "link": "metadata", "locations": "filter", "logical": "identity", diff --git a/hack/empty-flag-audit/spec.json b/hack/empty-flag-audit/spec.json index 8292a2a90..0346679ba 100644 --- a/hack/empty-flag-audit/spec.json +++ b/hack/empty-flag-audit/spec.json @@ -5218,9 +5218,15 @@ "error": "the server the audit runs against does not offer the product updates endpoints", "needs": "a server that implements it", "flags_to_test": [ + "all", + "dismiss", + "limit", "output" ], "flag_values": { + "all": "true", + "dismiss": "true", + "limit": "5", "output": "json" }, "setup": [],