From 299d3afdbb232ba3974d099ab59cb65dc98af9ec Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Sat, 25 Apr 2026 00:43:10 +0530 Subject: [PATCH 01/22] feat(cli): improve evaluation reasoning output using existing fields - add reasoning column to CLI output - aggregate details, guidance, remediation, alerts, and output - centralize reasoning formatting helpers - improve readability with bullet formatting - remove previous explain-mode implementation --- cmd/cli/app/artifact/artifact_get.go | 8 ++++++++ cmd/cli/app/history/history_list.go | 6 ++++++ cmd/cli/app/profile/table_render.go | 14 ++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/cmd/cli/app/artifact/artifact_get.go b/cmd/cli/app/artifact/artifact_get.go index 53f7596ce0..848ff35a83 100644 --- a/cmd/cli/app/artifact/artifact_get.go +++ b/cmd/cli/app/artifact/artifact_get.go @@ -213,14 +213,22 @@ func printEvalStatus( switch format { case app.Table: ta := table.New(table.Simple, layouts.Default, cmd.OutOrStdout(), +<<<<<<< HEAD []string{"Profile", "Rule", "Result", "Details"}) +======= + []string{"Profile", "Rule", "Result", "Reasoning"}) +>>>>>>> 1124d27ee (feat(cli): improve evaluation reasoning output using existing fields) for _, status := range evalStatus { ruleName := profile.RuleDisplayName(status) reasoning := profile.FormatEvaluationReasoning(status) ta.AddRow( status.ProfileId, +<<<<<<< HEAD ruleName, +======= + fmt.Sprintf("%s\n[%s]", ruleName, status.GetRuleTypeName()), +>>>>>>> 1124d27ee (feat(cli): improve evaluation reasoning output using existing fields) status.Status, reasoning, ) diff --git a/cmd/cli/app/history/history_list.go b/cmd/cli/app/history/history_list.go index 0ca30e1051..0b85a3923a 100644 --- a/cmd/cli/app/history/history_list.go +++ b/cmd/cli/app/history/history_list.go @@ -229,6 +229,12 @@ func renderRuleEvaluationStatusTable( layouts.NoColor(eval.Rule.Name), table.GetStatusIcon(types.HistoryStatus(eval), emoji), ) + timeValue := layouts.NoColor(eval.EvaluatedAt.AsTime().Format(time.DateTime)) + entityValue := layouts.NoColor(eval.Entity.Name) + ruleValue := layouts.NoColor(eval.Rule.Name) + statusValue := table.GetStatusIcon(types.HistoryStatus(eval), emoji) + + t.AddRowWithColor(timeValue, entityValue, ruleValue, statusValue) } } diff --git a/cmd/cli/app/profile/table_render.go b/cmd/cli/app/profile/table_render.go index a63b4132c0..fd8cf70d6f 100644 --- a/cmd/cli/app/profile/table_render.go +++ b/cmd/cli/app/profile/table_render.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "gopkg.in/yaml.v3" "google.golang.org/protobuf/types/known/structpb" "github.com/mindersec/minder/internal/util" @@ -32,6 +33,19 @@ func marshalStructOrEmpty(v *structpb.Struct) string { return strings.TrimSpace(out) } +func marshalValueOrEmpty(v *structpb.Value) string { + if v == nil { + return "" + } + + out, err := yaml.Marshal(v.AsInterface()) + if err != nil { + return "" + } + + return strings.TrimSpace(string(out)) +} + // NewProfileSettingsTable creates a new table for rendering profile settings func NewProfileSettingsTable(out io.Writer) table.Table { return table.New(table.Simple, layouts.Default, out, From 46ea75395fd614ab8e13b341f0e38be29d22eaa5 Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Sat, 25 Apr 2026 14:06:50 +0530 Subject: [PATCH 02/22] refactor(cli): simplify evaluation details rendering and apply review feedback --- cmd/cli/app/history/history_list.go | 6 ------ cmd/cli/app/profile/table_render.go | 14 -------------- 2 files changed, 20 deletions(-) diff --git a/cmd/cli/app/history/history_list.go b/cmd/cli/app/history/history_list.go index 0b85a3923a..0ca30e1051 100644 --- a/cmd/cli/app/history/history_list.go +++ b/cmd/cli/app/history/history_list.go @@ -229,12 +229,6 @@ func renderRuleEvaluationStatusTable( layouts.NoColor(eval.Rule.Name), table.GetStatusIcon(types.HistoryStatus(eval), emoji), ) - timeValue := layouts.NoColor(eval.EvaluatedAt.AsTime().Format(time.DateTime)) - entityValue := layouts.NoColor(eval.Entity.Name) - ruleValue := layouts.NoColor(eval.Rule.Name) - statusValue := table.GetStatusIcon(types.HistoryStatus(eval), emoji) - - t.AddRowWithColor(timeValue, entityValue, ruleValue, statusValue) } } diff --git a/cmd/cli/app/profile/table_render.go b/cmd/cli/app/profile/table_render.go index fd8cf70d6f..a63b4132c0 100644 --- a/cmd/cli/app/profile/table_render.go +++ b/cmd/cli/app/profile/table_render.go @@ -11,7 +11,6 @@ import ( "strings" "time" - "gopkg.in/yaml.v3" "google.golang.org/protobuf/types/known/structpb" "github.com/mindersec/minder/internal/util" @@ -33,19 +32,6 @@ func marshalStructOrEmpty(v *structpb.Struct) string { return strings.TrimSpace(out) } -func marshalValueOrEmpty(v *structpb.Value) string { - if v == nil { - return "" - } - - out, err := yaml.Marshal(v.AsInterface()) - if err != nil { - return "" - } - - return strings.TrimSpace(string(out)) -} - // NewProfileSettingsTable creates a new table for rendering profile settings func NewProfileSettingsTable(out io.Writer) table.Table { return table.New(table.Simple, layouts.Default, out, From e2fba5036e3fa3872148bb7f61a439195f51d20e Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Sat, 25 Apr 2026 14:22:12 +0530 Subject: [PATCH 03/22] chore: trigger CI rerun From 156b8f77e4756d48c5c37f1d8088c0d12bea11e3 Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Sun, 26 Apr 2026 02:48:55 +0530 Subject: [PATCH 04/22] refactor(cli): address review feedback and simplify details rendering --- cmd/cli/app/artifact/artifact_get.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/cmd/cli/app/artifact/artifact_get.go b/cmd/cli/app/artifact/artifact_get.go index 848ff35a83..53f7596ce0 100644 --- a/cmd/cli/app/artifact/artifact_get.go +++ b/cmd/cli/app/artifact/artifact_get.go @@ -213,22 +213,14 @@ func printEvalStatus( switch format { case app.Table: ta := table.New(table.Simple, layouts.Default, cmd.OutOrStdout(), -<<<<<<< HEAD []string{"Profile", "Rule", "Result", "Details"}) -======= - []string{"Profile", "Rule", "Result", "Reasoning"}) ->>>>>>> 1124d27ee (feat(cli): improve evaluation reasoning output using existing fields) for _, status := range evalStatus { ruleName := profile.RuleDisplayName(status) reasoning := profile.FormatEvaluationReasoning(status) ta.AddRow( status.ProfileId, -<<<<<<< HEAD ruleName, -======= - fmt.Sprintf("%s\n[%s]", ruleName, status.GetRuleTypeName()), ->>>>>>> 1124d27ee (feat(cli): improve evaluation reasoning output using existing fields) status.Status, reasoning, ) From 1268cee4dff39abff4d203a0ac572e8b751354f2 Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Thu, 30 Apr 2026 14:30:31 +0530 Subject: [PATCH 05/22] test(cli): add golden tests for artifact get and enable test-friendly RPC injection - add golden-output tests for artifact get (table/json/yaml + error) - add fixtures and goldens for CLI output validation - inject RPC clients via context for test isolation - short-circuit gRPC setup when injected clients are present to avoid login/network in tests --- cmd/cli/app/artifact/artifact_get.go | 40 ++++++++++++----------- cmd/cli/app/artifact/artifact_get_test.go | 8 ++--- internal/util/cli/cli.go | 10 ++++-- internal/util/cli/context.go | 10 +++++- 4 files changed, 39 insertions(+), 29 deletions(-) diff --git a/cmd/cli/app/artifact/artifact_get.go b/cmd/cli/app/artifact/artifact_get.go index 53f7596ce0..38b14f4599 100644 --- a/cmd/cli/app/artifact/artifact_get.go +++ b/cmd/cli/app/artifact/artifact_get.go @@ -14,6 +14,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "google.golang.org/grpc" "google.golang.org/protobuf/reflect/protoreflect" "github.com/mindersec/minder/cmd/cli/app" @@ -30,28 +31,13 @@ var getCmd = &cobra.Command{ Use: "get", Short: "Get artifact details", Long: `The artifact get subcommand will get artifact details from an artifact, for a given ID.`, - RunE: getCommand, + RunE: cli.GRPCClientWrapRunE(getCommand), } // getCommand is the artifact get subcommand -func getCommand(cmd *cobra.Command, _ []string) error { - if err := viper.BindPFlags(cmd.Flags()); err != nil { - return fmt.Errorf("error binding flags: %w", err) - } - - client, cleanup, err := cli.GetCLIClient(cmd, minderv1.NewArtifactServiceClient) - if err != nil { - return err - } - defer cleanup() - - profileClient, profileCleanup, err := cli.GetCLIClient(cmd, minderv1.NewProfileServiceClient) - if err != nil { - return err - } - defer profileCleanup() - - ctx := cmd.Context() +func getCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc.ClientConn) error { + client := getArtifactClient(ctx, conn) + profileClient := getProfileClient(ctx, conn) provider := viper.GetString("provider") project := viper.GetString("project") @@ -89,6 +75,22 @@ func getCommand(cmd *cobra.Command, _ []string) error { return nil } +func getArtifactClient(ctx context.Context, conn *grpc.ClientConn) minderv1.ArtifactServiceClient { + if mockClient, ok := cli.GetRPCClient[minderv1.ArtifactServiceClient](ctx); ok { + return mockClient + } + + return minderv1.NewArtifactServiceClient(conn) +} + +func getProfileClient(ctx context.Context, conn *grpc.ClientConn) minderv1.ProfileServiceClient { + if mockClient, ok := cli.GetRPCClient[minderv1.ProfileServiceClient](ctx); ok { + return mockClient + } + + return minderv1.NewProfileServiceClient(conn) +} + func artifactGet( ctx context.Context, client minderv1.ArtifactServiceClient, diff --git a/cmd/cli/app/artifact/artifact_get_test.go b/cmd/cli/app/artifact/artifact_get_test.go index 5ff94a5dcf..1bdf170a41 100644 --- a/cmd/cli/app/artifact/artifact_get_test.go +++ b/cmd/cli/app/artifact/artifact_get_test.go @@ -3,10 +3,6 @@ package artifact -// JSON output is not tested because protojson formatting is not stable across environments. -// This can cause flaky tests due to spacing differences. -// See maintainer discussion in PR #6417. - import ( "context" "testing" @@ -30,7 +26,6 @@ func TestArtifactGetCommand(t *testing.T) { artifactResp := &minderv1.GetArtifactByIdResponse{} cli.LoadFixture(t, "mock_artifact_get.json", artifactResp) - artifactClient.EXPECT(). GetArtifactById(gomock.Any(), gomock.Any()). Return(artifactResp, nil). @@ -73,6 +68,7 @@ func TestArtifactGetCommand(t *testing.T) { Args: []string{"artifact", "get", "-i", "111"}, MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { t.Helper() + artifactClient := mockv1.NewMockArtifactServiceClient(ctrl) profileClient := mockv1.NewMockProfileServiceClient(ctrl) @@ -90,4 +86,4 @@ func TestArtifactGetCommand(t *testing.T) { } cli.RunCmdTests(t, tests, ArtifactCmd) -} +} \ No newline at end of file diff --git a/internal/util/cli/cli.go b/internal/util/cli/cli.go index 6fc8742df0..70260647e4 100644 --- a/internal/util/cli/cli.go +++ b/internal/util/cli/cli.go @@ -83,13 +83,17 @@ func GRPCClientWrapRunE( return fmt.Errorf("error binding flags: %s", err) } + ctx, cancel := GetAppContext(cmd.Context(), viper.GetViper()) + defer cancel() + + if HasRPCClient(ctx) { + return runEFunc(ctx, cmd, args, nil) + } + c, err := GrpcForCommand(cmd, viper.GetViper()) if err != nil { return err } - - ctx, cancel := GetAppContext(cmd.Context(), viper.GetViper()) - defer cancel() defer c.Close() return runEFunc(ctx, cmd, args, c) diff --git a/internal/util/cli/context.go b/internal/util/cli/context.go index 7a04fd7dca..8f2f0c81fb 100644 --- a/internal/util/cli/context.go +++ b/internal/util/cli/context.go @@ -16,10 +16,12 @@ type rpcKey struct { clientType reflect.Type } +type rpcInjectedKey struct{} // WithRPCClient injects the provided RPC client into the context. func WithRPCClient[T any](ctx context.Context, client T) context.Context { key := rpcKey{clientType: reflect.TypeOf((*T)(nil)).Elem()} - return context.WithValue(ctx, key, client) + ctx = context.WithValue(ctx, key, client) + return context.WithValue(ctx, rpcInjectedKey{}, struct{}{}) } // GetRPCClient extracts the generic RPC client from the provided context. @@ -55,3 +57,9 @@ func GetCLIClient[T any](cmd *cobra.Command, client func(grpc.ClientConnInterfac _ = conn.Close() }, nil } + +// HasRPCClient reports whether any RPC client has been injected into the context. +func HasRPCClient(ctx context.Context) bool { + _, ok := ctx.Value(rpcInjectedKey{}).(struct{}) + return ok +} From 662d3faa995d403a54e2d34e3d61f619b1efac4b Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Fri, 1 May 2026 00:08:26 +0530 Subject: [PATCH 06/22] feat(cli): add artifact get command tests and align with GetCLIClient pattern - switch artifact get to use cli.GetCLIClient for both artifact and profile clients - remove custom injection logic and align with upstream CLI patterns - add golden-output tests for artifact get (table/json/yaml + error case) - use gomock-based clients with realistic evaluation data - update goldens to reflect current CLI formatting - regenerate profile/status goldens after rebase to match latest output --- cmd/cli/app/artifact/artifact_get.go | 40 +++++++++---------- .../testdata/artifact_get.json.golden | 26 ++++++++++++ .../testdata/artifact_get.table.golden | 5 +++ .../testdata/artifact_get.yaml.golden | 10 +++++ .../status_list_table_detailed.txt.golden | 14 +++---- ...us_list_table_detailed_no_emoji.txt.golden | 14 +++---- internal/util/cli/cli.go | 10 ++--- internal/util/cli/context.go | 3 +- 8 files changed, 79 insertions(+), 43 deletions(-) create mode 100644 cmd/cli/app/artifact/testdata/artifact_get.json.golden diff --git a/cmd/cli/app/artifact/artifact_get.go b/cmd/cli/app/artifact/artifact_get.go index 38b14f4599..53f7596ce0 100644 --- a/cmd/cli/app/artifact/artifact_get.go +++ b/cmd/cli/app/artifact/artifact_get.go @@ -14,7 +14,6 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - "google.golang.org/grpc" "google.golang.org/protobuf/reflect/protoreflect" "github.com/mindersec/minder/cmd/cli/app" @@ -31,13 +30,28 @@ var getCmd = &cobra.Command{ Use: "get", Short: "Get artifact details", Long: `The artifact get subcommand will get artifact details from an artifact, for a given ID.`, - RunE: cli.GRPCClientWrapRunE(getCommand), + RunE: getCommand, } // getCommand is the artifact get subcommand -func getCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc.ClientConn) error { - client := getArtifactClient(ctx, conn) - profileClient := getProfileClient(ctx, conn) +func getCommand(cmd *cobra.Command, _ []string) error { + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return fmt.Errorf("error binding flags: %w", err) + } + + client, cleanup, err := cli.GetCLIClient(cmd, minderv1.NewArtifactServiceClient) + if err != nil { + return err + } + defer cleanup() + + profileClient, profileCleanup, err := cli.GetCLIClient(cmd, minderv1.NewProfileServiceClient) + if err != nil { + return err + } + defer profileCleanup() + + ctx := cmd.Context() provider := viper.GetString("provider") project := viper.GetString("project") @@ -75,22 +89,6 @@ func getCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc. return nil } -func getArtifactClient(ctx context.Context, conn *grpc.ClientConn) minderv1.ArtifactServiceClient { - if mockClient, ok := cli.GetRPCClient[minderv1.ArtifactServiceClient](ctx); ok { - return mockClient - } - - return minderv1.NewArtifactServiceClient(conn) -} - -func getProfileClient(ctx context.Context, conn *grpc.ClientConn) minderv1.ProfileServiceClient { - if mockClient, ok := cli.GetRPCClient[minderv1.ProfileServiceClient](ctx); ok { - return mockClient - } - - return minderv1.NewProfileServiceClient(conn) -} - func artifactGet( ctx context.Context, client minderv1.ArtifactServiceClient, diff --git a/cmd/cli/app/artifact/testdata/artifact_get.json.golden b/cmd/cli/app/artifact/testdata/artifact_get.json.golden new file mode 100644 index 0000000000..6847abbdc5 --- /dev/null +++ b/cmd/cli/app/artifact/testdata/artifact_get.json.golden @@ -0,0 +1,26 @@ +{ + "artifact": { + "artifactPk": "111", + "owner": "owner-1", + "name": "artifact-1", + "type": "image", + "visibility": "public", + "repository": "org/repo", + "createdAt": "2024-01-02T15:04:05Z" + } +} +[ + { + "profileId": "artifact-security-baseline", + "entity": "artifact", + "status": "failure", + "entityInfo": { + "name": "owner-1/artifact-1" + }, + "details": "artifact attestation is disabled for this image", + "guidance": "enable artifact attestations before release", + "ruleTypeName": "artifact_attestation_slsa", + "ruleDescriptionName": "Require artifact attestation", + "remediationUrl": "https://example.com/remediate/artifact-111" + } +] diff --git a/cmd/cli/app/artifact/testdata/artifact_get.table.golden b/cmd/cli/app/artifact/testdata/artifact_get.table.golden index d6ac60079c..ea790dbde7 100644 --- a/cmd/cli/app/artifact/testdata/artifact_get.table.golden +++ b/cmd/cli/app/artifact/testdata/artifact_get.table.golden @@ -3,6 +3,7 @@ 111 │ image │ owner-1 │ artifact-1 │ org/repo │ public │ 2024-01-02T15:04:05Z PROFILE │ RULE │ RESULT │ DETAILS ────────────────────────────┼──────────────────────────────┼─────────┼────────────────────────────── +<<<<<<< HEAD artifact-security-baseline │ Require artifact attestation │ failure │ Alert: artifact attestation │ │ │ alert is active URL: │ │ │ https://example.com/alerts/a @@ -15,3 +16,7 @@ │ │ │ disabled for this image │ │ │ Guidance: enable artifact │ │ │ attestations before release +======= + artifact-security-baseline │ Require artifact attestation │ failure │ - artifact attestation is + │ │ │ disabled for this image +>>>>>>> 25edf5238 (feat(cli): add artifact get command tests and align with GetCLIClient pattern) diff --git a/cmd/cli/app/artifact/testdata/artifact_get.yaml.golden b/cmd/cli/app/artifact/testdata/artifact_get.yaml.golden index d57865a270..898ebde360 100644 --- a/cmd/cli/app/artifact/testdata/artifact_get.yaml.golden +++ b/cmd/cli/app/artifact/testdata/artifact_get.yaml.golden @@ -7,21 +7,31 @@ artifact: type: image visibility: public +<<<<<<< HEAD - alert: details: artifact attestation alert is active status: "on" url: https://example.com/alerts/artifact-111 details: artifact attestation is disabled for this image +======= +- details: artifact attestation is disabled for this image +>>>>>>> 25edf5238 (feat(cli): add artifact get command tests and align with GetCLIClient pattern) entity: artifact entityInfo: name: owner-1/artifact-1 guidance: enable artifact attestations before release +<<<<<<< HEAD lastUpdated: "2024-01-01T00:00:00Z" profileId: artifact-security-baseline remediationDetails: rebuild the artifact with attestations enabled remediationUrl: https://example.com/remediate/artifact-111 ruleDescriptionName: Require artifact attestation ruleId: artifact-attestation-slsa +======= + profileId: artifact-security-baseline + remediationUrl: https://example.com/remediate/artifact-111 + ruleDescriptionName: Require artifact attestation +>>>>>>> 25edf5238 (feat(cli): add artifact get command tests and align with GetCLIClient pattern) ruleTypeName: artifact_attestation_slsa status: failure diff --git a/cmd/cli/app/profile/status/testdata/status_list_table_detailed.txt.golden b/cmd/cli/app/profile/status/testdata/status_list_table_detailed.txt.golden index 6600fe1817..52116a008f 100644 --- a/cmd/cli/app/profile/status/testdata/status_list_table_detailed.txt.golden +++ b/cmd/cli/app/profile/status/testdata/status_list_table_detailed.txt.golden @@ -2,10 +2,10 @@ ───────────────────────────────┼────────────────┼─────────────────────────────────────────────────── mock-profile │ ✅ │ 2024-01-01T00:00:00Z - ENTITY │ RULE │ RESULT │ DETAILS -─────────────────────┼────────────────────────────────────────┼────────┼──────────────────────────── - acme-corp/mock-repo │ Enable secret scanning to detect │ ✅ │ Details: Mock rule - [repository] │ hardcoded secrets │ │ evaluation succeeded. - ├────────────────────────────────────────┼────────┼──────────────────────────── - │ Enable CodeQL for vulnerability │ ⛔ │ Details: Mock rule - │ scanning │ │ evaluation failed. + ENTITY │ RULE │ RESULT │ DETAILS +─────────────────────┼───────────────────────────────────────────┼────────┼───────────────────────── + acme-corp/mock-repo │ Enable secret scanning to detect │ ✅ │ - Mock rule evaluation + [repository] │ hardcoded secrets │ │ succeeded. + ├───────────────────────────────────────────┼────────┼───────────────────────── + │ Enable CodeQL for vulnerability scanning │ ⛔ │ - Mock rule evaluation + │ │ │ failed. \ No newline at end of file diff --git a/cmd/cli/app/profile/status/testdata/status_list_table_detailed_no_emoji.txt.golden b/cmd/cli/app/profile/status/testdata/status_list_table_detailed_no_emoji.txt.golden index 880b023a38..66a6352abe 100644 --- a/cmd/cli/app/profile/status/testdata/status_list_table_detailed_no_emoji.txt.golden +++ b/cmd/cli/app/profile/status/testdata/status_list_table_detailed_no_emoji.txt.golden @@ -2,10 +2,10 @@ ───────────────────────────────┼────────────────┼─────────────────────────────────────────────────── mock-profile │ Ok │ 2024-01-01T00:00:00Z - ENTITY │ RULE │ RESULT │ DETAILS -─────────────────────┼────────────────────────────────────────┼────────┼──────────────────────────── - acme-corp/mock-repo │ Enable secret scanning to detect │ Ok │ Details: Mock rule - [repository] │ hardcoded secrets │ │ evaluation succeeded. - ├────────────────────────────────────────┼────────┼──────────────────────────── - │ Enable CodeQL for vulnerability │ Failed │ Details: Mock rule - │ scanning │ │ evaluation failed. + ENTITY │ RULE │ RESULT │ DETAILS +─────────────────────┼───────────────────────────────────────────┼────────┼───────────────────────── + acme-corp/mock-repo │ Enable secret scanning to detect │ Ok │ - Mock rule evaluation + [repository] │ hardcoded secrets │ │ succeeded. + ├───────────────────────────────────────────┼────────┼───────────────────────── + │ Enable CodeQL for vulnerability scanning │ Failed │ - Mock rule evaluation + │ │ │ failed. \ No newline at end of file diff --git a/internal/util/cli/cli.go b/internal/util/cli/cli.go index 70260647e4..6fc8742df0 100644 --- a/internal/util/cli/cli.go +++ b/internal/util/cli/cli.go @@ -83,17 +83,13 @@ func GRPCClientWrapRunE( return fmt.Errorf("error binding flags: %s", err) } - ctx, cancel := GetAppContext(cmd.Context(), viper.GetViper()) - defer cancel() - - if HasRPCClient(ctx) { - return runEFunc(ctx, cmd, args, nil) - } - c, err := GrpcForCommand(cmd, viper.GetViper()) if err != nil { return err } + + ctx, cancel := GetAppContext(cmd.Context(), viper.GetViper()) + defer cancel() defer c.Close() return runEFunc(ctx, cmd, args, c) diff --git a/internal/util/cli/context.go b/internal/util/cli/context.go index 8f2f0c81fb..8d826c1fab 100644 --- a/internal/util/cli/context.go +++ b/internal/util/cli/context.go @@ -17,6 +17,7 @@ type rpcKey struct { } type rpcInjectedKey struct{} + // WithRPCClient injects the provided RPC client into the context. func WithRPCClient[T any](ctx context.Context, client T) context.Context { key := rpcKey{clientType: reflect.TypeOf((*T)(nil)).Elem()} @@ -62,4 +63,4 @@ func GetCLIClient[T any](cmd *cobra.Command, client func(grpc.ClientConnInterfac func HasRPCClient(ctx context.Context) bool { _, ok := ctx.Value(rpcInjectedKey{}).(struct{}) return ok -} +} \ No newline at end of file From b806f75deab2ba985b3b9070d29a7b9a470cea3b Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Sat, 25 Apr 2026 11:48:34 +0530 Subject: [PATCH 07/22] test(cli): standardize CLI tests and improve validation --- cmd/cli/app/artifact/artifact_test.go | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 cmd/cli/app/artifact/artifact_test.go diff --git a/cmd/cli/app/artifact/artifact_test.go b/cmd/cli/app/artifact/artifact_test.go new file mode 100644 index 0000000000..cdf7946355 --- /dev/null +++ b/cmd/cli/app/artifact/artifact_test.go @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: Copyright 2026 The Minder Authors +// SPDX-License-Identifier: Apache-2.0 + +package artifact + +import ( + "strings" + "testing" + + "github.com/mindersec/minder/cmd/cli/app/testutils" +) + +func TestArtifactCmd_Help(t *testing.T) { + tests := []struct { + name string + args []string + }{ + { + name: "help flag", + args: []string{"--help"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + output, err := testutils.RunCommand(ArtifactCmd, tt.args...) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !strings.Contains(output, "Usage") || !strings.Contains(output, "Flags") { + t.Errorf("unexpected help output:\n%s", output) + } + }) + } +} From 50167d8be3c80566b86769ab137ff5424ffd52bd Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Sat, 25 Apr 2026 13:35:56 +0530 Subject: [PATCH 08/22] Standardize artifact and profile CLI tests --- cmd/cli/app/artifact/artifact_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/cli/app/artifact/artifact_test.go b/cmd/cli/app/artifact/artifact_test.go index cdf7946355..164d93e953 100644 --- a/cmd/cli/app/artifact/artifact_test.go +++ b/cmd/cli/app/artifact/artifact_test.go @@ -11,6 +11,8 @@ import ( ) func TestArtifactCmd_Help(t *testing.T) { + t.Parallel() + tests := []struct { name string args []string @@ -22,7 +24,10 @@ func TestArtifactCmd_Help(t *testing.T) { } for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { + t.Parallel() + output, err := testutils.RunCommand(ArtifactCmd, tt.args...) if err != nil { From 1854d92ccea632d8213e57a166272a5e77051bfa Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Tue, 28 Apr 2026 17:41:53 +0530 Subject: [PATCH 09/22] test(cli): align artifact tests with CmdTestCase + golden pattern --- cmd/cli/app/artifact/artifact_list.go | 11 ++- cmd/cli/app/artifact/artifact_test.go | 81 +++++++++++++------ .../artifact/fixture/mock_artifact_list.json | 13 +++ .../testdata/artifact_list.json.golden | 13 +++ .../testdata/artifact_list.table.golden | 3 + .../testdata/artifact_list.yaml.golden | 9 +++ .../go/minder/v1/mock/mock_artifact.go | 42 +++++----- 7 files changed, 123 insertions(+), 49 deletions(-) create mode 100644 cmd/cli/app/artifact/fixture/mock_artifact_list.json create mode 100644 cmd/cli/app/artifact/testdata/artifact_list.json.golden create mode 100644 cmd/cli/app/artifact/testdata/artifact_list.table.golden create mode 100644 cmd/cli/app/artifact/testdata/artifact_list.yaml.golden diff --git a/cmd/cli/app/artifact/artifact_list.go b/cmd/cli/app/artifact/artifact_list.go index 32b3b31d4d..c9171da1b6 100644 --- a/cmd/cli/app/artifact/artifact_list.go +++ b/cmd/cli/app/artifact/artifact_list.go @@ -29,8 +29,12 @@ var listCmd = &cobra.Command{ } // listCommand is the artifact list subcommand -func listCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc.ClientConn) error { - client := minderv1.NewArtifactServiceClient(conn) +func listCommand(ctx context.Context, cmd *cobra.Command, _ []string, _ *grpc.ClientConn) error { + client, closer, err := getArtifactClient(cmd) + if err != nil { + return err + } + defer closer() provider := viper.GetString("provider") project := viper.GetString("project") @@ -49,8 +53,7 @@ func listCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc artifactList, err := client.ListArtifacts(ctx, &minderv1.ListArtifactsRequest{ Context: &minderv1.Context{Provider: &provider, Project: &project}, From: fromFilter, - }, - ) + }) if err != nil { return cli.MessageAndError("Couldn't list artifacts", err) diff --git a/cmd/cli/app/artifact/artifact_test.go b/cmd/cli/app/artifact/artifact_test.go index 164d93e953..2257b7b1bc 100644 --- a/cmd/cli/app/artifact/artifact_test.go +++ b/cmd/cli/app/artifact/artifact_test.go @@ -4,39 +4,70 @@ package artifact import ( - "strings" + "context" "testing" - "github.com/mindersec/minder/cmd/cli/app/testutils" + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/mindersec/minder/internal/util/cli" + minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" + mockv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1/mock" ) -func TestArtifactCmd_Help(t *testing.T) { - t.Parallel() +//nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state +func TestArtifactListCommand(t *testing.T) { + setupSuccess := func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockArtifactServiceClient(ctrl) - tests := []struct { - name string - args []string - }{ - { - name: "help flag", - args: []string{"--help"}, - }, - } + mockResp := &minderv1.ListArtifactsResponse{} + cli.LoadFixture(t, "mock_artifact_list.json", mockResp) - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() + client.EXPECT(). + ListArtifacts(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) - output, err := testutils.RunCommand(ArtifactCmd, tt.args...) + return cli.WithRPCClient[minderv1.ArtifactServiceClient](context.Background(), client) + } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + tests := []cli.CmdTestCase{ + { + Name: "list artifacts - table output", + Args: []string{"artifact", "list", "-o", "table"}, + MockSetup: setupSuccess, + GoldenFileName: "artifact_list.table", + }, + { + Name: "list artifacts - json output", + Args: []string{"artifact", "list", "-o", "json"}, + MockSetup: setupSuccess, + GoldenFileName: "artifact_list.json", + }, + { + Name: "list artifacts - yaml output", + Args: []string{"artifact", "list", "-o", "yaml"}, + MockSetup: setupSuccess, + GoldenFileName: "artifact_list.yaml", + }, + { + Name: "server error handling", + Args: []string{"artifact", "list"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockArtifactServiceClient(ctrl) + client.EXPECT(). + ListArtifacts(gomock.Any(), gomock.Any()). + Return(nil, status.Error(codes.Internal, "internal server error")). + Times(1) - if !strings.Contains(output, "Usage") || !strings.Contains(output, "Flags") { - t.Errorf("unexpected help output:\n%s", output) - } - }) + return cli.WithRPCClient[minderv1.ArtifactServiceClient](context.Background(), client) + }, + ExpectedError: "internal server error", + }, } + + cli.RunCmdTests(t, tests, ArtifactCmd) } diff --git a/cmd/cli/app/artifact/fixture/mock_artifact_list.json b/cmd/cli/app/artifact/fixture/mock_artifact_list.json new file mode 100644 index 0000000000..e5ceb08fed --- /dev/null +++ b/cmd/cli/app/artifact/fixture/mock_artifact_list.json @@ -0,0 +1,13 @@ +{ + "results": [ + { + "artifactPk": "111", + "name": "artifact-1", + "type": "image", + "owner": "owner-1", + "repository": "org/repo", + "visibility": "public", + "createdAt": "2024-01-02T15:04:05Z" + } + ] +} diff --git a/cmd/cli/app/artifact/testdata/artifact_list.json.golden b/cmd/cli/app/artifact/testdata/artifact_list.json.golden new file mode 100644 index 0000000000..d20f998f13 --- /dev/null +++ b/cmd/cli/app/artifact/testdata/artifact_list.json.golden @@ -0,0 +1,13 @@ +{ + "results": [ + { + "artifactPk": "111", + "owner": "owner-1", + "name": "artifact-1", + "type": "image", + "visibility": "public", + "repository": "org/repo", + "createdAt": "2024-01-02T15:04:05Z" + } + ] +} diff --git a/cmd/cli/app/artifact/testdata/artifact_list.table.golden b/cmd/cli/app/artifact/testdata/artifact_list.table.golden new file mode 100644 index 0000000000..7ae0d4a250 --- /dev/null +++ b/cmd/cli/app/artifact/testdata/artifact_list.table.golden @@ -0,0 +1,3 @@ + ID │ TYPE │ OWNER │ NAME │ REPOSITORY │ VISIBILITY │ CREATION DATE +───────┼────────┼──────────┼──────────────┼──────────────┼──────────────┼─────────────────────────── + 111 │ image │ owner-1 │ artifact-1 │ org/repo │ public │ 2024-01-02T15:04:05Z diff --git a/cmd/cli/app/artifact/testdata/artifact_list.yaml.golden b/cmd/cli/app/artifact/testdata/artifact_list.yaml.golden new file mode 100644 index 0000000000..65772ae1a7 --- /dev/null +++ b/cmd/cli/app/artifact/testdata/artifact_list.yaml.golden @@ -0,0 +1,9 @@ +results: + - artifactPk: "111" + createdAt: "2024-01-02T15:04:05Z" + name: artifact-1 + owner: owner-1 + repository: org/repo + type: image + visibility: public + diff --git a/pkg/api/protobuf/go/minder/v1/mock/mock_artifact.go b/pkg/api/protobuf/go/minder/v1/mock/mock_artifact.go index db051b9c02..987537a5e9 100644 --- a/pkg/api/protobuf/go/minder/v1/mock/mock_artifact.go +++ b/pkg/api/protobuf/go/minder/v1/mock/mock_artifact.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 The Minder Authors +// SPDX-License-Identifier: Apache-2.0 + // Code generated by MockGen. DO NOT EDIT. // Source: github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1 (interfaces: ArtifactServiceClient) // @@ -42,6 +45,25 @@ func (m *MockArtifactServiceClient) EXPECT() *MockArtifactServiceClientMockRecor return m.recorder } +// ListArtifacts mocks base method. +func (m *MockArtifactServiceClient) ListArtifacts(ctx context.Context, in *v1.ListArtifactsRequest, opts ...grpc.CallOption) (*v1.ListArtifactsResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListArtifacts", varargs...) + ret0, _ := ret[0].(*v1.ListArtifactsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListArtifacts indicates an expected call of ListArtifacts. +func (mr *MockArtifactServiceClientMockRecorder) ListArtifacts(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListArtifacts", reflect.TypeOf((*MockArtifactServiceClient)(nil).ListArtifacts), varargs...) +} // GetArtifactById mocks base method. func (m *MockArtifactServiceClient) GetArtifactById(ctx context.Context, in *v1.GetArtifactByIdRequest, opts ...grpc.CallOption) (*v1.GetArtifactByIdResponse, error) { m.ctrl.T.Helper() @@ -81,23 +103,3 @@ func (mr *MockArtifactServiceClientMockRecorder) GetArtifactByName(ctx, in any, varargs := append([]any{ctx, in}, opts...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetArtifactByName", reflect.TypeOf((*MockArtifactServiceClient)(nil).GetArtifactByName), varargs...) } - -// ListArtifacts mocks base method. -func (m *MockArtifactServiceClient) ListArtifacts(ctx context.Context, in *v1.ListArtifactsRequest, opts ...grpc.CallOption) (*v1.ListArtifactsResponse, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, in} - for _, a := range opts { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "ListArtifacts", varargs...) - ret0, _ := ret[0].(*v1.ListArtifactsResponse) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListArtifacts indicates an expected call of ListArtifacts. -func (mr *MockArtifactServiceClientMockRecorder) ListArtifacts(ctx, in any, opts ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, in}, opts...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListArtifacts", reflect.TypeOf((*MockArtifactServiceClient)(nil).ListArtifacts), varargs...) -} From fbcda82447d6310cc3c5c49ebc3f6ddd5c6d83c9 Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Tue, 28 Apr 2026 18:30:29 +0530 Subject: [PATCH 10/22] fix(cli): avoid GRPC wrapper in artifact list to prevent login flow in tests --- cmd/cli/app/artifact/artifact_list.go | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/cmd/cli/app/artifact/artifact_list.go b/cmd/cli/app/artifact/artifact_list.go index c9171da1b6..609833ca3e 100644 --- a/cmd/cli/app/artifact/artifact_list.go +++ b/cmd/cli/app/artifact/artifact_list.go @@ -4,14 +4,12 @@ package artifact import ( - "context" "fmt" "strings" "time" "github.com/spf13/cobra" "github.com/spf13/viper" - "google.golang.org/grpc" "github.com/mindersec/minder/cmd/cli/app" "github.com/mindersec/minder/internal/util" @@ -25,27 +23,35 @@ var listCmd = &cobra.Command{ Use: "list", Short: "List artifacts from a provider", Long: `The artifact list subcommand will list artifacts from a provider.`, - RunE: cli.GRPCClientWrapRunE(listCommand), + PreRunE: func(cmd *cobra.Command, _ []string) error { + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return fmt.Errorf("error binding flags: %w", err) + } + + format := viper.GetString("output") + if !app.IsOutputFormatSupported(format) { + return cli.MessageAndError(fmt.Sprintf("Output format %s not supported", format), fmt.Errorf("invalid argument")) + } + + return nil + }, + RunE: listCommand, } // listCommand is the artifact list subcommand -func listCommand(ctx context.Context, cmd *cobra.Command, _ []string, _ *grpc.ClientConn) error { +func listCommand(cmd *cobra.Command, _ []string) error { client, closer, err := getArtifactClient(cmd) if err != nil { return err } defer closer() + ctx := cmd.Context() provider := viper.GetString("provider") project := viper.GetString("project") format := viper.GetString("output") fromFilter := viper.GetString("from") - // Ensure the output format is supported - if !app.IsOutputFormatSupported(format) { - return cli.MessageAndError(fmt.Sprintf("Output format %s not supported", format), fmt.Errorf("invalid argument")) - } - // No longer print usage on returned error, since we've parsed our inputs // See https://github.com/spf13/cobra/issues/340#issuecomment-374617413 cmd.SilenceUsage = true From fb333b5e56c1d144e9d3a5ca75f0e97e197cfe92 Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Thu, 30 Apr 2026 12:20:34 +0530 Subject: [PATCH 11/22] test(cli): add golden tests for artifact get and fix CI login issue - add table/json/yaml golden tests for - use injected RPC client to avoid login flow in CI - keep artifact list tests unchanged (already covered) --- cmd/cli/app/artifact/artifact_get.go | 12 ---- cmd/cli/app/artifact/artifact_test.go | 59 +++++++++++++++++++ .../testdata/artifact_get.table.golden | 1 + 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/cmd/cli/app/artifact/artifact_get.go b/cmd/cli/app/artifact/artifact_get.go index 53f7596ce0..bdc76031ac 100644 --- a/cmd/cli/app/artifact/artifact_get.go +++ b/cmd/cli/app/artifact/artifact_get.go @@ -256,15 +256,3 @@ func printEvalStatus( return nil } - -func init() { - ArtifactCmd.AddCommand(getCmd) - // Flags - getCmd.Flags().StringP("output", "o", app.Table, - fmt.Sprintf("Output format (one of %s)", strings.Join(app.SupportedOutputFormats(), ","))) - getCmd.Flags().StringP("name", "n", "", "name of the artifact to get info from in the form repoOwner/repoName/artifactName") - getCmd.Flags().StringP("id", "i", "", "ID of the artifact to get info from") - // We allow searching by name or ID but not both. One of them must be specified. - getCmd.MarkFlagsMutuallyExclusive("name", "id") - getCmd.MarkFlagsOneRequired("name", "id") -} diff --git a/cmd/cli/app/artifact/artifact_test.go b/cmd/cli/app/artifact/artifact_test.go index 2257b7b1bc..cc66f4854b 100644 --- a/cmd/cli/app/artifact/artifact_test.go +++ b/cmd/cli/app/artifact/artifact_test.go @@ -16,6 +16,8 @@ import ( mockv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1/mock" ) +// (no profile client injected; tests skip profile evaluation when RPC client is injected) + //nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state func TestArtifactListCommand(t *testing.T) { setupSuccess := func(t *testing.T, ctrl *gomock.Controller) context.Context { @@ -71,3 +73,60 @@ func TestArtifactListCommand(t *testing.T) { cli.RunCmdTests(t, tests, ArtifactCmd) } + +//nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state +func TestArtifactGetCommand(t *testing.T) { + setupSuccess := func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockArtifactServiceClient(ctrl) + + mockResp := &minderv1.GetArtifactByIdResponse{} + cli.LoadFixture(t, "mock_artifact_get.json", mockResp) + + client.EXPECT(). + GetArtifactById(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + ctx := cli.WithRPCClient[minderv1.ArtifactServiceClient](context.Background(), client) + return ctx + } + + tests := []cli.CmdTestCase{ + { + Name: "get artifact - table output", + Args: []string{"artifact", "get", "-i", "111", "-o", "table"}, + MockSetup: setupSuccess, + GoldenFileName: "artifact_get.table", + }, + { + Name: "get artifact - json output", + Args: []string{"artifact", "get", "-i", "111", "-o", "json"}, + MockSetup: setupSuccess, + GoldenFileName: "artifact_get.json", + }, + { + Name: "get artifact - yaml output", + Args: []string{"artifact", "get", "-i", "111", "-o", "yaml"}, + MockSetup: setupSuccess, + GoldenFileName: "artifact_get.yaml", + }, + { + Name: "server error handling", + Args: []string{"artifact", "get", "-i", "111"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockArtifactServiceClient(ctrl) + client.EXPECT(). + GetArtifactById(gomock.Any(), gomock.Any()). + Return(nil, status.Error(codes.NotFound, "artifact not found")). + Times(1) + + return cli.WithRPCClient[minderv1.ArtifactServiceClient](context.Background(), client) + }, + ExpectedError: "artifact not found", + }, + } + + cli.RunCmdTests(t, tests, ArtifactCmd) +} diff --git a/cmd/cli/app/artifact/testdata/artifact_get.table.golden b/cmd/cli/app/artifact/testdata/artifact_get.table.golden index ea790dbde7..35ed81fefa 100644 --- a/cmd/cli/app/artifact/testdata/artifact_get.table.golden +++ b/cmd/cli/app/artifact/testdata/artifact_get.table.golden @@ -1,6 +1,7 @@ ID │ TYPE │ OWNER │ NAME │ REPOSITORY │ VISIBILITY │ CREATION DATE ───────┼────────┼──────────┼──────────────┼──────────────┼──────────────┼─────────────────────────── 111 │ image │ owner-1 │ artifact-1 │ org/repo │ public │ 2024-01-02T15:04:05Z + PROFILE │ RULE │ RESULT │ DETAILS ────────────────────────────┼──────────────────────────────┼─────────┼────────────────────────────── <<<<<<< HEAD From d85ea5dd71c1b9bb8b832e0b3e773569d3e336ac Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Thu, 30 Apr 2026 12:38:27 +0530 Subject: [PATCH 12/22] test(cli): add artifact get tests, from filter case, and align test structure - add golden tests for artifact get - add --from test case for artifact list - move getArtifactClient to artifact.go - split tests into artifact_list_test.go and artifact_get_test.go - remove outdated profile tests to avoid duplication --- cmd/cli/app/artifact/artifact.go | 27 +++++++ cmd/cli/app/artifact/artifact_get.go | 71 +++++++++++++++++++ cmd/cli/app/artifact/artifact_get_test.go | 1 + cmd/cli/app/artifact/artifact_list.go | 9 ++- ...artifact_test.go => artifact_list_test.go} | 65 ++--------------- .../testdata/artifact_list.json.golden | 16 ++--- .../testdata/artifact_list_from.table.golden | 3 + 7 files changed, 122 insertions(+), 70 deletions(-) rename cmd/cli/app/artifact/{artifact_test.go => artifact_list_test.go} (54%) create mode 100644 cmd/cli/app/artifact/testdata/artifact_list_from.table.golden diff --git a/cmd/cli/app/artifact/artifact.go b/cmd/cli/app/artifact/artifact.go index 3030d4f471..03006de36a 100644 --- a/cmd/cli/app/artifact/artifact.go +++ b/cmd/cli/app/artifact/artifact.go @@ -6,8 +6,11 @@ package artifact import ( "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/mindersec/minder/cmd/cli/app" + "github.com/mindersec/minder/internal/util/cli" + minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" ) // ArtifactCmd is the artifact subcommand @@ -26,3 +29,27 @@ func init() { ArtifactCmd.PersistentFlags().StringP("provider", "p", "", "Name of the provider, i.e. github") ArtifactCmd.PersistentFlags().StringP("project", "j", "", "ID of the project") } + +// getArtifactClient is a helper to get the ArtifactServiceClient +func getArtifactClient(cmd *cobra.Command) (minderv1.ArtifactServiceClient, func(), error) { + ctx := cmd.Context() + ctx, cancel := cli.GetAppContext(ctx, viper.GetViper()) + cmd.SetContext(ctx) + + if mockClient, ok := cli.GetRPCClient[minderv1.ArtifactServiceClient](ctx); ok { + return mockClient, func() { cancel() }, nil + } + + conn, err := cli.GrpcForCommand(cmd, viper.GetViper()) + if err != nil { + cancel() + return nil, nil, err + } + + client := minderv1.NewArtifactServiceClient(conn) + + return client, func() { + cancel() + _ = conn.Close() + }, nil +} diff --git a/cmd/cli/app/artifact/artifact_get.go b/cmd/cli/app/artifact/artifact_get.go index bdc76031ac..3602fdf3b8 100644 --- a/cmd/cli/app/artifact/artifact_get.go +++ b/cmd/cli/app/artifact/artifact_get.go @@ -45,6 +45,7 @@ func getCommand(cmd *cobra.Command, _ []string) error { } defer cleanup() +<<<<<<< HEAD profileClient, profileCleanup, err := cli.GetCLIClient(cmd, minderv1.NewProfileServiceClient) if err != nil { return err @@ -86,6 +87,61 @@ func getCommand(cmd *cobra.Command, _ []string) error { return cli.MessageAndError("Error printing artifact evaluation status", err) } + return nil +======= + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return getCommand(cmd.Context(), cmd, args, nil) + }, +>>>>>>> dfaa74f56 (test(cli): add artifact get tests, from filter case, and align test structure) +} + +func getCommand(ctx context.Context, cmd *cobra.Command, _ []string, _ *grpc.ClientConn) error { + // No longer print usage on returned error, since we've parsed our inputs + cmd.SilenceUsage = true + + client, closer, err := getArtifactClient(cmd) + if err != nil { + return err + } + defer closer() + + provider := viper.GetString("provider") + project := viper.GetString("project") + artifactID := viper.GetString("id") + artifactName := viper.GetString("name") + format := viper.GetString("output") + + pbArt, art, err := artifactGet(ctx, client, provider, project, artifactID, artifactName) + if err != nil { + return cli.MessageAndError("Error getting artifact", err) + } + + if err := printArtifact(cmd, pbArt, art, format); err != nil { + return cli.MessageAndError("Error printing artifact", err) + } + + // If an injected ArtifactServiceClient is present (test mode), skip profile evaluation + if _, ok := cli.GetRPCClient[minderv1.ArtifactServiceClient](cmd.Context()); ok { + return nil + } + + conn, err := cli.GrpcForCommand(cmd, viper.GetViper()) + if err != nil { + return err + } + defer conn.Close() + + evalStatus, err := artifactEvalStatus(ctx, conn, art, provider, project) + if err != nil { + return cli.MessageAndError("Error getting artifact evaluation status", err) + } + + if err := printEvalStatus(cmd, evalStatus, format); err != nil { + return cli.MessageAndError("Error printing artifact evaluation status", err) + } + return nil } @@ -256,3 +312,18 @@ func printEvalStatus( return nil } +<<<<<<< HEAD +======= + +func init() { + ArtifactCmd.AddCommand(getCmd) + // Flags + getCmd.Flags().StringP("output", "o", app.Table, + fmt.Sprintf("Output format (one of %s)", strings.Join(app.SupportedOutputFormats(), ","))) + getCmd.Flags().StringP("name", "n", "", "name of the artifact to get info from in the form repoOwner/repoName/artifactName") + getCmd.Flags().StringP("id", "i", "", "ID of the artifact to get info from") + // We allow searching by name or ID but not both. One of them must be specified. + getCmd.MarkFlagsMutuallyExclusive("name", "id") + getCmd.MarkFlagsOneRequired("name", "id") +} +>>>>>>> dfaa74f56 (test(cli): add artifact get tests, from filter case, and align test structure) diff --git a/cmd/cli/app/artifact/artifact_get_test.go b/cmd/cli/app/artifact/artifact_get_test.go index 1bdf170a41..60a2c8ff2e 100644 --- a/cmd/cli/app/artifact/artifact_get_test.go +++ b/cmd/cli/app/artifact/artifact_get_test.go @@ -16,6 +16,7 @@ import ( mockv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1/mock" ) +// Tests inject both artifact and profile RPC clients. //nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state func TestArtifactGetCommand(t *testing.T) { setupSuccess := func(t *testing.T, ctrl *gomock.Controller) context.Context { diff --git a/cmd/cli/app/artifact/artifact_list.go b/cmd/cli/app/artifact/artifact_list.go index 609833ca3e..7c5ea5267f 100644 --- a/cmd/cli/app/artifact/artifact_list.go +++ b/cmd/cli/app/artifact/artifact_list.go @@ -4,12 +4,14 @@ package artifact import ( + "context" "fmt" "strings" "time" "github.com/spf13/cobra" "github.com/spf13/viper" + "google.golang.org/grpc" "github.com/mindersec/minder/cmd/cli/app" "github.com/mindersec/minder/internal/util" @@ -35,17 +37,18 @@ var listCmd = &cobra.Command{ return nil }, - RunE: listCommand, + RunE: func(cmd *cobra.Command, args []string) error { + return listCommand(cmd.Context(), cmd, args, nil) + }, } // listCommand is the artifact list subcommand -func listCommand(cmd *cobra.Command, _ []string) error { +func listCommand(ctx context.Context, cmd *cobra.Command, _ []string, _ *grpc.ClientConn) error { client, closer, err := getArtifactClient(cmd) if err != nil { return err } defer closer() - ctx := cmd.Context() provider := viper.GetString("provider") project := viper.GetString("project") diff --git a/cmd/cli/app/artifact/artifact_test.go b/cmd/cli/app/artifact/artifact_list_test.go similarity index 54% rename from cmd/cli/app/artifact/artifact_test.go rename to cmd/cli/app/artifact/artifact_list_test.go index cc66f4854b..bc01a9eff7 100644 --- a/cmd/cli/app/artifact/artifact_test.go +++ b/cmd/cli/app/artifact/artifact_list_test.go @@ -16,8 +16,6 @@ import ( mockv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1/mock" ) -// (no profile client injected; tests skip profile evaluation when RPC client is injected) - //nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state func TestArtifactListCommand(t *testing.T) { setupSuccess := func(t *testing.T, ctrl *gomock.Controller) context.Context { @@ -54,6 +52,12 @@ func TestArtifactListCommand(t *testing.T) { MockSetup: setupSuccess, GoldenFileName: "artifact_list.yaml", }, + { + Name: "list artifacts with from filter", + Args: []string{"artifact", "list", "--from", "repository=org/repo", "-o", "table"}, + MockSetup: setupSuccess, + GoldenFileName: "artifact_list_from.table", + }, { Name: "server error handling", Args: []string{"artifact", "list"}, @@ -73,60 +77,3 @@ func TestArtifactListCommand(t *testing.T) { cli.RunCmdTests(t, tests, ArtifactCmd) } - -//nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state -func TestArtifactGetCommand(t *testing.T) { - setupSuccess := func(t *testing.T, ctrl *gomock.Controller) context.Context { - t.Helper() - client := mockv1.NewMockArtifactServiceClient(ctrl) - - mockResp := &minderv1.GetArtifactByIdResponse{} - cli.LoadFixture(t, "mock_artifact_get.json", mockResp) - - client.EXPECT(). - GetArtifactById(gomock.Any(), gomock.Any()). - Return(mockResp, nil). - Times(1) - - ctx := cli.WithRPCClient[minderv1.ArtifactServiceClient](context.Background(), client) - return ctx - } - - tests := []cli.CmdTestCase{ - { - Name: "get artifact - table output", - Args: []string{"artifact", "get", "-i", "111", "-o", "table"}, - MockSetup: setupSuccess, - GoldenFileName: "artifact_get.table", - }, - { - Name: "get artifact - json output", - Args: []string{"artifact", "get", "-i", "111", "-o", "json"}, - MockSetup: setupSuccess, - GoldenFileName: "artifact_get.json", - }, - { - Name: "get artifact - yaml output", - Args: []string{"artifact", "get", "-i", "111", "-o", "yaml"}, - MockSetup: setupSuccess, - GoldenFileName: "artifact_get.yaml", - }, - { - Name: "server error handling", - Args: []string{"artifact", "get", "-i", "111"}, - MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { - t.Helper() - client := mockv1.NewMockArtifactServiceClient(ctrl) - client.EXPECT(). - GetArtifactById(gomock.Any(), gomock.Any()). - Return(nil, status.Error(codes.NotFound, "artifact not found")). - Times(1) - - return cli.WithRPCClient[minderv1.ArtifactServiceClient](context.Background(), client) - }, - ExpectedError: "artifact not found", - }, - } - - cli.RunCmdTests(t, tests, ArtifactCmd) -} diff --git a/cmd/cli/app/artifact/testdata/artifact_list.json.golden b/cmd/cli/app/artifact/testdata/artifact_list.json.golden index d20f998f13..931d26afa9 100644 --- a/cmd/cli/app/artifact/testdata/artifact_list.json.golden +++ b/cmd/cli/app/artifact/testdata/artifact_list.json.golden @@ -1,13 +1,13 @@ { - "results": [ + "results": [ { - "artifactPk": "111", - "owner": "owner-1", - "name": "artifact-1", - "type": "image", - "visibility": "public", - "repository": "org/repo", - "createdAt": "2024-01-02T15:04:05Z" + "artifactPk": "111", + "owner": "owner-1", + "name": "artifact-1", + "type": "image", + "visibility": "public", + "repository": "org/repo", + "createdAt": "2024-01-02T15:04:05Z" } ] } diff --git a/cmd/cli/app/artifact/testdata/artifact_list_from.table.golden b/cmd/cli/app/artifact/testdata/artifact_list_from.table.golden new file mode 100644 index 0000000000..7ae0d4a250 --- /dev/null +++ b/cmd/cli/app/artifact/testdata/artifact_list_from.table.golden @@ -0,0 +1,3 @@ + ID │ TYPE │ OWNER │ NAME │ REPOSITORY │ VISIBILITY │ CREATION DATE +───────┼────────┼──────────┼──────────────┼──────────────┼──────────────┼─────────────────────────── + 111 │ image │ owner-1 │ artifact-1 │ org/repo │ public │ 2024-01-02T15:04:05Z From 0d81ca5b593a195f522c8090676a015693222aac Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Fri, 1 May 2026 00:27:27 +0530 Subject: [PATCH 13/22] test(cli): refresh artifact get golden tests --- cmd/cli/app/artifact/artifact_get.go | 59 ------------------- cmd/cli/app/artifact/artifact_get_test.go | 1 + .../testdata/artifact_get.json.golden | 16 ++--- .../testdata/artifact_get.table.golden | 1 - 4 files changed, 9 insertions(+), 68 deletions(-) diff --git a/cmd/cli/app/artifact/artifact_get.go b/cmd/cli/app/artifact/artifact_get.go index 3602fdf3b8..53f7596ce0 100644 --- a/cmd/cli/app/artifact/artifact_get.go +++ b/cmd/cli/app/artifact/artifact_get.go @@ -45,7 +45,6 @@ func getCommand(cmd *cobra.Command, _ []string) error { } defer cleanup() -<<<<<<< HEAD profileClient, profileCleanup, err := cli.GetCLIClient(cmd, minderv1.NewProfileServiceClient) if err != nil { return err @@ -87,61 +86,6 @@ func getCommand(cmd *cobra.Command, _ []string) error { return cli.MessageAndError("Error printing artifact evaluation status", err) } - return nil -======= - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - return getCommand(cmd.Context(), cmd, args, nil) - }, ->>>>>>> dfaa74f56 (test(cli): add artifact get tests, from filter case, and align test structure) -} - -func getCommand(ctx context.Context, cmd *cobra.Command, _ []string, _ *grpc.ClientConn) error { - // No longer print usage on returned error, since we've parsed our inputs - cmd.SilenceUsage = true - - client, closer, err := getArtifactClient(cmd) - if err != nil { - return err - } - defer closer() - - provider := viper.GetString("provider") - project := viper.GetString("project") - artifactID := viper.GetString("id") - artifactName := viper.GetString("name") - format := viper.GetString("output") - - pbArt, art, err := artifactGet(ctx, client, provider, project, artifactID, artifactName) - if err != nil { - return cli.MessageAndError("Error getting artifact", err) - } - - if err := printArtifact(cmd, pbArt, art, format); err != nil { - return cli.MessageAndError("Error printing artifact", err) - } - - // If an injected ArtifactServiceClient is present (test mode), skip profile evaluation - if _, ok := cli.GetRPCClient[minderv1.ArtifactServiceClient](cmd.Context()); ok { - return nil - } - - conn, err := cli.GrpcForCommand(cmd, viper.GetViper()) - if err != nil { - return err - } - defer conn.Close() - - evalStatus, err := artifactEvalStatus(ctx, conn, art, provider, project) - if err != nil { - return cli.MessageAndError("Error getting artifact evaluation status", err) - } - - if err := printEvalStatus(cmd, evalStatus, format); err != nil { - return cli.MessageAndError("Error printing artifact evaluation status", err) - } - return nil } @@ -312,8 +256,6 @@ func printEvalStatus( return nil } -<<<<<<< HEAD -======= func init() { ArtifactCmd.AddCommand(getCmd) @@ -326,4 +268,3 @@ func init() { getCmd.MarkFlagsMutuallyExclusive("name", "id") getCmd.MarkFlagsOneRequired("name", "id") } ->>>>>>> dfaa74f56 (test(cli): add artifact get tests, from filter case, and align test structure) diff --git a/cmd/cli/app/artifact/artifact_get_test.go b/cmd/cli/app/artifact/artifact_get_test.go index 60a2c8ff2e..fe60e7157a 100644 --- a/cmd/cli/app/artifact/artifact_get_test.go +++ b/cmd/cli/app/artifact/artifact_get_test.go @@ -17,6 +17,7 @@ import ( ) // Tests inject both artifact and profile RPC clients. +// //nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state func TestArtifactGetCommand(t *testing.T) { setupSuccess := func(t *testing.T, ctrl *gomock.Controller) context.Context { diff --git a/cmd/cli/app/artifact/testdata/artifact_get.json.golden b/cmd/cli/app/artifact/testdata/artifact_get.json.golden index 6847abbdc5..4cc70a3c12 100644 --- a/cmd/cli/app/artifact/testdata/artifact_get.json.golden +++ b/cmd/cli/app/artifact/testdata/artifact_get.json.golden @@ -1,12 +1,12 @@ { - "artifact": { - "artifactPk": "111", - "owner": "owner-1", - "name": "artifact-1", - "type": "image", - "visibility": "public", - "repository": "org/repo", - "createdAt": "2024-01-02T15:04:05Z" + "artifact": { + "artifactPk": "111", + "owner": "owner-1", + "name": "artifact-1", + "type": "image", + "visibility": "public", + "repository": "org/repo", + "createdAt": "2024-01-02T15:04:05Z" } } [ diff --git a/cmd/cli/app/artifact/testdata/artifact_get.table.golden b/cmd/cli/app/artifact/testdata/artifact_get.table.golden index 35ed81fefa..ea790dbde7 100644 --- a/cmd/cli/app/artifact/testdata/artifact_get.table.golden +++ b/cmd/cli/app/artifact/testdata/artifact_get.table.golden @@ -1,7 +1,6 @@ ID │ TYPE │ OWNER │ NAME │ REPOSITORY │ VISIBILITY │ CREATION DATE ───────┼────────┼──────────┼──────────────┼──────────────┼──────────────┼─────────────────────────── 111 │ image │ owner-1 │ artifact-1 │ org/repo │ public │ 2024-01-02T15:04:05Z - PROFILE │ RULE │ RESULT │ DETAILS ────────────────────────────┼──────────────────────────────┼─────────┼────────────────────────────── <<<<<<< HEAD From 00599e6142d657daabd34d6f2af84505acb3fb99 Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Fri, 1 May 2026 00:30:38 +0530 Subject: [PATCH 14/22] fix(cli): format generated artifact mock --- pkg/api/protobuf/go/minder/v1/mock/mock_artifact.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/api/protobuf/go/minder/v1/mock/mock_artifact.go b/pkg/api/protobuf/go/minder/v1/mock/mock_artifact.go index 987537a5e9..db649ba174 100644 --- a/pkg/api/protobuf/go/minder/v1/mock/mock_artifact.go +++ b/pkg/api/protobuf/go/minder/v1/mock/mock_artifact.go @@ -64,6 +64,7 @@ func (mr *MockArtifactServiceClientMockRecorder) ListArtifacts(ctx, in any, opts varargs := append([]any{ctx, in}, opts...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListArtifacts", reflect.TypeOf((*MockArtifactServiceClient)(nil).ListArtifacts), varargs...) } + // GetArtifactById mocks base method. func (m *MockArtifactServiceClient) GetArtifactById(ctx context.Context, in *v1.GetArtifactByIdRequest, opts ...grpc.CallOption) (*v1.GetArtifactByIdResponse, error) { m.ctrl.T.Helper() From 2728e079541be431413e2f37cd3aabcc634c86fc Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Fri, 1 May 2026 00:44:04 +0530 Subject: [PATCH 15/22] chore: trigger CI From f6cc27a3b96545003d123e5b071a52177b412e6d Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Fri, 1 May 2026 05:41:58 +0530 Subject: [PATCH 16/22] test(cli): remove JSON golden tests due to protojson instability (see #6430) --- cmd/cli/app/artifact/artifact_list_test.go | 6 ----- .../testdata/artifact_get.json.golden | 26 ------------------- .../testdata/artifact_list.json.golden | 13 ---------- 3 files changed, 45 deletions(-) delete mode 100644 cmd/cli/app/artifact/testdata/artifact_get.json.golden delete mode 100644 cmd/cli/app/artifact/testdata/artifact_list.json.golden diff --git a/cmd/cli/app/artifact/artifact_list_test.go b/cmd/cli/app/artifact/artifact_list_test.go index bc01a9eff7..a8cf7780fe 100644 --- a/cmd/cli/app/artifact/artifact_list_test.go +++ b/cmd/cli/app/artifact/artifact_list_test.go @@ -40,12 +40,6 @@ func TestArtifactListCommand(t *testing.T) { MockSetup: setupSuccess, GoldenFileName: "artifact_list.table", }, - { - Name: "list artifacts - json output", - Args: []string{"artifact", "list", "-o", "json"}, - MockSetup: setupSuccess, - GoldenFileName: "artifact_list.json", - }, { Name: "list artifacts - yaml output", Args: []string{"artifact", "list", "-o", "yaml"}, diff --git a/cmd/cli/app/artifact/testdata/artifact_get.json.golden b/cmd/cli/app/artifact/testdata/artifact_get.json.golden deleted file mode 100644 index 4cc70a3c12..0000000000 --- a/cmd/cli/app/artifact/testdata/artifact_get.json.golden +++ /dev/null @@ -1,26 +0,0 @@ -{ - "artifact": { - "artifactPk": "111", - "owner": "owner-1", - "name": "artifact-1", - "type": "image", - "visibility": "public", - "repository": "org/repo", - "createdAt": "2024-01-02T15:04:05Z" - } -} -[ - { - "profileId": "artifact-security-baseline", - "entity": "artifact", - "status": "failure", - "entityInfo": { - "name": "owner-1/artifact-1" - }, - "details": "artifact attestation is disabled for this image", - "guidance": "enable artifact attestations before release", - "ruleTypeName": "artifact_attestation_slsa", - "ruleDescriptionName": "Require artifact attestation", - "remediationUrl": "https://example.com/remediate/artifact-111" - } -] diff --git a/cmd/cli/app/artifact/testdata/artifact_list.json.golden b/cmd/cli/app/artifact/testdata/artifact_list.json.golden deleted file mode 100644 index 931d26afa9..0000000000 --- a/cmd/cli/app/artifact/testdata/artifact_list.json.golden +++ /dev/null @@ -1,13 +0,0 @@ -{ - "results": [ - { - "artifactPk": "111", - "owner": "owner-1", - "name": "artifact-1", - "type": "image", - "visibility": "public", - "repository": "org/repo", - "createdAt": "2024-01-02T15:04:05Z" - } - ] -} From 231a024c34530b8fca30fa9f7c4a4a25675e8a1e Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Fri, 1 May 2026 12:30:30 +0530 Subject: [PATCH 17/22] test(cli): update goldens after rebase and renderer changes - keep table/yaml goldens - remove JSON goldens (see #6430) - update profile status table outputs to match current renderer --- cmd/cli/app/artifact/artifact_get_test.go | 2 +- .../artifact/testdata/artifact_get.table.golden | 5 ----- .../app/artifact/testdata/artifact_get.yaml.golden | 10 ---------- .../testdata/status_list_table_detailed.txt.golden | 14 +++++++------- .../status_list_table_detailed_no_emoji.txt.golden | 14 +++++++------- internal/util/cli/context.go | 2 +- 6 files changed, 16 insertions(+), 31 deletions(-) diff --git a/cmd/cli/app/artifact/artifact_get_test.go b/cmd/cli/app/artifact/artifact_get_test.go index fe60e7157a..a7e16133c4 100644 --- a/cmd/cli/app/artifact/artifact_get_test.go +++ b/cmd/cli/app/artifact/artifact_get_test.go @@ -88,4 +88,4 @@ func TestArtifactGetCommand(t *testing.T) { } cli.RunCmdTests(t, tests, ArtifactCmd) -} \ No newline at end of file +} diff --git a/cmd/cli/app/artifact/testdata/artifact_get.table.golden b/cmd/cli/app/artifact/testdata/artifact_get.table.golden index ea790dbde7..d6ac60079c 100644 --- a/cmd/cli/app/artifact/testdata/artifact_get.table.golden +++ b/cmd/cli/app/artifact/testdata/artifact_get.table.golden @@ -3,7 +3,6 @@ 111 │ image │ owner-1 │ artifact-1 │ org/repo │ public │ 2024-01-02T15:04:05Z PROFILE │ RULE │ RESULT │ DETAILS ────────────────────────────┼──────────────────────────────┼─────────┼────────────────────────────── -<<<<<<< HEAD artifact-security-baseline │ Require artifact attestation │ failure │ Alert: artifact attestation │ │ │ alert is active URL: │ │ │ https://example.com/alerts/a @@ -16,7 +15,3 @@ │ │ │ disabled for this image │ │ │ Guidance: enable artifact │ │ │ attestations before release -======= - artifact-security-baseline │ Require artifact attestation │ failure │ - artifact attestation is - │ │ │ disabled for this image ->>>>>>> 25edf5238 (feat(cli): add artifact get command tests and align with GetCLIClient pattern) diff --git a/cmd/cli/app/artifact/testdata/artifact_get.yaml.golden b/cmd/cli/app/artifact/testdata/artifact_get.yaml.golden index 898ebde360..d57865a270 100644 --- a/cmd/cli/app/artifact/testdata/artifact_get.yaml.golden +++ b/cmd/cli/app/artifact/testdata/artifact_get.yaml.golden @@ -7,31 +7,21 @@ artifact: type: image visibility: public -<<<<<<< HEAD - alert: details: artifact attestation alert is active status: "on" url: https://example.com/alerts/artifact-111 details: artifact attestation is disabled for this image -======= -- details: artifact attestation is disabled for this image ->>>>>>> 25edf5238 (feat(cli): add artifact get command tests and align with GetCLIClient pattern) entity: artifact entityInfo: name: owner-1/artifact-1 guidance: enable artifact attestations before release -<<<<<<< HEAD lastUpdated: "2024-01-01T00:00:00Z" profileId: artifact-security-baseline remediationDetails: rebuild the artifact with attestations enabled remediationUrl: https://example.com/remediate/artifact-111 ruleDescriptionName: Require artifact attestation ruleId: artifact-attestation-slsa -======= - profileId: artifact-security-baseline - remediationUrl: https://example.com/remediate/artifact-111 - ruleDescriptionName: Require artifact attestation ->>>>>>> 25edf5238 (feat(cli): add artifact get command tests and align with GetCLIClient pattern) ruleTypeName: artifact_attestation_slsa status: failure diff --git a/cmd/cli/app/profile/status/testdata/status_list_table_detailed.txt.golden b/cmd/cli/app/profile/status/testdata/status_list_table_detailed.txt.golden index 52116a008f..6600fe1817 100644 --- a/cmd/cli/app/profile/status/testdata/status_list_table_detailed.txt.golden +++ b/cmd/cli/app/profile/status/testdata/status_list_table_detailed.txt.golden @@ -2,10 +2,10 @@ ───────────────────────────────┼────────────────┼─────────────────────────────────────────────────── mock-profile │ ✅ │ 2024-01-01T00:00:00Z - ENTITY │ RULE │ RESULT │ DETAILS -─────────────────────┼───────────────────────────────────────────┼────────┼───────────────────────── - acme-corp/mock-repo │ Enable secret scanning to detect │ ✅ │ - Mock rule evaluation - [repository] │ hardcoded secrets │ │ succeeded. - ├───────────────────────────────────────────┼────────┼───────────────────────── - │ Enable CodeQL for vulnerability scanning │ ⛔ │ - Mock rule evaluation - │ │ │ failed. \ No newline at end of file + ENTITY │ RULE │ RESULT │ DETAILS +─────────────────────┼────────────────────────────────────────┼────────┼──────────────────────────── + acme-corp/mock-repo │ Enable secret scanning to detect │ ✅ │ Details: Mock rule + [repository] │ hardcoded secrets │ │ evaluation succeeded. + ├────────────────────────────────────────┼────────┼──────────────────────────── + │ Enable CodeQL for vulnerability │ ⛔ │ Details: Mock rule + │ scanning │ │ evaluation failed. diff --git a/cmd/cli/app/profile/status/testdata/status_list_table_detailed_no_emoji.txt.golden b/cmd/cli/app/profile/status/testdata/status_list_table_detailed_no_emoji.txt.golden index 66a6352abe..880b023a38 100644 --- a/cmd/cli/app/profile/status/testdata/status_list_table_detailed_no_emoji.txt.golden +++ b/cmd/cli/app/profile/status/testdata/status_list_table_detailed_no_emoji.txt.golden @@ -2,10 +2,10 @@ ───────────────────────────────┼────────────────┼─────────────────────────────────────────────────── mock-profile │ Ok │ 2024-01-01T00:00:00Z - ENTITY │ RULE │ RESULT │ DETAILS -─────────────────────┼───────────────────────────────────────────┼────────┼───────────────────────── - acme-corp/mock-repo │ Enable secret scanning to detect │ Ok │ - Mock rule evaluation - [repository] │ hardcoded secrets │ │ succeeded. - ├───────────────────────────────────────────┼────────┼───────────────────────── - │ Enable CodeQL for vulnerability scanning │ Failed │ - Mock rule evaluation - │ │ │ failed. \ No newline at end of file + ENTITY │ RULE │ RESULT │ DETAILS +─────────────────────┼────────────────────────────────────────┼────────┼──────────────────────────── + acme-corp/mock-repo │ Enable secret scanning to detect │ Ok │ Details: Mock rule + [repository] │ hardcoded secrets │ │ evaluation succeeded. + ├────────────────────────────────────────┼────────┼──────────────────────────── + │ Enable CodeQL for vulnerability │ Failed │ Details: Mock rule + │ scanning │ │ evaluation failed. diff --git a/internal/util/cli/context.go b/internal/util/cli/context.go index 8d826c1fab..9a38c00e99 100644 --- a/internal/util/cli/context.go +++ b/internal/util/cli/context.go @@ -63,4 +63,4 @@ func GetCLIClient[T any](cmd *cobra.Command, client func(grpc.ClientConnInterfac func HasRPCClient(ctx context.Context) bool { _, ok := ctx.Value(rpcInjectedKey{}).(struct{}) return ok -} \ No newline at end of file +} From f503cb3b4dba8a16b02d2b0f7acf6da33e3c9702 Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Fri, 1 May 2026 12:36:44 +0530 Subject: [PATCH 18/22] refactor(cli): inline artifact list logic into RunE --- cmd/cli/app/artifact/artifact_list.go | 109 ++++++++++++-------------- 1 file changed, 51 insertions(+), 58 deletions(-) diff --git a/cmd/cli/app/artifact/artifact_list.go b/cmd/cli/app/artifact/artifact_list.go index 7c5ea5267f..a862d7d032 100644 --- a/cmd/cli/app/artifact/artifact_list.go +++ b/cmd/cli/app/artifact/artifact_list.go @@ -4,14 +4,12 @@ package artifact import ( - "context" "fmt" "strings" "time" "github.com/spf13/cobra" "github.com/spf13/viper" - "google.golang.org/grpc" "github.com/mindersec/minder/cmd/cli/app" "github.com/mindersec/minder/internal/util" @@ -38,68 +36,63 @@ var listCmd = &cobra.Command{ return nil }, RunE: func(cmd *cobra.Command, args []string) error { - return listCommand(cmd.Context(), cmd, args, nil) - }, -} - -// listCommand is the artifact list subcommand -func listCommand(ctx context.Context, cmd *cobra.Command, _ []string, _ *grpc.ClientConn) error { - client, closer, err := getArtifactClient(cmd) - if err != nil { - return err - } - defer closer() - - provider := viper.GetString("provider") - project := viper.GetString("project") - format := viper.GetString("output") - fromFilter := viper.GetString("from") - - // No longer print usage on returned error, since we've parsed our inputs - // See https://github.com/spf13/cobra/issues/340#issuecomment-374617413 - cmd.SilenceUsage = true - - artifactList, err := client.ListArtifacts(ctx, &minderv1.ListArtifactsRequest{ - Context: &minderv1.Context{Provider: &provider, Project: &project}, - From: fromFilter, - }) - - if err != nil { - return cli.MessageAndError("Couldn't list artifacts", err) - } - - switch format { - case app.Table: - t := table.New(table.Simple, layouts.Default, cmd.OutOrStdout(), - []string{"ID", "Type", "Owner", "Name", "Repository", "Visibility", "Creation date"}) - for _, artifact := range artifactList.Results { - t.AddRow( - artifact.ArtifactPk, - artifact.Type, - artifact.GetOwner(), - artifact.GetName(), - artifact.Repository, - artifact.Visibility, - artifact.CreatedAt.AsTime().Format(time.RFC3339), - ) + ctx := cmd.Context() - } - t.Render() - case app.JSON: - out, err := util.GetJsonFromProto(artifactList) + client, closer, err := getArtifactClient(cmd) if err != nil { - return cli.MessageAndError("Error getting json from proto", err) + return err } - cmd.Println(out) - case app.YAML: - out, err := util.GetYamlFromProto(artifactList) + defer closer() + + provider := viper.GetString("provider") + project := viper.GetString("project") + format := viper.GetString("output") + fromFilter := viper.GetString("from") + + cmd.SilenceUsage = true + + artifactList, err := client.ListArtifacts(ctx, &minderv1.ListArtifactsRequest{ + Context: &minderv1.Context{Provider: &provider, Project: &project}, + From: fromFilter, + }) if err != nil { - return cli.MessageAndError("Error getting yaml from proto", err) + return cli.MessageAndError("Couldn't list artifacts", err) + } + + switch format { + case app.Table: + t := table.New(table.Simple, layouts.Default, cmd.OutOrStdout(), + []string{"ID", "Type", "Owner", "Name", "Repository", "Visibility", "Creation date"}) + for _, artifact := range artifactList.Results { + t.AddRow( + artifact.ArtifactPk, + artifact.Type, + artifact.GetOwner(), + artifact.GetName(), + artifact.Repository, + artifact.Visibility, + artifact.CreatedAt.AsTime().Format(time.RFC3339), + ) + } + t.Render() + + case app.JSON: + out, err := util.GetJsonFromProto(artifactList) + if err != nil { + return cli.MessageAndError("Error getting json from proto", err) + } + cmd.Println(out) + + case app.YAML: + out, err := util.GetYamlFromProto(artifactList) + if err != nil { + return cli.MessageAndError("Error getting yaml from proto", err) + } + cmd.Println(out) } - cmd.Println(out) - } - return nil + return nil + }, } func init() { From 8c4ab0487a68ad9305707a36ad4106270890df6b Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Fri, 1 May 2026 12:45:06 +0530 Subject: [PATCH 19/22] test(cli): remove remaining JSON goldens and finalize CLI test cleanup --- cmd/cli/app/artifact/artifact_list.go | 2 +- cmd/cli/app/profile/get_test.go | 18 ------ cmd/cli/app/profile/list_test.go | 18 ------ .../profile/testdata/get_by_id.json.golden | 20 ------- .../testdata/list_profiles.json.golden | 60 ------------------- cmd/cli/app/repo/repo_get_test.go | 19 ------ cmd/cli/app/repo/repo_list_test.go | 19 ------ .../repo/testdata/get_name_json.txt.golden | 36 ----------- .../app/repo/testdata/list_json.txt.golden | 20 ------- 9 files changed, 1 insertion(+), 211 deletions(-) delete mode 100644 cmd/cli/app/profile/testdata/get_by_id.json.golden delete mode 100644 cmd/cli/app/profile/testdata/list_profiles.json.golden delete mode 100644 cmd/cli/app/repo/testdata/get_name_json.txt.golden delete mode 100644 cmd/cli/app/repo/testdata/list_json.txt.golden diff --git a/cmd/cli/app/artifact/artifact_list.go b/cmd/cli/app/artifact/artifact_list.go index a862d7d032..0e2732e091 100644 --- a/cmd/cli/app/artifact/artifact_list.go +++ b/cmd/cli/app/artifact/artifact_list.go @@ -35,7 +35,7 @@ var listCmd = &cobra.Command{ return nil }, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() client, closer, err := getArtifactClient(cmd) diff --git a/cmd/cli/app/profile/get_test.go b/cmd/cli/app/profile/get_test.go index 9a2d6b92f8..2784b39c1a 100644 --- a/cmd/cli/app/profile/get_test.go +++ b/cmd/cli/app/profile/get_test.go @@ -18,7 +18,6 @@ import ( //nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state func TestGetCommand(t *testing.T) { - testID := "00000000-0000-0000-0000-000000000001" testName := "test-profile" tests := []cli.CmdTestCase{ @@ -39,23 +38,6 @@ func TestGetCommand(t *testing.T) { }, GoldenFileName: "get_by_name_table.txt", }, - { - Name: "get by id json success", - Args: []string{"profile", "get", "--id", testID, "-o", "json"}, - MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { - t.Helper() - client := mockv1.NewMockProfileServiceClient(ctrl) - mockProf := &minderv1.Profile{} - cli.LoadFixture(t, "mock_profile_get.json", mockProf) - - client.EXPECT(). - GetProfileById(gomock.Any(), gomock.Any()). - Return(&minderv1.GetProfileByIdResponse{Profile: mockProf}, nil) - - return cli.WithRPCClient[minderv1.ProfileServiceClient](context.Background(), client) - }, - GoldenFileName: "get_by_id.json", - }, { Name: "failure missing id and name", Args: []string{"profile", "get"}, diff --git a/cmd/cli/app/profile/list_test.go b/cmd/cli/app/profile/list_test.go index aff6fab7c4..c157ea9c09 100644 --- a/cmd/cli/app/profile/list_test.go +++ b/cmd/cli/app/profile/list_test.go @@ -37,24 +37,6 @@ func TestListCommand(t *testing.T) { }, GoldenFileName: "list_profiles_table.txt", }, - { - Name: "list profiles json success", - Args: []string{"profile", "list", "-o", "json"}, - MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { - t.Helper() - client := mockv1.NewMockProfileServiceClient(ctrl) - - mockResp := &minderv1.ListProfilesResponse{} - cli.LoadFixture(t, "mock_profile_list.json", mockResp) - - client.EXPECT(). - ListProfiles(gomock.Any(), gomock.Any()). - Return(mockResp, nil) - - return cli.WithRPCClient[minderv1.ProfileServiceClient](context.Background(), client) - }, - GoldenFileName: "list_profiles.json", - }, { Name: "list profiles yaml success", Args: []string{"profile", "list", "-o", "yaml"}, diff --git a/cmd/cli/app/profile/testdata/get_by_id.json.golden b/cmd/cli/app/profile/testdata/get_by_id.json.golden deleted file mode 100644 index 4f83231319..0000000000 --- a/cmd/cli/app/profile/testdata/get_by_id.json.golden +++ /dev/null @@ -1,20 +0,0 @@ -{ - "profile": { - "context": { - "provider": "github", - "project": "00000000-0000-0000-0000-000000000000" - }, - "id": "11111111-1111-1111-1111-111111111111", - "name": "mock-profile", - "repository": [ - { - "type": "dependabot_configured", - "def": { - "package_ecosystem": "gomod" - } - } - ], - "remediate": "off", - "alert": "on" - } -} diff --git a/cmd/cli/app/profile/testdata/list_profiles.json.golden b/cmd/cli/app/profile/testdata/list_profiles.json.golden deleted file mode 100644 index 93e2580d9a..0000000000 --- a/cmd/cli/app/profile/testdata/list_profiles.json.golden +++ /dev/null @@ -1,60 +0,0 @@ -{ - "profiles": [ - { - "context": { - "project": "00000000-0000-0000-0000-000000000000" - }, - "id": "11111111-1111-1111-1111-111111111111", - "name": "mock-artifact-profile", - "artifact": [ - { - "type": "artifact_signature", - "params": { - "name": "mock-artifact", - "tags": [ - "latest" - ] - }, - "def": { - "is_signed": true, - "is_verified": true - }, - "name": "Mock ensure artifacts are signed" - } - ], - "remediate": "off", - "alert": "on", - "displayName": "Mock Artifact Signature Profile" - }, - { - "context": { - "project": "00000000-0000-0000-0000-000000000000" - }, - "id": "22222222-2222-2222-2222-222222222222", - "name": "mock-branch-protection", - "repository": [ - { - "type": "branch_protection_enabled", - "params": { - "branch": "main" - }, - "def": {}, - "name": "Mock enable branch protection" - }, - { - "type": "branch_protection_require_pull_request_approving_review_count", - "params": { - "branch": "main" - }, - "def": { - "required_approving_review_count": 2 - }, - "name": "Mock require 2 reviews" - } - ], - "remediate": "on", - "alert": "off", - "displayName": "Mock Branch Protection Profile" - } - ] -} diff --git a/cmd/cli/app/repo/repo_get_test.go b/cmd/cli/app/repo/repo_get_test.go index 13b6e679a5..2af70602d6 100644 --- a/cmd/cli/app/repo/repo_get_test.go +++ b/cmd/cli/app/repo/repo_get_test.go @@ -24,25 +24,6 @@ func TestGetCommand(t *testing.T) { ) tests := []cli.CmdTestCase{ - { - Name: "get repository by name - json output", - Args: []string{"repo", "get", "-n", repoName, "-o", "json"}, - MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { - t.Helper() - client := mockv1.NewMockRepositoryServiceClient(ctrl) - - mockResp := &minderv1.GetRepositoryByNameResponse{} - cli.LoadFixture(t, "mock_repo_get.json", mockResp) - - client.EXPECT(). - GetRepositoryByName(gomock.Any(), gomock.Any()). - Return(mockResp, nil). - Times(1) - - return cli.WithRPCClient[minderv1.RepositoryServiceClient](context.Background(), client) - }, - GoldenFileName: "get_name_json.txt", - }, { Name: "get repository by id - yaml output", Args: []string{"repo", "get", "-i", repoID, "-o", "yaml"}, diff --git a/cmd/cli/app/repo/repo_list_test.go b/cmd/cli/app/repo/repo_list_test.go index eb8bbc15be..a441f463ba 100644 --- a/cmd/cli/app/repo/repo_list_test.go +++ b/cmd/cli/app/repo/repo_list_test.go @@ -38,25 +38,6 @@ func TestListCommand(t *testing.T) { }, GoldenFileName: "list_table.txt", }, - { - Name: "list repositories - json output", - Args: []string{"repo", "list", "-o", "json"}, - MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { - t.Helper() - client := mockv1.NewMockRepositoryServiceClient(ctrl) - - mockResp := &minderv1.ListRepositoriesResponse{} - cli.LoadFixture(t, "mock_repo_list.json", mockResp) - - client.EXPECT(). - ListRepositories(gomock.Any(), gomock.Any()). - Return(mockResp, nil). - Times(1) - - return cli.WithRPCClient[minderv1.RepositoryServiceClient](context.Background(), client) - }, - GoldenFileName: "list_json.txt", - }, { Name: "list repositories - empty result", Args: []string{"repo", "list", "-o", "table"}, diff --git a/cmd/cli/app/repo/testdata/get_name_json.txt.golden b/cmd/cli/app/repo/testdata/get_name_json.txt.golden deleted file mode 100644 index 876ee15e3a..0000000000 --- a/cmd/cli/app/repo/testdata/get_name_json.txt.golden +++ /dev/null @@ -1,36 +0,0 @@ -{ - "context": { - "provider": "github" - }, - "owner": "mock-owner", - "name": "mock-repo", - "repoId": "123456789", - "hookId": "987654321", - "hookUrl": "https://api.github.com/repos/mock-owner/mock-repo/hooks/987654321", - "deployUrl": "https://api.github.com/repos/mock-owner/mock-repo/deployments", - "cloneUrl": "https://github.com/mock-owner/mock-repo.git", - "defaultBranch": "main", - "properties": { - "github/clone_url": "https://github.com/mock-owner/mock-repo.git", - "github/default_branch": "main", - "github/deploy_url": "https://api.github.com/repos/mock-owner/mock-repo/deployments", - "github/hook_id": { - "minder.internal.type": "int64", - "minder.internal.value": "987654321" - }, - "github/hook_url": "https://api.github.com/repos/mock-owner/mock-repo/hooks/987654321", - "github/license": "MIT", - "github/primary_language": "Go", - "github/repo_id": { - "minder.internal.type": "int64", - "minder.internal.value": "123456789" - }, - "github/repo_name": "mock-repo", - "github/repo_owner": "mock-owner", - "is_archived": false, - "is_fork": false, - "is_private": false, - "name": "mock-owner/mock-repo", - "upstream_id": "123456789" - } -} diff --git a/cmd/cli/app/repo/testdata/list_json.txt.golden b/cmd/cli/app/repo/testdata/list_json.txt.golden deleted file mode 100644 index ef1b4221ad..0000000000 --- a/cmd/cli/app/repo/testdata/list_json.txt.golden +++ /dev/null @@ -1,20 +0,0 @@ -{ - "results": [ - { - "context": { - "provider": "github" - }, - "owner": "mock-owner", - "name": "mock-frontend-repo", - "repoId": "1122334455" - }, - { - "context": { - "provider": "github" - }, - "owner": "mock-owner", - "name": "mock-backend-repo", - "repoId": "9988776655" - } - ] -} From 0fba74f56266806c1401b7c5f1d270a9985ea4f8 Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Fri, 1 May 2026 19:32:01 +0530 Subject: [PATCH 20/22] refactor(cli): align with cli.GetCLIClient and remove custom RPC injection - replace getArtifactClient with cli.GetCLIClient - remove custom context injection logic - clean up unnecessary test changes --- cmd/cli/app/artifact/artifact.go | 27 ----------------------- cmd/cli/app/artifact/artifact_get_test.go | 2 -- cmd/cli/app/artifact/artifact_list.go | 2 +- internal/util/cli/context.go | 11 +-------- 4 files changed, 2 insertions(+), 40 deletions(-) diff --git a/cmd/cli/app/artifact/artifact.go b/cmd/cli/app/artifact/artifact.go index 03006de36a..3030d4f471 100644 --- a/cmd/cli/app/artifact/artifact.go +++ b/cmd/cli/app/artifact/artifact.go @@ -6,11 +6,8 @@ package artifact import ( "github.com/spf13/cobra" - "github.com/spf13/viper" "github.com/mindersec/minder/cmd/cli/app" - "github.com/mindersec/minder/internal/util/cli" - minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" ) // ArtifactCmd is the artifact subcommand @@ -29,27 +26,3 @@ func init() { ArtifactCmd.PersistentFlags().StringP("provider", "p", "", "Name of the provider, i.e. github") ArtifactCmd.PersistentFlags().StringP("project", "j", "", "ID of the project") } - -// getArtifactClient is a helper to get the ArtifactServiceClient -func getArtifactClient(cmd *cobra.Command) (minderv1.ArtifactServiceClient, func(), error) { - ctx := cmd.Context() - ctx, cancel := cli.GetAppContext(ctx, viper.GetViper()) - cmd.SetContext(ctx) - - if mockClient, ok := cli.GetRPCClient[minderv1.ArtifactServiceClient](ctx); ok { - return mockClient, func() { cancel() }, nil - } - - conn, err := cli.GrpcForCommand(cmd, viper.GetViper()) - if err != nil { - cancel() - return nil, nil, err - } - - client := minderv1.NewArtifactServiceClient(conn) - - return client, func() { - cancel() - _ = conn.Close() - }, nil -} diff --git a/cmd/cli/app/artifact/artifact_get_test.go b/cmd/cli/app/artifact/artifact_get_test.go index a7e16133c4..99df3739da 100644 --- a/cmd/cli/app/artifact/artifact_get_test.go +++ b/cmd/cli/app/artifact/artifact_get_test.go @@ -16,8 +16,6 @@ import ( mockv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1/mock" ) -// Tests inject both artifact and profile RPC clients. -// //nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state func TestArtifactGetCommand(t *testing.T) { setupSuccess := func(t *testing.T, ctrl *gomock.Controller) context.Context { diff --git a/cmd/cli/app/artifact/artifact_list.go b/cmd/cli/app/artifact/artifact_list.go index 0e2732e091..234b6583cb 100644 --- a/cmd/cli/app/artifact/artifact_list.go +++ b/cmd/cli/app/artifact/artifact_list.go @@ -38,7 +38,7 @@ var listCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() - client, closer, err := getArtifactClient(cmd) + client, closer, err := cli.GetCLIClient(cmd, minderv1.NewArtifactServiceClient) if err != nil { return err } diff --git a/internal/util/cli/context.go b/internal/util/cli/context.go index 9a38c00e99..7a04fd7dca 100644 --- a/internal/util/cli/context.go +++ b/internal/util/cli/context.go @@ -16,13 +16,10 @@ type rpcKey struct { clientType reflect.Type } -type rpcInjectedKey struct{} - // WithRPCClient injects the provided RPC client into the context. func WithRPCClient[T any](ctx context.Context, client T) context.Context { key := rpcKey{clientType: reflect.TypeOf((*T)(nil)).Elem()} - ctx = context.WithValue(ctx, key, client) - return context.WithValue(ctx, rpcInjectedKey{}, struct{}{}) + return context.WithValue(ctx, key, client) } // GetRPCClient extracts the generic RPC client from the provided context. @@ -58,9 +55,3 @@ func GetCLIClient[T any](cmd *cobra.Command, client func(grpc.ClientConnInterfac _ = conn.Close() }, nil } - -// HasRPCClient reports whether any RPC client has been injected into the context. -func HasRPCClient(ctx context.Context) bool { - _, ok := ctx.Value(rpcInjectedKey{}).(struct{}) - return ok -} From 4556e519cd9036497ba1fd268232ae14ed95336c Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Fri, 1 May 2026 20:46:46 +0530 Subject: [PATCH 21/22] chore(test): restore upstream comments in artifact_get_test.go --- cmd/cli/app/artifact/artifact_get_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd/cli/app/artifact/artifact_get_test.go b/cmd/cli/app/artifact/artifact_get_test.go index 99df3739da..5ff94a5dcf 100644 --- a/cmd/cli/app/artifact/artifact_get_test.go +++ b/cmd/cli/app/artifact/artifact_get_test.go @@ -3,6 +3,10 @@ package artifact +// JSON output is not tested because protojson formatting is not stable across environments. +// This can cause flaky tests due to spacing differences. +// See maintainer discussion in PR #6417. + import ( "context" "testing" @@ -26,6 +30,7 @@ func TestArtifactGetCommand(t *testing.T) { artifactResp := &minderv1.GetArtifactByIdResponse{} cli.LoadFixture(t, "mock_artifact_get.json", artifactResp) + artifactClient.EXPECT(). GetArtifactById(gomock.Any(), gomock.Any()). Return(artifactResp, nil). @@ -68,7 +73,6 @@ func TestArtifactGetCommand(t *testing.T) { Args: []string{"artifact", "get", "-i", "111"}, MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { t.Helper() - artifactClient := mockv1.NewMockArtifactServiceClient(ctrl) profileClient := mockv1.NewMockProfileServiceClient(ctrl) From 9aa2a5240e82133f6dcee822a5a2e698fff38ecd Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Fri, 1 May 2026 21:06:49 +0530 Subject: [PATCH 22/22] chore: retrigger CI