Skip to content

Commit dc8e50e

Browse files
authored
Update application version command (#30)
1 parent d7cdd81 commit dc8e50e

File tree

8 files changed

+579
-5
lines changed

8 files changed

+579
-5
lines changed

apptrust/commands/flags.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const (
1313
VersionPromote = "version-promote"
1414
VersionDelete = "version-delete"
1515
VersionRelease = "version-release"
16+
VersionUpdate = "version-update"
1617
PackageBind = "package-bind"
1718
PackageUnbind = "package-unbind"
1819
AppCreate = "app-create"
@@ -54,6 +55,8 @@ const (
5455
ReleaseBundlesFlag = "release-bundles"
5556
SourceVersionFlag = "source-version"
5657
PackagesFlag = "packages"
58+
PropertiesFlag = "properties"
59+
DeletePropertyFlag = "delete-property"
5760
)
5861

5962
// Flag keys mapped to their corresponding components.Flag definition.
@@ -87,11 +90,13 @@ var flagsMap = map[string]components.Flag{
8790
ExcludeReposFlag: components.NewStringFlag(ExcludeReposFlag, "Semicolon-separated list of repositories to exclude.", func(f *components.StringFlag) { f.Mandatory = false }),
8891
IncludeReposFlag: components.NewStringFlag(IncludeReposFlag, "Semicolon-separated list of repositories to include.", func(f *components.StringFlag) { f.Mandatory = false }),
8992
PropsFlag: components.NewStringFlag(PropsFlag, "Semicolon-separated list of properties in the form of 'key1=value1;key2=value2;...' to be added to each artifact.", func(f *components.StringFlag) { f.Mandatory = false }),
90-
TagFlag: components.NewStringFlag(TagFlag, "A tag to associate with the version.", func(f *components.StringFlag) { f.Mandatory = false }),
93+
TagFlag: components.NewStringFlag(TagFlag, "A tag to associate with the version. Must contain only alphanumeric characters, hyphens (-), underscores (_), and dots (.).", func(f *components.StringFlag) { f.Mandatory = false }),
9194
BuildsFlag: components.NewStringFlag(BuildsFlag, "List of builds in format 'name1:number1[:timestamp1];name2:number2[:timestamp2]'", func(f *components.StringFlag) { f.Mandatory = false }),
9295
ReleaseBundlesFlag: components.NewStringFlag(ReleaseBundlesFlag, "List of release bundles in format 'name1:version1;name2:version2'", func(f *components.StringFlag) { f.Mandatory = false }),
9396
SourceVersionFlag: components.NewStringFlag(SourceVersionFlag, "Source versions in format 'app1:version1;app2:version2'", func(f *components.StringFlag) { f.Mandatory = false }),
9497
PackagesFlag: components.NewStringFlag(PackagesFlag, "List of packages in format 'name1;name2'", func(f *components.StringFlag) { f.Mandatory = false }),
98+
PropertiesFlag: components.NewStringFlag(PropertiesFlag, "Sets or updates custom properties for the application version in format 'key1=value1[,value2,...];key2=value3[,value4,...]'", func(f *components.StringFlag) { f.Mandatory = false }),
99+
DeletePropertyFlag: components.NewStringFlag(DeletePropertyFlag, "Remove a property key and all its values", func(f *components.StringFlag) { f.Mandatory = false }),
95100
}
96101

97102
var commandFlags = map[string][]string{
@@ -140,6 +145,15 @@ var commandFlags = map[string][]string{
140145
accessToken,
141146
serverId,
142147
},
148+
VersionUpdate: {
149+
url,
150+
user,
151+
accessToken,
152+
serverId,
153+
TagFlag,
154+
PropertiesFlag,
155+
DeletePropertyFlag,
156+
},
143157

144158
PackageBind: {
145159
url,

apptrust/commands/utils/utils.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,48 @@ func ParseNameVersionPairs(input string) ([][2]string, error) {
136136
}
137137
return result, nil
138138
}
139+
140+
// ParseListPropertiesFlag parses a properties string into a map of keys to value slices.
141+
// Format: "key1=value1[,value2,...];key2=value3[,value4,...]"
142+
// Examples:
143+
// - "status=rc" -> {"status": ["rc"]}
144+
// - "status=rc,validated" -> {"status": ["rc", "validated"]}
145+
// - "status=rc;deployed_to=staging" -> {"status": ["rc"], "deployed_to": ["staging"]}
146+
// - "old_flag=" -> {"old_flag": []} (clears values)
147+
func ParseListPropertiesFlag(propertiesStr string) (map[string][]string, error) {
148+
if propertiesStr == "" {
149+
return nil, nil
150+
}
151+
152+
result := make(map[string][]string)
153+
pairs := strings.Split(propertiesStr, ";")
154+
155+
for _, pair := range pairs {
156+
keyValue := strings.SplitN(strings.TrimSpace(pair), "=", 2)
157+
if len(keyValue) != 2 {
158+
return nil, errorutils.CheckErrorf("invalid property format: \"%s\" (expected key=value1[,value2,...])", pair)
159+
}
160+
161+
key := strings.TrimSpace(keyValue[0])
162+
valuesStr := strings.TrimSpace(keyValue[1])
163+
164+
if key == "" {
165+
return nil, errorutils.CheckErrorf("property key cannot be empty")
166+
}
167+
168+
var values []string
169+
if valuesStr != "" {
170+
values = strings.Split(valuesStr, ",")
171+
for i, v := range values {
172+
values[i] = strings.TrimSpace(v)
173+
}
174+
} else {
175+
// Return empty slice instead of nil for empty values
176+
values = []string{}
177+
}
178+
// Always set the key, even with empty values (to clear values)
179+
result[key] = values
180+
}
181+
182+
return result, nil
183+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package version
2+
3+
//go:generate ${PROJECT_DIR}/scripts/mockgen.sh ${GOFILE}
4+
5+
import (
6+
"github.com/jfrog/jfrog-cli-application/apptrust/app"
7+
"github.com/jfrog/jfrog-cli-application/apptrust/commands"
8+
"github.com/jfrog/jfrog-cli-application/apptrust/commands/utils"
9+
"github.com/jfrog/jfrog-cli-application/apptrust/common"
10+
"github.com/jfrog/jfrog-cli-application/apptrust/model"
11+
"github.com/jfrog/jfrog-cli-application/apptrust/service"
12+
"github.com/jfrog/jfrog-cli-application/apptrust/service/versions"
13+
commonCLiCommands "github.com/jfrog/jfrog-cli-core/v2/common/commands"
14+
pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common"
15+
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
16+
coreConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config"
17+
"github.com/jfrog/jfrog-client-go/utils/errorutils"
18+
"github.com/jfrog/jfrog-client-go/utils/log"
19+
)
20+
21+
type updateAppVersionCommand struct {
22+
versionService versions.VersionService
23+
serverDetails *coreConfig.ServerDetails
24+
applicationKey string
25+
version string
26+
requestPayload *model.UpdateAppVersionRequest
27+
}
28+
29+
func (uv *updateAppVersionCommand) Run() error {
30+
log.Info("Updating application version:", uv.applicationKey, "version:", uv.version)
31+
32+
ctx, err := service.NewContext(*uv.serverDetails)
33+
if err != nil {
34+
log.Error("Failed to create service context:", err)
35+
return err
36+
}
37+
38+
err = uv.versionService.UpdateAppVersion(ctx, uv.applicationKey, uv.version, uv.requestPayload)
39+
if err != nil {
40+
log.Error("Failed to update application version:", err)
41+
return err
42+
}
43+
44+
log.Info("Successfully updated application version:", uv.applicationKey, "version:", uv.version)
45+
return nil
46+
}
47+
48+
func (uv *updateAppVersionCommand) ServerDetails() (*coreConfig.ServerDetails, error) {
49+
return uv.serverDetails, nil
50+
}
51+
52+
func (uv *updateAppVersionCommand) CommandName() string {
53+
return commands.VersionUpdate
54+
}
55+
56+
func (uv *updateAppVersionCommand) prepareAndRunCommand(ctx *components.Context) error {
57+
if len(ctx.Arguments) != 2 {
58+
return pluginsCommon.WrongNumberOfArgumentsHandler(ctx)
59+
}
60+
61+
if err := uv.parseFlagsAndSetFields(ctx); err != nil {
62+
return err
63+
}
64+
65+
var err error
66+
uv.requestPayload, err = uv.buildRequestPayload(ctx)
67+
if errorutils.CheckError(err) != nil {
68+
return err
69+
}
70+
71+
return commonCLiCommands.Exec(uv)
72+
}
73+
74+
// parseFlagsAndSetFields parses CLI flags and sets struct fields accordingly.
75+
func (uv *updateAppVersionCommand) parseFlagsAndSetFields(ctx *components.Context) error {
76+
uv.applicationKey = ctx.Arguments[0]
77+
uv.version = ctx.Arguments[1]
78+
79+
serverDetails, err := utils.ServerDetailsByFlags(ctx)
80+
if err != nil {
81+
return err
82+
}
83+
uv.serverDetails = serverDetails
84+
return nil
85+
}
86+
87+
func (uv *updateAppVersionCommand) buildRequestPayload(ctx *components.Context) (*model.UpdateAppVersionRequest, error) {
88+
request := &model.UpdateAppVersionRequest{}
89+
90+
if ctx.IsFlagSet(commands.TagFlag) {
91+
request.Tag = ctx.GetStringFlagValue(commands.TagFlag)
92+
}
93+
94+
// Handle properties - use spec format: key=value1[,value2,...]
95+
if ctx.IsFlagSet(commands.PropertiesFlag) {
96+
properties, err := utils.ParseListPropertiesFlag(ctx.GetStringFlagValue(commands.PropertiesFlag))
97+
if err != nil {
98+
return nil, err
99+
}
100+
request.Properties = properties
101+
}
102+
103+
// Handle delete properties
104+
if ctx.IsFlagSet(commands.DeletePropertyFlag) {
105+
deleteProps := utils.ParseSliceFlag(ctx.GetStringFlagValue(commands.DeletePropertyFlag))
106+
request.DeleteProperties = deleteProps
107+
}
108+
109+
return request, nil
110+
}
111+
112+
func GetUpdateAppVersionCommand(appContext app.Context) components.Command {
113+
cmd := &updateAppVersionCommand{versionService: appContext.GetVersionService()}
114+
return components.Command{
115+
Name: commands.VersionUpdate,
116+
Description: "Updates the user-defined annotations (tag and custom key-value properties) for a specified application version.",
117+
Category: common.CategoryVersion,
118+
Aliases: []string{"vu"},
119+
Arguments: []components.Argument{
120+
{
121+
Name: "app-key",
122+
Description: "The application key of the application for which the version is being updated.",
123+
Optional: false,
124+
},
125+
{
126+
Name: "version",
127+
Description: "The version number (in SemVer format) for the application version to update.",
128+
Optional: false,
129+
},
130+
},
131+
Flags: commands.GetCommandFlags(commands.VersionUpdate),
132+
Action: cmd.prepareAndRunCommand,
133+
}
134+
}

0 commit comments

Comments
 (0)