diff --git a/apptrust/commands/flags.go b/apptrust/commands/flags.go index 13e9ba0..083b259 100644 --- a/apptrust/commands/flags.go +++ b/apptrust/commands/flags.go @@ -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" @@ -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. @@ -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{ @@ -140,6 +145,15 @@ var commandFlags = map[string][]string{ accessToken, serverId, }, + VersionUpdate: { + url, + user, + accessToken, + serverId, + TagFlag, + PropertiesFlag, + DeletePropertyFlag, + }, PackageBind: { url, diff --git a/apptrust/commands/utils/utils.go b/apptrust/commands/utils/utils.go index 2d19e7d..adf0fca 100644 --- a/apptrust/commands/utils/utils.go +++ b/apptrust/commands/utils/utils.go @@ -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 +} diff --git a/apptrust/commands/version/update_app_version_cmd.go b/apptrust/commands/version/update_app_version_cmd.go new file mode 100644 index 0000000..abebb18 --- /dev/null +++ b/apptrust/commands/version/update_app_version_cmd.go @@ -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, + } +} diff --git a/apptrust/commands/version/update_app_version_cmd_test.go b/apptrust/commands/version/update_app_version_cmd_test.go new file mode 100644 index 0000000..948cd26 --- /dev/null +++ b/apptrust/commands/version/update_app_version_cmd_test.go @@ -0,0 +1,234 @@ +package version + +import ( + "errors" + "testing" + + mockversions "github.com/jfrog/jfrog-cli-application/apptrust/service/versions/mocks" + "go.uber.org/mock/gomock" + + "github.com/jfrog/jfrog-cli-application/apptrust/commands" + "github.com/jfrog/jfrog-cli-application/apptrust/model" + "github.com/jfrog/jfrog-cli-core/v2/plugins/components" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/stretchr/testify/assert" +) + +func TestUpdateAppVersionCommand_Run(t *testing.T) { + tests := []struct { + name string + request *model.UpdateAppVersionRequest + shouldError bool + errorMessage string + }{ + { + name: "success", + request: &model.UpdateAppVersionRequest{ + Tag: "release/1.2.3", + Properties: map[string][]string{ + "status": {"rc", "validated"}, + }, + }, + }, + { + name: "context error", + request: &model.UpdateAppVersionRequest{ + Tag: "test-tag", + }, + shouldError: true, + errorMessage: "context error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockVersionService := mockversions.NewMockVersionService(ctrl) + if tt.shouldError { + mockVersionService.EXPECT().UpdateAppVersion(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(errors.New(tt.errorMessage)).Times(1) + } else { + mockVersionService.EXPECT().UpdateAppVersion(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil).Times(1) + } + + cmd := &updateAppVersionCommand{ + versionService: mockVersionService, + serverDetails: &config.ServerDetails{Url: "https://example.com"}, + applicationKey: "app-key", + version: "1.0.0", + requestPayload: tt.request, + } + + err := cmd.Run() + if tt.shouldError { + assert.Error(t, err) + assert.Equal(t, tt.errorMessage, err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestUpdateAppVersionCommand_FlagsSuite(t *testing.T) { + tests := []struct { + name string + ctxSetup func(*components.Context) + expectsError bool + errorContains string + expectsPayload *model.UpdateAppVersionRequest + }{ + { + name: "tag only", + ctxSetup: func(ctx *components.Context) { + ctx.Arguments = []string{"app-key", "1.0.0"} + ctx.AddStringFlag(commands.TagFlag, "release/1.2.3") + }, + expectsPayload: &model.UpdateAppVersionRequest{ + Tag: "release/1.2.3", + }, + }, + { + name: "properties only - single value", + ctxSetup: func(ctx *components.Context) { + ctx.Arguments = []string{"app-key", "1.0.0"} + ctx.AddStringFlag(commands.PropertiesFlag, "status=rc") + }, + expectsPayload: &model.UpdateAppVersionRequest{ + Properties: map[string][]string{ + "status": {"rc"}, + }, + }, + }, + { + name: "properties only - multiple values", + ctxSetup: func(ctx *components.Context) { + ctx.Arguments = []string{"app-key", "1.0.0"} + ctx.AddStringFlag(commands.PropertiesFlag, "status=rc,validated") + }, + expectsPayload: &model.UpdateAppVersionRequest{ + Properties: map[string][]string{ + "status": {"rc", "validated"}, + }, + }, + }, + { + name: "properties only - multiple properties", + ctxSetup: func(ctx *components.Context) { + ctx.Arguments = []string{"app-key", "1.0.0"} + ctx.AddStringFlag(commands.PropertiesFlag, "status=rc,validated;deployed_to=staging-A,staging-B") + }, + expectsPayload: &model.UpdateAppVersionRequest{ + Properties: map[string][]string{ + "status": {"rc", "validated"}, + "deployed_to": {"staging-A", "staging-B"}, + }, + }, + }, + { + name: "delete properties only", + ctxSetup: func(ctx *components.Context) { + ctx.Arguments = []string{"app-key", "1.0.0"} + ctx.AddStringFlag(commands.DeletePropertyFlag, "legacy_param;toBeDeleted") + }, + expectsPayload: &model.UpdateAppVersionRequest{ + DeleteProperties: []string{"legacy_param", "toBeDeleted"}, + }, + }, + { + name: "empty properties (clears values)", + ctxSetup: func(ctx *components.Context) { + ctx.Arguments = []string{"app-key", "1.0.0"} + ctx.AddStringFlag(commands.PropertiesFlag, "old_feature_flag=") + }, + expectsPayload: &model.UpdateAppVersionRequest{ + Properties: map[string][]string{ + "old_feature_flag": {}, + }, + }, + }, + { + name: "combined update", + ctxSetup: func(ctx *components.Context) { + ctx.Arguments = []string{"app-key", "1.0.0"} + ctx.AddStringFlag(commands.TagFlag, "release/1.2.3") + ctx.AddStringFlag(commands.PropertiesFlag, "status=rc,validated") + ctx.AddStringFlag(commands.DeletePropertyFlag, "old_param") + }, + expectsPayload: &model.UpdateAppVersionRequest{ + Tag: "release/1.2.3", + Properties: map[string][]string{ + "status": {"rc", "validated"}, + }, + DeleteProperties: []string{"old_param"}, + }, + }, + { + name: "empty tag (removes tag)", + ctxSetup: func(ctx *components.Context) { + ctx.Arguments = []string{"app-key", "1.0.0"} + ctx.AddStringFlag(commands.TagFlag, "") + }, + expectsPayload: &model.UpdateAppVersionRequest{ + Tag: "", + }, + }, + { + name: "invalid property format", + ctxSetup: func(ctx *components.Context) { + ctx.Arguments = []string{"app-key", "1.0.0"} + ctx.AddStringFlag(commands.PropertiesFlag, "invalid-format") + }, + expectsError: true, + errorContains: "invalid property format", + }, + { + name: "empty property key", + ctxSetup: func(ctx *components.Context) { + ctx.Arguments = []string{"app-key", "1.0.0"} + ctx.AddStringFlag(commands.PropertiesFlag, "=value") + }, + expectsError: true, + errorContains: "property key cannot be empty", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := &components.Context{} + tt.ctxSetup(ctx) + ctx.AddStringFlag("url", "https://example.com") + + var actualPayload *model.UpdateAppVersionRequest + mockVersionService := mockversions.NewMockVersionService(ctrl) + if !tt.expectsError { + mockVersionService.EXPECT().UpdateAppVersion(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(_ interface{}, _ string, _ string, req *model.UpdateAppVersionRequest) error { + actualPayload = req + return nil + }).Times(1) + } + + cmd := &updateAppVersionCommand{ + versionService: mockVersionService, + } + + err := cmd.prepareAndRunCommand(ctx) + if tt.expectsError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectsPayload, actualPayload) + } + }) + } +} diff --git a/apptrust/model/update_app_version_request.go b/apptrust/model/update_app_version_request.go new file mode 100644 index 0000000..1d22f39 --- /dev/null +++ b/apptrust/model/update_app_version_request.go @@ -0,0 +1,7 @@ +package model + +type UpdateAppVersionRequest struct { + Tag string `json:"tag,omitempty"` + Properties map[string][]string `json:"properties,omitempty"` + DeleteProperties []string `json:"delete_properties,omitempty"` +} diff --git a/apptrust/service/versions/version_service.go b/apptrust/service/versions/version_service.go index 0eb27b5..a4578ab 100644 --- a/apptrust/service/versions/version_service.go +++ b/apptrust/service/versions/version_service.go @@ -4,6 +4,7 @@ package versions import ( "fmt" + "net/http" "strconv" "github.com/jfrog/jfrog-cli-application/apptrust/service" @@ -16,6 +17,7 @@ type VersionService interface { PromoteAppVersion(ctx service.Context, applicationKey string, version string, payload *model.PromoteAppVersionRequest, sync bool) error ReleaseAppVersion(ctx service.Context, applicationKey string, version string, request *model.ReleaseAppVersionRequest, sync bool) error DeleteAppVersion(ctx service.Context, applicationKey string, version string) error + UpdateAppVersion(ctx service.Context, applicationKey string, version string, request *model.UpdateAppVersionRequest) error } type versionService struct{} @@ -31,7 +33,7 @@ func (vs *versionService) CreateAppVersion(ctx service.Context, request *model.C return err } - if response.StatusCode != 201 { + if response.StatusCode != http.StatusCreated { return fmt.Errorf("failed to create app version. Status code: %d. \n%s", response.StatusCode, responseBody) } @@ -46,7 +48,7 @@ func (vs *versionService) PromoteAppVersion(ctx service.Context, applicationKey, return err } - if response.StatusCode >= 400 { + if response.StatusCode >= http.StatusBadRequest { return fmt.Errorf("failed to promote app version. Status code: %d. \n%s", response.StatusCode, responseBody) } @@ -61,7 +63,7 @@ func (vs *versionService) ReleaseAppVersion(ctx service.Context, applicationKey, return err } - if response.StatusCode >= 400 { + if response.StatusCode >= http.StatusBadRequest { return fmt.Errorf("failed to release app version. Status code: %d. \n%s", response.StatusCode, responseBody) } @@ -76,10 +78,25 @@ func (vs *versionService) DeleteAppVersion(ctx service.Context, applicationKey, return err } - if response.StatusCode != 204 { + if response.StatusCode != http.StatusNoContent { return fmt.Errorf("failed to delete app version. Status code: %d.\n%s", response.StatusCode, responseBody) } return nil } + +func (vs *versionService) UpdateAppVersion(ctx service.Context, applicationKey string, version string, request *model.UpdateAppVersionRequest) error { + endpoint := fmt.Sprintf("/v1/applications/%s/versions/%s", applicationKey, version) + response, responseBody, err := ctx.GetHttpClient().Patch(endpoint, request) + if err != nil { + return err + } + + if response.StatusCode != http.StatusAccepted { + return fmt.Errorf("failed to update app version. Status code: %d. \n%s", + response.StatusCode, responseBody) + } + + return nil +} diff --git a/apptrust/service/versions/version_service_test.go b/apptrust/service/versions/version_service_test.go index d49c6de..8499f66 100644 --- a/apptrust/service/versions/version_service_test.go +++ b/apptrust/service/versions/version_service_test.go @@ -293,3 +293,125 @@ func TestReleaseAppVersion(t *testing.T) { }) } } + +func TestUpdateAppVersion(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + service := NewVersionService() + + tests := []struct { + name string + request *model.UpdateAppVersionRequest + mockResponse *http.Response + mockResponseBody string + mockError error + expectError bool + errorMsg string + }{ + { + name: "success - tag only", + request: &model.UpdateAppVersionRequest{ + Tag: "release/1.2.3", + }, + mockResponse: &http.Response{StatusCode: http.StatusAccepted}, + mockResponseBody: "{}", + mockError: nil, + expectError: false, + errorMsg: "", + }, + { + name: "success - properties only", + request: &model.UpdateAppVersionRequest{ + Properties: map[string][]string{ + "status": {"rc", "validated"}, + }, + }, + mockResponse: &http.Response{StatusCode: http.StatusAccepted}, + mockResponseBody: "{}", + mockError: nil, + expectError: false, + errorMsg: "", + }, + { + name: "success - delete properties only", + request: &model.UpdateAppVersionRequest{ + DeleteProperties: []string{"legacy_param", "toBeDeleted"}, + }, + mockResponse: &http.Response{StatusCode: http.StatusAccepted}, + mockResponseBody: "{}", + mockError: nil, + expectError: false, + errorMsg: "", + }, + { + name: "success - combined update", + request: &model.UpdateAppVersionRequest{ + Tag: "release/1.2.3", + Properties: map[string][]string{ + "status": {"rc", "validated"}, + }, + DeleteProperties: []string{"old_param"}, + }, + mockResponse: &http.Response{StatusCode: http.StatusAccepted}, + mockResponseBody: "{}", + mockError: nil, + expectError: false, + errorMsg: "", + }, + { + name: "failure - 400", + request: &model.UpdateAppVersionRequest{ + Tag: "invalid-tag", + }, + mockResponse: &http.Response{StatusCode: http.StatusBadRequest}, + mockResponseBody: "bad request", + mockError: nil, + expectError: true, + errorMsg: "failed to update app version", + }, + { + name: "failure - 404", + request: &model.UpdateAppVersionRequest{ + Tag: "release/1.2.3", + }, + mockResponse: &http.Response{StatusCode: http.StatusNotFound}, + mockResponseBody: "not found", + mockError: nil, + expectError: true, + errorMsg: "failed to update app version", + }, + { + name: "http client error", + request: &model.UpdateAppVersionRequest{ + Tag: "release/1.2.3", + }, + mockResponse: nil, + mockResponseBody: "", + mockError: errors.New("http client error"), + expectError: true, + errorMsg: "http client error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockHttpClient := mockhttp.NewMockApptrustHttpClient(ctrl) + mockHttpClient.EXPECT().Patch("/v1/applications/test-app/versions/1.0.0", tt.request). + Return(tt.mockResponse, []byte(tt.mockResponseBody), tt.mockError).Times(1) + + mockCtx := mockservice.NewMockContext(ctrl) + mockCtx.EXPECT().GetHttpClient().Return(mockHttpClient).AnyTimes() + + err := service.UpdateAppVersion(mockCtx, "test-app", "1.0.0", tt.request) + if tt.expectError { + assert.Error(t, err) + if tt.errorMsg != "" { + assert.Contains(t, err.Error(), tt.errorMsg) + } + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/cli/cli.go b/cli/cli.go index 01b6dc7..e5006a5 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -47,6 +47,7 @@ func GetJfrogCliApptrustApp() components.App { version.GetPromoteAppVersionCommand(appContext), version.GetReleaseAppVersionCommand(appContext), version.GetDeleteAppVersionCommand(appContext), + version.GetUpdateAppVersionCommand(appContext), packagecmds.GetBindPackageCommand(appContext), packagecmds.GetUnbindPackageCommand(appContext), application.GetCreateAppCommand(appContext),