Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion apptrust/commands/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const (
VersionPromote = "version-promote"
VersionDelete = "version-delete"
VersionRelease = "version-release"
VersionUpdate = "version-update"
PackageBind = "package-bind"
PackageUnbind = "package-unbind"
AppCreate = "app-create"
Expand Down Expand Up @@ -54,6 +55,8 @@ const (
ReleaseBundlesFlag = "release-bundles"
SourceVersionFlag = "source-version"
PackagesFlag = "packages"
PropertiesFlag = "properties"
DeletePropertyFlag = "delete-property"
)

// Flag keys mapped to their corresponding components.Flag definition.
Expand Down Expand Up @@ -87,11 +90,13 @@ var flagsMap = map[string]components.Flag{
ExcludeReposFlag: components.NewStringFlag(ExcludeReposFlag, "Semicolon-separated list of repositories to exclude.", func(f *components.StringFlag) { f.Mandatory = false }),
IncludeReposFlag: components.NewStringFlag(IncludeReposFlag, "Semicolon-separated list of repositories to include.", func(f *components.StringFlag) { f.Mandatory = false }),
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 }),
TagFlag: components.NewStringFlag(TagFlag, "A tag to associate with the version.", func(f *components.StringFlag) { f.Mandatory = false }),
TagFlag: components.NewStringFlag(TagFlag, "A tag to associate with the version. Must contain only alphanumeric characters, hyphens (-), underscores (_), and dots (.). Examples: 'release-1.2.3', 'v1.0.0', 'production_build'.", func(f *components.StringFlag) { f.Mandatory = false }),
BuildsFlag: components.NewStringFlag(BuildsFlag, "List of builds in format 'name1:number1[:timestamp1];name2:number2[:timestamp2]'", func(f *components.StringFlag) { f.Mandatory = false }),
ReleaseBundlesFlag: components.NewStringFlag(ReleaseBundlesFlag, "List of release bundles in format 'name1:version1;name2:version2'", func(f *components.StringFlag) { f.Mandatory = false }),
SourceVersionFlag: components.NewStringFlag(SourceVersionFlag, "Source versions in format 'app1:version1;app2:version2'", func(f *components.StringFlag) { f.Mandatory = false }),
PackagesFlag: components.NewStringFlag(PackagesFlag, "List of packages in format 'name1;name2'", func(f *components.StringFlag) { f.Mandatory = false }),
PropertiesFlag: components.NewStringFlag(PropertiesFlag, "Set or update a property: 'key=val1[,val2,...]'", func(f *components.StringFlag) { f.Mandatory = false }),
DeletePropertyFlag: components.NewStringFlag(DeletePropertyFlag, "Remove a property key and all its values", func(f *components.StringFlag) { f.Mandatory = false }),
}

var commandFlags = map[string][]string{
Expand Down Expand Up @@ -140,6 +145,15 @@ var commandFlags = map[string][]string{
accessToken,
serverId,
},
VersionUpdate: {
url,
user,
accessToken,
serverId,
TagFlag,
PropertiesFlag,
DeletePropertyFlag,
},

PackageBind: {
url,
Expand Down
42 changes: 42 additions & 0 deletions apptrust/commands/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,45 @@ func ParseNameVersionPairs(input string) ([][2]string, error) {
}
return result, nil
}

// ParsePropertiesFlag parses a properties string into a map of keys to value slices.
// Format: "key1=value1[,value2,...];key2=value3[,value4,...]"
// Examples:
// - "status=rc" -> {"status": ["rc"]}
// - "status=rc,validated" -> {"status": ["rc", "validated"]}
// - "status=rc;deployed_to=staging" -> {"status": ["rc"], "deployed_to": ["staging"]}
// - "old_flag=" -> {"old_flag": []} (clears values)
func ParsePropertiesFlag(propertiesStr string) (map[string][]string, error) {
if propertiesStr == "" {
return nil, nil
}

result := make(map[string][]string)
pairs := strings.Split(propertiesStr, ";")

for _, pair := range pairs {
keyValue := strings.SplitN(strings.TrimSpace(pair), "=", 2)
if len(keyValue) != 2 {
return nil, errorutils.CheckErrorf("invalid property format: \"%s\" (expected key=value1[,value2,...])", pair)
}

key := strings.TrimSpace(keyValue[0])
valuesStr := strings.TrimSpace(keyValue[1])

if key == "" {
return nil, errorutils.CheckErrorf("property key cannot be empty")
}

var values []string
if valuesStr != "" {
values = strings.Split(valuesStr, ",")
for i, v := range values {
values[i] = strings.TrimSpace(v)
}
}
// Always set the key, even with empty values (to clear values)
result[key] = values
}

return result, nil
}
79 changes: 79 additions & 0 deletions apptrust/commands/utils/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,82 @@ func TestParseNameVersionPairs(t *testing.T) {
})
}
}

