From b5498efc26f711b3363939c892e5d003484f011c Mon Sep 17 00:00:00 2001 From: Simon Castagna Date: Wed, 28 Jan 2026 10:23:10 +0100 Subject: [PATCH 1/3] Add flow-tag flag to list trails --- cmd/kosli/listTrails.go | 5 +++++ cmd/kosli/listTrails_test.go | 14 +++++++++++++- cmd/kosli/root.go | 1 + cmd/kosli/testHelpers.go | 12 ++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/cmd/kosli/listTrails.go b/cmd/kosli/listTrails.go index 215d0683e..cc1770be8 100644 --- a/cmd/kosli/listTrails.go +++ b/cmd/kosli/listTrails.go @@ -56,6 +56,7 @@ type listTrailsOptions struct { listOptions flowName string fingerprint string + flowTag string } type Trail struct { @@ -97,6 +98,7 @@ func newListTrailsCmd(out io.Writer) *cobra.Command { cmd.Flags().StringVarP(&o.flowName, "flow", "f", "", flowNameFlagOptional) cmd.Flags().StringVarP(&o.fingerprint, "fingerprint", "F", "", fingerprintInTrailsFlag) + cmd.Flags().StringVarP(&o.flowTag, "flow-tag", "t", "", flowTagFlag) addListFlags(cmd, &o.listOptions, 20) return cmd @@ -110,6 +112,9 @@ func (o *listTrailsOptions) run(out io.Writer) error { if o.fingerprint != "" { url += fmt.Sprintf("&fingerprint=%s", o.fingerprint) } + if o.flowTag != "" { + url += fmt.Sprintf("&flow_tag=%s", o.flowTag) + } reqParams := &requests.RequestParams{ Method: http.MethodGet, diff --git a/cmd/kosli/listTrails_test.go b/cmd/kosli/listTrails_test.go index 06c5ec079..426516fc4 100644 --- a/cmd/kosli/listTrails_test.go +++ b/cmd/kosli/listTrails_test.go @@ -15,6 +15,8 @@ type ListTrailsCommandTestSuite struct { flowName string trailName string fingerprint string + flowTagKey string + flowTagValue string defaultKosliArguments string acmeOrgKosliArguments string } @@ -27,18 +29,23 @@ func (suite *ListTrailsCommandTestSuite) SetupTest() { } suite.flowName = "list-trails" + suite.flowTagKey = "team" + suite.flowTagValue = "backend" suite.trailName = "trail-name" suite.fingerprint = "7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9" + + // First flow (tagged), trail and artifact for the default org suite.defaultKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken) CreateFlowWithTemplate(suite.flowName, "testdata/valid_template.yml", suite.T()) BeginTrail(suite.trailName, suite.flowName, "", suite.T()) CreateArtifactOnTrail(suite.flowName, suite.trailName, "artifact", suite.fingerprint, "artifact-name", suite.T()) + TagFlow(suite.flowName, suite.flowTagKey, suite.flowTagValue, suite.T()) + // Second flow for the acme org global.Org = "acme-org" global.ApiToken = "v3OWZiYWu9G2IMQStYg9BcPQUQ88lJNNnTJTNq8jfvmkR1C5wVpHSs7F00JcB5i6OGeUzrKt3CwRq7ndcN4TTfMeo8ASVJ5NdHpZT7DkfRfiFvm8s7GbsIHh2PtiQJYs2UoN13T8DblV5C4oKb6-yWH73h67OhotPlKfVKazR-c" CreateFlowWithTemplate(suite.flowName, "testdata/valid_template.yml", suite.T()) suite.acmeOrgKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken) - } func (suite *ListTrailsCommandTestSuite) TestListTrailsCmd() { @@ -91,6 +98,11 @@ func (suite *ListTrailsCommandTestSuite) TestListTrailsCmd() { cmd: fmt.Sprintf(`list trails --fingerprint %s --output json %s`, suite.fingerprint, suite.defaultKosliArguments), goldenJson: []jsonCheck{{"data", "non-empty"}}, }, + { + name: "10 can list trails in a flow with the provided tag", + cmd: fmt.Sprintf(`list trails --flow %s --flow-tag %s=%s --output json %s`, suite.flowName, suite.flowTagKey, suite.flowTagValue, suite.defaultKosliArguments), + goldenJson: []jsonCheck{{"data", "non-empty"}}, + }, } runTestCmd(suite.T(), tests) diff --git a/cmd/kosli/root.go b/cmd/kosli/root.go index 5b3c1d54f..b81054ba5 100644 --- a/cmd/kosli/root.go +++ b/cmd/kosli/root.go @@ -93,6 +93,7 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file, flowNameFlag = "The Kosli flow name." flowNameFlagOptional = "[optional] The Kosli flow name." fingerprintInTrailsFlag = "[optional] The SHA256 fingerprint of the artifact to filter trails by." + flowTagFlag = "[optional] A key=value flow tag to filter trails by." trailNameFlag = "The Kosli trail name." trailNameFlagOptional = "[optional] The Kosli trail name." templateArtifactName = "The name of the artifact in the yml template file." diff --git a/cmd/kosli/testHelpers.go b/cmd/kosli/testHelpers.go index c1e25fd40..9b1f2ebd1 100644 --- a/cmd/kosli/testHelpers.go +++ b/cmd/kosli/testHelpers.go @@ -595,3 +595,15 @@ func GetAttestationId(flowName, trailName, attestationName string, t *testing.T) require.True(t, ok, "attestation_id field not found or not a string") return id } + +// TagFlow tags a flow with a key-value pair +func TagFlow(flowName, tagKey, tagValue string, t *testing.T) { + t.Helper() + o := &tagOptions{ + payload: TagResourcePayload{ + SetTags: map[string]string{tagKey: tagValue}, + }, + } + err := o.run([]string{"flow", flowName}) + require.NoError(t, err, "flow should be tagged without error") +} From 441257ea0d89d0f59f1c185e716419662a0ef757 Mon Sep 17 00:00:00 2001 From: Simon Castagna Date: Wed, 28 Jan 2026 11:07:46 +0100 Subject: [PATCH 2/3] Add validation of flow-tag flag --- cmd/kosli/list.go | 10 ---------- cmd/kosli/listTrails.go | 13 ++++++++++++- cmd/kosli/listTrails_test.go | 13 ++++++++++++- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/cmd/kosli/list.go b/cmd/kosli/list.go index f4c335036..5b223e1b6 100644 --- a/cmd/kosli/list.go +++ b/cmd/kosli/list.go @@ -24,16 +24,6 @@ func (o *listOptions) validate(cmd *cobra.Command) error { return nil } -func (o *listOptions) validateForListTrails(cmd *cobra.Command) error { - if o.pageNumber <= 0 { - return ErrorBeforePrintingUsage(cmd, "page number must be a positive integer") - } - if o.pageLimit < 0 { - return ErrorBeforePrintingUsage(cmd, "page limit must be a positive integer") - } - return nil -} - func newListCmd(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "list", diff --git a/cmd/kosli/listTrails.go b/cmd/kosli/listTrails.go index cc1770be8..1c8f93dea 100644 --- a/cmd/kosli/listTrails.go +++ b/cmd/kosli/listTrails.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "strings" "github.com/kosli-dev/cli/internal/output" "github.com/kosli-dev/cli/internal/requests" @@ -59,6 +60,16 @@ type listTrailsOptions struct { flowTag string } +func (o *listTrailsOptions) validate(cmd *cobra.Command) error { + if err := o.listOptions.validate(cmd); err != nil { + return err + } + if o.flowTag != "" && !strings.Contains(o.flowTag, "=") { + return ErrorBeforePrintingUsage(cmd, "flag '--flow-tag' must be in the format of key=value") + } + return nil +} + type Trail struct { Name string `json:"name"` Description string `json:"description"` @@ -89,7 +100,7 @@ func newListTrailsCmd(out io.Writer) *cobra.Command { if err != nil { return ErrorBeforePrintingUsage(cmd, err.Error()) } - return o.validateForListTrails(cmd) + return o.validate(cmd) }, RunE: func(cmd *cobra.Command, args []string) error { return o.run(out) diff --git a/cmd/kosli/listTrails_test.go b/cmd/kosli/listTrails_test.go index 426516fc4..a3a6b00d9 100644 --- a/cmd/kosli/listTrails_test.go +++ b/cmd/kosli/listTrails_test.go @@ -100,9 +100,20 @@ func (suite *ListTrailsCommandTestSuite) TestListTrailsCmd() { }, { name: "10 can list trails in a flow with the provided tag", - cmd: fmt.Sprintf(`list trails --flow %s --flow-tag %s=%s --output json %s`, suite.flowName, suite.flowTagKey, suite.flowTagValue, suite.defaultKosliArguments), + cmd: fmt.Sprintf(`list trails --flow-tag %s=%s --output json %s`, suite.flowTagKey, suite.flowTagValue, suite.defaultKosliArguments), goldenJson: []jsonCheck{{"data", "non-empty"}}, }, + { + name: "11 listing trails with a non-existing flow-tag returns no trails", + cmd: fmt.Sprintf(`list trails --flow-tag non=existing %s`, suite.defaultKosliArguments), + golden: "No trails were found.\n", + }, + { + wantError: true, + name: "12 the value of the flow-tag flag must be a key-value pair", + cmd: fmt.Sprintf(`list trails --flow-tag %s --output json %s`, "invalid-tag", suite.defaultKosliArguments), + golden: "Error: flag '--flow-tag' must be in the format of key=value\nUsage: kosli list trails [flags]\n", + }, } runTestCmd(suite.T(), tests) From 6114274b9a6618a00a46fc4943eea3802f6acd7b Mon Sep 17 00:00:00 2001 From: Simon Castagna Date: Wed, 28 Jan 2026 11:10:58 +0100 Subject: [PATCH 3/3] Update documentation --- cmd/kosli/listTrails.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd/kosli/listTrails.go b/cmd/kosli/listTrails.go index 1c8f93dea..e6dfe22bd 100644 --- a/cmd/kosli/listTrails.go +++ b/cmd/kosli/listTrails.go @@ -14,7 +14,7 @@ import ( const listTrailsShortDesc = `List Trails of an org.` -const listTrailsLongDesc = listTrailsShortDesc + `The list can be filtered by flow and artifact fingerprint. The results are paginated and ordered from latest to oldest.` +const listTrailsLongDesc = listTrailsShortDesc + `The list can be filtered by flow, flow tag and artifact fingerprint. The results are paginated and ordered from latest to oldest.` const listTrailsExample = ` # get a paginated list of trails for a flow: @@ -50,7 +50,13 @@ kosli list trails \ --fingerprint yourArtifactFingerprint \ --api-token yourAPIToken \ --org yourOrgName \ - --output json \ + --output json + + # get a paginated list of trails across all flows tagged with the provided key-value pair: +kosli list trails \ + --flow-tag team=backend \ + --api-token yourAPIToken \ + --org yourOrgName ` type listTrailsOptions struct {