Skip to content

Commit 21c6dd5

Browse files
ehl-jfclaude
andauthored
Add --format flag support to create-evidence (#54)
* JGC-479 - Add --format flag support to create-evidence * JGC-479 - Use assert.JSONEq for JSON output tests Replaces Contains-based assertions in printCreateEvidenceResponse JSON tests with full-payload JSONEq comparisons, per PR review feedback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b58a3e3 commit 21c6dd5

5 files changed

Lines changed: 293 additions & 37 deletions

File tree

evidence/cli/command/command_cli.go

Lines changed: 136 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
11
package command
22

33
import (
4+
"encoding/json"
45
"errors"
56
"fmt"
7+
"io"
68
"os"
79
"slices"
810
"strings"
911

12+
"github.com/jedib0t/go-pretty/v6/table"
13+
"github.com/jedib0t/go-pretty/v6/text"
1014
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/application"
1115
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/artifacts"
1216
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/build"
1317
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/flags"
1418
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/github"
15-
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/interface"
16-
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/package"
19+
_interface "github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/interface"
20+
_package "github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/package"
1721
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/releasebundle"
1822
commandUtils "github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/utils"
1923
evdConfig "github.com/jfrog/jfrog-cli-evidence/evidence/config"
24+
"github.com/jfrog/jfrog-cli-evidence/evidence/model"
2025

2126
commonCliUtils "github.com/jfrog/jfrog-cli-core/v2/common/cliutils"
2227
"github.com/jfrog/jfrog-cli-core/v2/common/commands"
28+
"github.com/jfrog/jfrog-cli-core/v2/common/format"
2329
pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common"
2430
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
2531
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
@@ -37,15 +43,23 @@ import (
3743
generateCmd "github.com/jfrog/jfrog-cli-evidence/evidence/generate"
3844
)
3945

46+
// responseCollector is implemented by evidence create commands that accumulate
47+
// CreateResponse values during Run(). The interface is checked via type assertion
48+
// after command execution so that printCreateEvidenceResponse can format output.
49+
type responseCollector interface {
50+
CollectedResponses() []*model.CreateResponse
51+
}
52+
4053
func GetCommands() []components.Command {
4154
return []components.Command{
4255
{
43-
Name: "create-evidence",
44-
Aliases: []string{"create"},
45-
Flags: flags.GetCommandFlags(flags.CreateEvidence),
46-
Description: create.GetDescription(),
47-
Arguments: create.GetArguments(),
48-
Action: createEvidence,
56+
Name: "create-evidence",
57+
Aliases: []string{"create"},
58+
Flags: flags.GetCommandFlags(flags.CreateEvidence),
59+
Description: create.GetDescription(),
60+
Arguments: create.GetArguments(),
61+
Action: createEvidence,
62+
SupportedFormats: []format.OutputFormat{format.Json, format.Table},
4963
},
5064
{
5165
Name: "get-evidence",
@@ -74,8 +88,10 @@ func GetCommands() []components.Command {
7488
}
7589
}
7690

77-
var execFunc = commands.Exec
78-
var ErrUnsupportedSubject = errors.New("unsupported subject")
91+
var (
92+
execFunc = commands.Exec
93+
ErrUnsupportedSubject = errors.New("unsupported subject")
94+
)
7995

8096
func createEvidence(ctx *components.Context) error {
8197
if err := validateCreateEvidenceCommonContext(ctx); err != nil {
@@ -90,23 +106,50 @@ func createEvidence(ctx *components.Context) error {
90106
return err
91107
}
92108

93-
if slices.Contains(evidenceType, flags.TypeFlag) || (slices.Contains(evidenceType, flags.BuildName) && slices.Contains(evidenceType, flags.TypeFlag)) {
94-
return github.NewEvidenceGitHubCommand(ctx, execFunc).CreateEvidence(ctx, serverDetails)
109+
outputFormat, err := ctx.GetOutputFormat()
110+
if err != nil {
111+
return err
95112
}
96113

97-
evidenceCommands := map[string]func(*components.Context, commandUtils.ExecCommandFunc) _interface.EvidenceCommands{
98-
flags.SubjectRepoPath: artifacts.NewEvidenceCustomCommand,
99-
flags.ReleaseBundle: releasebundle.NewEvidenceReleaseBundleCommand,
100-
flags.BuildName: build.NewEvidenceBuildCommand,
101-
flags.PackageName: _package.NewEvidencePackageCommand,
102-
flags.ApplicationKey: application.NewEvidenceApplicationCommand,
114+
// Wrap execFunc to capture collected responses after each command runs.
115+
var collectedResponses []*model.CreateResponse
116+
collectingExec := func(cmd commands.Command) error {
117+
if runErr := execFunc(cmd); runErr != nil {
118+
return runErr
119+
}
120+
if rc, ok := cmd.(responseCollector); ok {
121+
collectedResponses = append(collectedResponses, rc.CollectedResponses()...)
122+
}
123+
return nil
103124
}
104125

105-
if commandFunc, exists := evidenceCommands[evidenceType[0]]; exists {
106-
return commandFunc(ctx, execFunc).CreateEvidence(ctx, serverDetails)
126+
var createErr error
127+
if slices.Contains(evidenceType, flags.TypeFlag) || (slices.Contains(evidenceType, flags.BuildName) && slices.Contains(evidenceType, flags.TypeFlag)) {
128+
createErr = github.NewEvidenceGitHubCommand(ctx, collectingExec).CreateEvidence(ctx, serverDetails)
129+
} else {
130+
evidenceCommands := map[string]func(*components.Context, commandUtils.ExecCommandFunc) _interface.EvidenceCommands{
131+
flags.SubjectRepoPath: artifacts.NewEvidenceCustomCommand,
132+
flags.ReleaseBundle: releasebundle.NewEvidenceReleaseBundleCommand,
133+
flags.BuildName: build.NewEvidenceBuildCommand,
134+
flags.PackageName: _package.NewEvidencePackageCommand,
135+
flags.ApplicationKey: application.NewEvidenceApplicationCommand,
136+
}
137+
138+
if commandFunc, exists := evidenceCommands[evidenceType[0]]; exists {
139+
createErr = commandFunc(ctx, collectingExec).CreateEvidence(ctx, serverDetails)
140+
} else {
141+
return ErrUnsupportedSubject
142+
}
107143
}
108144

109-
return ErrUnsupportedSubject
145+
if createErr != nil {
146+
return createErr
147+
}
148+
149+
if outputFormat != "" {
150+
return printCreateEvidenceResponse(os.Stdout, outputFormat, collectedResponses)
151+
}
152+
return nil
110153
}
111154

112155
func getEvidence(ctx *components.Context) error {
@@ -487,3 +530,75 @@ func generateKeyPair(ctx *components.Context) error {
487530
cmd := generateCmd.NewGenerateKeyPairCommand(serverDetails, uploadKey, alias, keyFilePath, fileName)
488531
return cmd.Run()
489532
}
533+
534+
// tableFieldOrder defines the display order for create-evidence table output.
535+
var tableFieldOrder = []struct {
536+
key string
537+
value func(*model.CreateResponse) string
538+
}{
539+
{"repository", func(r *model.CreateResponse) string { return r.Repository }},
540+
{"path", func(r *model.CreateResponse) string { return r.Path }},
541+
{"name", func(r *model.CreateResponse) string { return r.Name }},
542+
{"uri", func(r *model.CreateResponse) string { return r.Uri }},
543+
{"sha256", func(r *model.CreateResponse) string { return r.Sha256 }},
544+
{"predicate_type", func(r *model.CreateResponse) string { return r.PredicateType }},
545+
{"predicate_category", func(r *model.CreateResponse) string { return r.PredicateCategory }},
546+
{"predicate_slug", func(r *model.CreateResponse) string { return r.PredicateSlug }},
547+
{"created_at", func(r *model.CreateResponse) string { return r.CreatedAt }},
548+
{"created_by", func(r *model.CreateResponse) string { return r.CreatedBy }},
549+
{"verified", func(r *model.CreateResponse) string {
550+
if r.Verified {
551+
return "true"
552+
}
553+
return "false"
554+
}},
555+
{"provider_id", func(r *model.CreateResponse) string { return r.ProviderId }},
556+
}
557+
558+
// printCreateEvidenceResponse writes formatted output for the given responses.
559+
func printCreateEvidenceResponse(w io.Writer, outputFormat format.OutputFormat, responses []*model.CreateResponse) error {
560+
switch outputFormat {
561+
case format.Json:
562+
return printCreateEvidenceJSON(w, responses)
563+
case format.Table:
564+
return printCreateEvidenceTable(w, responses)
565+
default:
566+
return fmt.Errorf("unsupported format %q: accepted values are json, table", outputFormat)
567+
}
568+
}
569+
570+
func printCreateEvidenceJSON(w io.Writer, responses []*model.CreateResponse) error {
571+
enc := json.NewEncoder(w)
572+
enc.SetIndent("", " ")
573+
if len(responses) == 1 {
574+
return enc.Encode(responses[0])
575+
}
576+
return enc.Encode(responses)
577+
}
578+
579+
func printCreateEvidenceTable(w io.Writer, responses []*model.CreateResponse) error {
580+
for i, r := range responses {
581+
if i > 0 {
582+
if _, err := fmt.Fprintln(w); err != nil {
583+
return err
584+
}
585+
}
586+
t := table.NewWriter()
587+
t.SetOutputMirror(w)
588+
t.SetStyle(table.StyleLight)
589+
t.Style().Options.SeparateRows = false
590+
t.AppendHeader(table.Row{"FIELD", "VALUE"})
591+
t.SetColumnConfigs([]table.ColumnConfig{
592+
{Number: 1, Align: text.AlignLeft, AlignHeader: text.AlignLeft},
593+
{Number: 2, Align: text.AlignLeft, AlignHeader: text.AlignLeft},
594+
})
595+
for _, field := range tableFieldOrder {
596+
val := field.value(r)
597+
if val != "" {
598+
t.AppendRow(table.Row{field.key, val})
599+
}
600+
}
601+
t.Render()
602+
}
603+
return nil
604+
}

evidence/cli/command/command_cli_test.go

Lines changed: 140 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
package command
22

33
import (
4+
"bytes"
45
"flag"
5-
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/flags"
6-
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/utils"
7-
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/test"
8-
evdConfig "github.com/jfrog/jfrog-cli-evidence/evidence/config"
96
"os"
107
"testing"
118

129
"github.com/jfrog/jfrog-cli-core/v2/common/commands"
10+
"github.com/jfrog/jfrog-cli-core/v2/common/format"
1311
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
1412
coreUtils "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
13+
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/flags"
14+
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/utils"
15+
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/test"
16+
evdConfig "github.com/jfrog/jfrog-cli-evidence/evidence/config"
17+
"github.com/jfrog/jfrog-cli-evidence/evidence/model"
1518
"github.com/stretchr/testify/assert"
1619
"github.com/urfave/cli"
1720
"go.uber.org/mock/gomock"
@@ -878,3 +881,136 @@ func TestValidateCreateEvidenceCommonContext_Attachments(t *testing.T) {
878881
assert.Error(t, validateCreateEvidenceCommonContext(c))
879882
})
880883
}
884+
885+
// ── printCreateEvidenceResponse tests ──────────────────────────────────────────
886+
887+
func sampleResponse() *model.CreateResponse {
888+
return &model.CreateResponse{
889+
Repository: "my-repo",
890+
Path: "com/example",
891+
Name: "artifact-1.0.jar",
892+
Uri: "https://example.jfrog.io/my-repo/com/example/artifact-1.0.jar",
893+
Sha256: "abc123def456",
894+
PredicateType: "https://in-toto.io/attestation/vulns",
895+
PredicateCategory: "SPDX",
896+
PredicateSlug: "vulns",
897+
CreatedAt: "2024-01-15T10:30:00Z",
898+
CreatedBy: "ci-user",
899+
Verified: true,
900+
ProviderId: "jfrog",
901+
}
902+
}
903+
904+
func TestPrintCreateEvidenceResponse_JSON(t *testing.T) {
905+
var buf bytes.Buffer
906+
r := sampleResponse()
907+
908+
err := printCreateEvidenceResponse(&buf, format.Json, []*model.CreateResponse{r})
909+
assert.NoError(t, err)
910+
911+
expected := `{
912+
"repository": "my-repo",
913+
"path": "com/example",
914+
"name": "artifact-1.0.jar",
915+
"uri": "https://example.jfrog.io/my-repo/com/example/artifact-1.0.jar",
916+
"sha256": "abc123def456",
917+
"predicate_category": "SPDX",
918+
"predicate_type": "https://in-toto.io/attestation/vulns",
919+
"predicate_slug": "vulns",
920+
"created_at": "2024-01-15T10:30:00Z",
921+
"created_by": "ci-user",
922+
"verified": true,
923+
"provider_id": "jfrog"
924+
}`
925+
assert.JSONEq(t, expected, buf.String())
926+
}
927+
928+
func TestPrintCreateEvidenceResponse_JSON_MultipleResponses(t *testing.T) {
929+
var buf bytes.Buffer
930+
r1 := sampleResponse()
931+
r2 := sampleResponse()
932+
r2.Name = "artifact-2.0.jar"
933+
934+
err := printCreateEvidenceResponse(&buf, format.Json, []*model.CreateResponse{r1, r2})
935+
assert.NoError(t, err)
936+
937+
expected := `[
938+
{
939+
"repository": "my-repo",
940+
"path": "com/example",
941+
"name": "artifact-1.0.jar",
942+
"uri": "https://example.jfrog.io/my-repo/com/example/artifact-1.0.jar",
943+
"sha256": "abc123def456",
944+
"predicate_category": "SPDX",
945+
"predicate_type": "https://in-toto.io/attestation/vulns",
946+
"predicate_slug": "vulns",
947+
"created_at": "2024-01-15T10:30:00Z",
948+
"created_by": "ci-user",
949+
"verified": true,
950+
"provider_id": "jfrog"
951+
},
952+
{
953+
"repository": "my-repo",
954+
"path": "com/example",
955+
"name": "artifact-2.0.jar",
956+
"uri": "https://example.jfrog.io/my-repo/com/example/artifact-1.0.jar",
957+
"sha256": "abc123def456",
958+
"predicate_category": "SPDX",
959+
"predicate_type": "https://in-toto.io/attestation/vulns",
960+
"predicate_slug": "vulns",
961+
"created_at": "2024-01-15T10:30:00Z",
962+
"created_by": "ci-user",
963+
"verified": true,
964+
"provider_id": "jfrog"
965+
}
966+
]`
967+
assert.JSONEq(t, expected, buf.String())
968+
}
969+
970+
func TestPrintCreateEvidenceResponse_Table(t *testing.T) {
971+
var buf bytes.Buffer
972+
r := sampleResponse()
973+
974+
err := printCreateEvidenceResponse(&buf, format.Table, []*model.CreateResponse{r})
975+
assert.NoError(t, err)
976+
977+
out := buf.String()
978+
// Table must have a FIELD / VALUE header.
979+
assert.Contains(t, out, "FIELD")
980+
assert.Contains(t, out, "VALUE")
981+
// Known field names and values should appear.
982+
assert.Contains(t, out, "repository")
983+
assert.Contains(t, out, "my-repo")
984+
assert.Contains(t, out, "sha256")
985+
assert.Contains(t, out, "abc123def456")
986+
assert.Contains(t, out, "verified")
987+
assert.Contains(t, out, "true")
988+
}
989+
990+
func TestPrintCreateEvidenceResponse_Table_AbsentFieldsOmitted(t *testing.T) {
991+
var buf bytes.Buffer
992+
// Sparse response – only repository is set.
993+
r := &model.CreateResponse{
994+
Repository: "sparse-repo",
995+
}
996+
997+
err := printCreateEvidenceResponse(&buf, format.Table, []*model.CreateResponse{r})
998+
assert.NoError(t, err)
999+
1000+
out := buf.String()
1001+
assert.Contains(t, out, "repository")
1002+
assert.Contains(t, out, "sparse-repo")
1003+
// Fields that are empty must not appear in the table.
1004+
assert.NotContains(t, out, "sha256")
1005+
assert.NotContains(t, out, "predicate_type")
1006+
assert.NotContains(t, out, "provider_id")
1007+
}
1008+
1009+
func TestPrintCreateEvidenceResponse_UnsupportedFormat(t *testing.T) {
1010+
var buf bytes.Buffer
1011+
r := sampleResponse()
1012+
1013+
err := printCreateEvidenceResponse(&buf, format.OutputFormat("xml"), []*model.CreateResponse{r})
1014+
assert.Error(t, err)
1015+
assert.Contains(t, err.Error(), "unsupported format")
1016+
}

evidence/create/create_base.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ type createEvidenceBase struct {
5353
artifactoryClient artifactory.ArtifactoryServicesManager
5454
uploader evidenceUploader
5555
stmtResolver sonar.StatementResolver
56+
collectedResponses []*model.CreateResponse
57+
}
58+
59+
// CollectedResponses returns the list of CreateResponse objects gathered during evidence upload.
60+
func (c *createEvidenceBase) CollectedResponses() []*model.CreateResponse {
61+
return c.collectedResponses
5662
}
5763

5864
const EvdDefaultUser = "JFrog CLI"
@@ -275,6 +281,7 @@ func (c *createEvidenceBase) uploadEvidence(evidencePayload []byte, repoPath str
275281
} else {
276282
log.Info("Evidence successfully created but not verified due to missing/invalid public key")
277283
}
284+
c.collectedResponses = append(c.collectedResponses, createResponse)
278285
return createResponse, nil
279286
}
280287

0 commit comments

Comments
 (0)