Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 (.).", 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, "Sets or updates custom properties for the application version in format 'key1=value1[,value2,...];key2=value3[,value4,...]'", 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
45 changes: 45 additions & 0 deletions apptrust/commands/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,48 @@ func ParseNameVersionPairs(input string) ([][2]string, error) {
}
return result, nil
}

// ParseListPropertiesFlag 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 ParseListPropertiesFlag(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)
}
} else {
// Return empty slice instead of nil for empty values
values = []string{}
}
// Always set the key, even with empty values (to clear values)
result[key] = values
}

return result, nil
}
134 changes: 134 additions & 0 deletions apptrust/commands/version/update_app_version_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
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.applicationKey, uv.version, 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{}

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.ParseListPropertiesFlag(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