func TestParsePropertiesFlag(t *testing.T) {
tests := []struct {
name string
input string
expected map[string][]string
expectErr bool
}{
{
name: "empty string",
input: "",
expected: nil,
},
{
name: "single property with single value",
input: "status=rc",
expected: map[string][]string{
"status": {"rc"},
},
},
{
name: "single property with multiple values",
input: "status=rc,validated",
expected: map[string][]string{
"status": {"rc", "validated"},
},
},
{
name: "multiple properties",
input: "status=rc,validated;deployed_to=staging-A,staging-B",
expected: map[string][]string{
"status": {"rc", "validated"},
"deployed_to": {"staging-A", "staging-B"},
},
},
{
name: "empty values (clears values)",
input: "old_feature_flag=",
expected: map[string][]string{
"old_feature_flag": nil,
},
},
{
name: "with spaces",
input: " status = rc , validated ; deployed_to = staging-A , staging-B ",
expected: map[string][]string{
"status": {"rc", "validated"},
"deployed_to": {"staging-A", "staging-B"},
},
},
{
name: "invalid format - missing =",
input: "invalid-format",
expectErr: true,
},
{
name: "empty key",
input: "=value",
expectErr: true,
},
{
name: "empty key with spaces",
input: " =value",
expectErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := ParsePropertiesFlag(tt.input)
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
}
})
}
}
137 changes: 137 additions & 0 deletions apptrust/commands/version/update_app_version_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package version

//go:generate ${PROJECT_DIR}/scripts/mockgen.sh ${GOFILE}

import (
"github.com/jfrog/jfrog-cli-application/apptrust/app"
"github.com/jfrog/jfrog-cli-application/apptrust/commands"
"github.com/jfrog/jfrog-cli-application/apptrust/commands/utils"
"github.com/jfrog/jfrog-cli-application/apptrust/common"
"github.com/jfrog/jfrog-cli-application/apptrust/model"
"github.com/jfrog/jfrog-cli-application/apptrust/service"
"github.com/jfrog/jfrog-cli-application/apptrust/service/versions"
commonCLiCommands "github.com/jfrog/jfrog-cli-core/v2/common/commands"
pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common"
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
coreConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config"
"github.com/jfrog/jfrog-client-go/utils/errorutils"
"github.com/jfrog/jfrog-client-go/utils/log"
)

type updateAppVersionCommand struct {
versionService versions.VersionService
serverDetails *coreConfig.ServerDetails
applicationKey string
version string
requestPayload *model.UpdateAppVersionRequest
}

func (uv *updateAppVersionCommand) Run() error {
log.Info("Updating application version:", uv.applicationKey, "version:", uv.version)

ctx, err := service.NewContext(*uv.serverDetails)
if err != nil {
log.Error("Failed to create service context:", err)
return err
}

err = uv.versionService.UpdateAppVersion(ctx, uv.requestPayload)
if err != nil {
log.Error("Failed to update application version:", err)
return err
}

log.Info("Successfully updated application version:", uv.applicationKey, "version:", uv.version)
return nil
}

func (uv *updateAppVersionCommand) ServerDetails() (*coreConfig.ServerDetails, error) {
return uv.serverDetails, nil
}

func (uv *updateAppVersionCommand) CommandName() string {
return commands.VersionUpdate
}

func (uv *updateAppVersionCommand) prepareAndRunCommand(ctx *components.Context) error {
if len(ctx.Arguments) != 2 {
return pluginsCommon.WrongNumberOfArgumentsHandler(ctx)
}

if err := uv.parseFlagsAndSetFields(ctx); err != nil {
return err
}

var err error
uv.requestPayload, err = uv.buildRequestPayload(ctx)
if errorutils.CheckError(err) != nil {
return err
}

return commonCLiCommands.Exec(uv)
}

// parseFlagsAndSetFields parses CLI flags and sets struct fields accordingly.
func (uv *updateAppVersionCommand) parseFlagsAndSetFields(ctx *components.Context) error {
uv.applicationKey = ctx.Arguments[0]
uv.version = ctx.Arguments[1]

serverDetails, err := utils.ServerDetailsByFlags(ctx)
if err != nil {
return err
}
uv.serverDetails = serverDetails
return nil
}

func (uv *updateAppVersionCommand) buildRequestPayload(ctx *components.Context) (*model.UpdateAppVersionRequest, error) {
request := &model.UpdateAppVersionRequest{
ApplicationKey: uv.applicationKey,
Version: uv.version,
}

if ctx.IsFlagSet(commands.TagFlag) {
request.Tag = ctx.GetStringFlagValue(commands.TagFlag)
}

// Handle properties - use spec format: key=value1[,value2,...]
if ctx.IsFlagSet(commands.PropertiesFlag) {
properties, err := utils.ParsePropertiesFlag(ctx.GetStringFlagValue(commands.PropertiesFlag))
if err != nil {
return nil, err
}
request.Properties = properties
}

// Handle delete properties
if ctx.IsFlagSet(commands.DeletePropertyFlag) {
deleteProps := utils.ParseSliceFlag(ctx.GetStringFlagValue(commands.DeletePropertyFlag))
request.DeleteProperties = deleteProps
}

return request, nil
}

func GetUpdateAppVersionCommand(appContext app.Context) components.Command {
cmd := &updateAppVersionCommand{versionService: appContext.GetVersionService()}
return components.Command{
Name: commands.VersionUpdate,
Description: "Updates the user-defined annotations (tag and custom key-value properties) for a specified application version.",
Category: common.CategoryVersion,
Aliases: []string{"vu"},
Arguments: []components.Argument{
{
Name: "app-key",
Description: "The application key of the application for which the version is being updated.",
Optional: false,
},
{
Name: "version",
Description: "The version number (in SemVer format) for the application version to update.",
Optional: false,
},
},
Flags: commands.GetCommandFlags(commands.VersionUpdate),
Action: cmd.prepareAndRunCommand,
}
}
Loading
Loading