Skip to content

Commit 1aa30f0

Browse files
committed
cr
1 parent 693f884 commit 1aa30f0

File tree

6 files changed

+129
-111
lines changed

6 files changed

+129
-111
lines changed

apptrust/commands/application/create_app_cmd.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import (
1717
"github.com/jfrog/jfrog-cli-application/apptrust/service/applications"
1818
)
1919

20+
const (
21+
entrySeparator = ";"
22+
partSeparator = ":"
23+
)
24+
2025
type createAppCommand struct {
2126
serverDetails *coreConfig.ServerDetails
2227
applicationService applications.ApplicationService

apptrust/commands/flags.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const (
5353
BuildsFlag = "builds"
5454
ReleaseBundlesFlag = "release-bundles"
5555
SourceVersionFlag = "source-version"
56+
PackagesFlag = "packages"
5657
)
5758

5859
// Flag keys mapped to their corresponding components.Flag definition.
@@ -90,6 +91,7 @@ var flagsMap = map[string]components.Flag{
9091
BuildsFlag: components.NewStringFlag(BuildsFlag, "List of builds in format 'name1:number1[:timestamp1];name2:number2[:timestamp2]'", func(f *components.StringFlag) { f.Mandatory = false }),
9192
ReleaseBundlesFlag: components.NewStringFlag(ReleaseBundlesFlag, "List of release bundles in format 'name1:version1;name2:version2'", func(f *components.StringFlag) { f.Mandatory = false }),
9293
SourceVersionFlag: components.NewStringFlag(SourceVersionFlag, "Source versions in format 'app1:version1;app2:version2'", func(f *components.StringFlag) { f.Mandatory = false }),
94+
PackagesFlag: components.NewStringFlag(PackagesFlag, "List of packages in format 'name1;name2'", func(f *components.StringFlag) { f.Mandatory = false }),
9395
}
9496

9597
var commandFlags = map[string][]string{
@@ -100,9 +102,8 @@ var commandFlags = map[string][]string{
100102
serverId,
101103
ApplicationKeyFlag,
102104
TagFlag,
105+
PackagesFlag,
103106
PackageTypeFlag,
104-
PackageNameFlag,
105-
PackageVersionFlag,
106107
PackageRepositoryFlag,
107108
BuildsFlag,
108109
ReleaseBundlesFlag,
@@ -145,13 +146,18 @@ var commandFlags = map[string][]string{
145146
user,
146147
accessToken,
147148
serverId,
149+
ApplicationKeyFlag,
150+
PackagesFlag,
151+
PackageTypeFlag,
148152
},
149-
150153
PackageUnbind: {
151154
url,
152155
user,
153156
accessToken,
154157
serverId,
158+
ApplicationKeyFlag,
159+
PackagesFlag,
160+
PackageTypeFlag,
155161
},
156162

157163
Ping: {

apptrust/commands/utils/utils.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ import (
1313
"github.com/jfrog/jfrog-client-go/utils/errorutils"
1414
)
1515

16+
const (
17+
EntrySeparator = ";"
18+
PartSeparator = ":"
19+
)
20+
1621
func AssertValueProvided(c *components.Context, fieldName string) error {
1722
if c.GetStringFlagValue(fieldName) == "" {
1823
return errorutils.CheckErrorf("the --%s option is mandatory", fieldName)
@@ -81,3 +86,34 @@ func ValidateEnumFlag(flagName, value string, defaultValue string, allowedValues
8186
return "", errorutils.CheckErrorf("invalid value for --%s: '%s'. Allowed values: %s",
8287
flagName, value, coreutils.ListToText(allowedValues))
8388
}
89+
90+
func ParsePackagesFlag(flagValue string) ([]map[string]string, error) {
91+
if flagValue == "" {
92+
return nil, nil
93+
}
94+
pairs := strings.Split(flagValue, ",")
95+
var result []map[string]string
96+
for _, pair := range pairs {
97+
parts := strings.SplitN(strings.TrimSpace(pair), ":", 2)
98+
if len(parts) != 2 {
99+
return nil, fmt.Errorf("invalid package format: %s (expected <name>:<version>)", pair)
100+
}
101+
result = append(result, map[string]string{"name": parts[0], "version": parts[1]})
102+
}
103+
return result, nil
104+
}
105+
106+
// ParseDelimitedSlice splits a delimited string into a slice of string slices.
107+
// Example: input "a:1;b:2" with entrySep=";" and partSep=":" returns [][]string{{"a","1"},{"b","2"}}
108+
func ParseDelimitedSlice(input string) [][]string {
109+
var result [][]string
110+
if input == "" {
111+
return result
112+
}
113+
entries := strings.Split(input, EntrySeparator)
114+
for _, entry := range entries {
115+
parts := strings.Split(entry, PartSeparator)
116+
result = append(result, parts)
117+
}
118+
return result
119+
}

apptrust/commands/version/create_app_version_cmd.go

Lines changed: 35 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package version
22

33
import (
44
"encoding/json"
5-
"strings"
6-
"time"
75

86
"github.com/jfrog/jfrog-cli-application/apptrust/service/versions"
97

@@ -22,6 +20,11 @@ import (
2220
"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
2321
)
2422

23+
const (
24+
entrySeparator = ";"
25+
partSeparator = ":"
26+
)
27+
2528
type createAppVersionCommand struct {
2629
versionService versions.VersionService
2730
serverDetails *coreConfig.ServerDetails
@@ -74,12 +77,8 @@ func (cv *createAppVersionCommand) buildRequestPayload(ctx *components.Context)
7477
err error
7578
)
7679

77-
if err = validateNoSpecAndFlagsTogether(ctx); err != nil {
78-
return nil, err
79-
}
80-
8180
if ctx.IsFlagSet(commands.SpecFlag) {
82-
sources, err = cv.buildSourcesFromSpec(ctx)
81+
sources, err = cv.loadFromSpec(ctx)
8382
} else {
8483
sources, err = cv.buildSourcesFromFlags(ctx)
8584
}
@@ -91,24 +90,26 @@ func (cv *createAppVersionCommand) buildRequestPayload(ctx *components.Context)
9190
return &model.CreateAppVersionRequest{
9291
ApplicationKey: ctx.Arguments[0],
9392
Version: ctx.Arguments[1],
94-
Sources: appendSourceIfExists(sources),
93+
Sources: sources,
9594
Tag: ctx.GetStringFlagValue(commands.TagFlag),
9695
}, nil
9796
}
9897

99-
func (cv *createAppVersionCommand) buildSourcesFromSpec(ctx *components.Context) (*model.CreateVersionSources, error) {
100-
return cv.loadFromSpec(ctx)
101-
}
102-
10398
func (cv *createAppVersionCommand) buildSourcesFromFlags(ctx *components.Context) (*model.CreateVersionSources, error) {
10499
sources := &model.CreateVersionSources{}
105-
if ctx.IsFlagSet(commands.PackageNameFlag) {
106-
sources.Packages = append(sources.Packages, model.CreateVersionPackage{
107-
Type: ctx.GetStringFlagValue(commands.PackageTypeFlag),
108-
Name: ctx.GetStringFlagValue(commands.PackageNameFlag),
109-
Version: ctx.GetStringFlagValue(commands.PackageVersionFlag),
110-
Repository: ctx.GetStringFlagValue(commands.PackageRepositoryFlag),
111-
})
100+
if ctx.IsFlagSet(commands.PackagesFlag) {
101+
parsedPkgs, err := utils.ParsePackagesFlag(ctx.GetStringFlagValue(commands.PackagesFlag))
102+
if err != nil {
103+
return nil, err
104+
}
105+
for _, pkg := range parsedPkgs {
106+
sources.Packages = append(sources.Packages, model.CreateVersionPackage{
107+
Type: ctx.GetStringFlagValue(commands.PackageTypeFlag),
108+
Name: pkg["name"],
109+
Version: pkg["version"],
110+
Repository: ctx.GetStringFlagValue(commands.PackageRepositoryFlag),
111+
})
112+
}
112113
}
113114
if buildsStr := ctx.GetStringFlagValue(commands.BuildsFlag); buildsStr != "" {
114115
builds, err := cv.parseBuilds(buildsStr)
@@ -167,81 +168,51 @@ func (cv *createAppVersionCommand) loadFromSpec(ctx *components.Context) (*model
167168
return sources, nil
168169
}
169170

170-
// Helper method to parse builds string format: "name1:number1[:timestamp1];name2:number2[:timestamp2]"
171171
func (cv *createAppVersionCommand) parseBuilds(buildsStr string) ([]model.CreateVersionBuild, error) {
172172
var builds []model.CreateVersionBuild
173-
buildEntries := strings.Split(buildsStr, ";")
174-
175-
for _, entry := range buildEntries {
176-
parts := strings.Split(entry, ":")
173+
for _, parts := range utils.ParseDelimitedSlice(buildsStr) {
177174
if len(parts) < 2 || len(parts) > 3 {
178-
return nil, errorutils.CheckErrorf("invalid build format: '%s'. Expected format: name:number[:timestamp]", entry)
175+
return nil, errorutils.CheckErrorf("invalid build format: %v", parts)
179176
}
180-
181177
build := model.CreateVersionBuild{
182178
Name: parts[0],
183179
Number: parts[1],
184180
}
185-
186-
// Add timestamp if provided (optional)
187181
if len(parts) == 3 {
188182
build.Started = parts[2]
189-
190-
// Validate timestamp format
191-
_, err := time.Parse(time.RFC3339, build.Started)
192-
if err != nil {
193-
return nil, errorutils.CheckErrorf("invalid timestamp format for build '%s': %s. Expected RFC3339 format (e.g., 2006-01-02T15:04:05Z)", build.Name, err.Error())
194-
}
195183
}
196-
197184
builds = append(builds, build)
198185
}
199-
200186
return builds, nil
201187
}
202188

203189
// Helper method to parse release bundles string format: "name1:version1;name2:version2"
204190
func (cv *createAppVersionCommand) parseReleaseBundles(rbStr string) ([]model.CreateVersionReleaseBundle, error) {
205-
var releaseBundles []model.CreateVersionReleaseBundle
206-
rbEntries := strings.Split(rbStr, ";")
207-
208-
for _, entry := range rbEntries {
209-
parts := strings.Split(entry, ":")
191+
var bundles []model.CreateVersionReleaseBundle
192+
for _, parts := range utils.ParseDelimitedSlice(rbStr) {
210193
if len(parts) != 2 {
211-
return nil, errorutils.CheckErrorf("invalid release bundle format: '%s'. Expected format: name:version", entry)
194+
return nil, errorutils.CheckErrorf("invalid release bundle format: %v", parts)
212195
}
213-
214-
rb := model.CreateVersionReleaseBundle{
196+
bundles = append(bundles, model.CreateVersionReleaseBundle{
215197
Name: parts[0],
216198
Version: parts[1],
217-
}
218-
219-
releaseBundles = append(releaseBundles, rb)
199+
})
220200
}
221-
222-
return releaseBundles, nil
201+
return bundles, nil
223202
}
224203

225-
// Helper method to parse source versions string format: "app1:version1;app2:version2"
226204
func (cv *createAppVersionCommand) parseSourceVersions(sourceVersionsStr string) ([]model.CreateVersionReference, error) {
227-
var sourceVersions []model.CreateVersionReference
228-
versionEntries := strings.Split(sourceVersionsStr, ";")
229-
230-
for _, entry := range versionEntries {
231-
parts := strings.Split(entry, ":")
205+
var refs []model.CreateVersionReference
206+
for _, parts := range utils.ParseDelimitedSlice(sourceVersionsStr) {
232207
if len(parts) != 2 {
233-
return nil, errorutils.CheckErrorf("invalid source version format: '%s'. Expected format: app-key:version", entry)
208+
return nil, errorutils.CheckErrorf("invalid source version format: %v", parts)
234209
}
235-
236-
sv := model.CreateVersionReference{
210+
refs = append(refs, model.CreateVersionReference{
237211
ApplicationKey: parts[0],
238212
Version: parts[1],
239-
}
240-
241-
sourceVersions = append(sourceVersions, sv)
213+
})
242214
}
243-
244-
return sourceVersions, nil
215+
return refs, nil
245216
}
246217

247218
// Updated validation logic to support multiple source types
@@ -278,7 +249,7 @@ func GetCreateAppVersionCommand(appContext app.Context) components.Command {
278249
cmd := &createAppVersionCommand{versionService: appContext.GetVersionService()}
279250
return components.Command{
280251
Name: commands.VersionCreate,
281-
Description: "Creates a new version of the specified application with the given version number, tag, and associated packages, builds, release bundles, or source versions.",
252+
Description: "Create application version.",
282253
Category: common.CategoryVersion,
283254
Aliases: []string{"vc"},
284255
Arguments: []components.Argument{
@@ -303,17 +274,6 @@ func handleMissingPackageDetailsError() error {
303274
commands.SpecFlag, commands.PackageTypeFlag, commands.PackageNameFlag, commands.PackageVersionFlag, commands.PackageRepositoryFlag)
304275
}
305276

306-
// Helper to return a pointer only if the object has any sources, otherwise returns nil
307-
func appendSourceIfExists(s *model.CreateVersionSources) *model.CreateVersionSources {
308-
if s == nil {
309-
return nil
310-
}
311-
if len(s.Packages) > 0 || len(s.Builds) > 0 || len(s.ReleaseBundles) > 0 || len(s.Versions) > 0 {
312-
return s
313-
}
314-
return nil
315-
}
316-
317277
// Returns error if both --spec and any other source flag are set
318278
func validateNoSpecAndFlagsTogether(ctx *components.Context) error {
319279
if ctx.IsFlagSet(commands.SpecFlag) {

apptrust/commands/version/create_app_version_cmd_test.go

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -114,47 +114,57 @@ func TestCreateAppVersionCommand_FlagsSuite(t *testing.T) {
114114
name: "all flags",
115115
ctxSetup: func(ctx *components.Context) {
116116
ctx.Arguments = []string{"app-key", "1.0.0"}
117-
ctx.AddStringFlag("package-type", "type")
118-
ctx.AddStringFlag("package-name", "name")
119-
ctx.AddStringFlag("package-version", "1.0.0")
120-
ctx.AddStringFlag("package-repository", "repo")
121-
ctx.AddStringFlag("url", "https://example.com")
117+
ctx.AddStringFlag("tag", "release-tag")
118+
ctx.AddStringFlag("packages", "pkg1:1.2.3,pkg2:2.3.4")
119+
ctx.AddStringFlag("build", "build1:1.0.0:2024-01-01;build2:2.0.0:2024-02-01")
120+
ctx.AddStringFlag("release-bundle", "rb1:1.0.0;rb2:2.0.0")
121+
ctx.AddStringFlag("source-version", "source-app:3.2.1")
122122
},
123123
expectsPayload: &model.CreateAppVersionRequest{
124124
ApplicationKey: "app-key",
125125
Version: "1.0.0",
126+
Tag: "release-tag",
126127
Sources: &model.CreateVersionSources{
127-
Packages: []model.CreateVersionPackage{{
128-
Type: "type",
129-
Name: "name",
130-
Version: "1.0.0",
131-
Repository: "repo",
132-
}},
128+
Packages: []model.CreateVersionPackage{
129+
{Name: "pkg1", Version: "1.2.3"},
130+
{Name: "pkg2", Version: "2.3.4"},
131+
},
132+
Builds: nil,
133+
ReleaseBundles: nil,
134+
Versions: []model.CreateVersionReference{
135+
{ApplicationKey: "source-app", Version: "3.2.1"},
136+
},
133137
},
134138
},
135139
},
136140
{
137-
name: "minimum flags",
141+
name: "spec only",
138142
ctxSetup: func(ctx *components.Context) {
139143
ctx.Arguments = []string{"app-key", "1.0.0"}
140-
ctx.AddStringFlag("package-type", "type")
141-
ctx.AddStringFlag("package-name", "name")
142-
ctx.AddStringFlag("package-version", "1.0.0")
143-
ctx.AddStringFlag("package-repository", "repo")
144-
ctx.AddStringFlag("url", "https://example.com")
144+
ctx.AddStringFlag("spec", "/file1.txt")
145145
},
146-
expectsPayload: &model.CreateAppVersionRequest{
147-
ApplicationKey: "app-key",
148-
Version: "1.0.0",
149-
Sources: &model.CreateVersionSources{
150-
Packages: []model.CreateVersionPackage{{
151-
Type: "type",
152-
Name: "name",
153-
Version: "1.0.0",
154-
Repository: "repo",
155-
}},
156-
},
146+
expectsPayload: nil,
147+
expectsError: true,
148+
errorContains: "no such file or directory",
149+
},
150+
{
151+
name: "spec-vars only",
152+
ctxSetup: func(ctx *components.Context) {
153+
ctx.Arguments = []string{"app-key", "1.0.0"}
154+
ctx.AddStringFlag("spec-vars", "key1:val1,key2:val2")
155+
},
156+
expectsPayload: nil,
157+
expectsError: true,
158+
errorContains: "Missing source information",
159+
},
160+
{
161+
name: "empty flags",
162+
ctxSetup: func(ctx *components.Context) {
163+
ctx.Arguments = []string{"app-key", "1.0.0"}
157164
},
165+
expectsPayload: nil,
166+
expectsError: true,
167+
errorContains: "Missing source information",
158168
},
159169
}
160170

@@ -198,7 +208,7 @@ func TestParseBuilds(t *testing.T) {
198208
cmd := &createAppVersionCommand{}
199209

200210
// Test basic build parsing
201-
builds, err := cmd.parseBuilds("build1:1.0.0;build2:2.0.0")
211+
builds, err := cmd.parseBuilds("build1:1.0.0:2024-01-01;build2:2.0.0:2024-02-01")
202212
assert.NoError(t, err)
203213
assert.Len(t, builds, 2)
204214
assert.Equal(t, "build1", builds[0].Name)

0 commit comments

Comments
 (0)