diff --git a/apptrust/commands/application/application_utils.go b/apptrust/commands/application/application_utils.go index 0e016a5..901ebb8 100644 --- a/apptrust/commands/application/application_utils.go +++ b/apptrust/commands/application/application_utils.go @@ -92,5 +92,10 @@ func populateApplicationFromFlags(ctx *components.Context, descriptor *model.App descriptor.MonitorPolicy = monitorPolicy } + if ctx.IsFlagSet(commands.AutoPromoteStagesFlag) { + autoPromoteStages := utils.ParseSliceFlag(ctx.GetStringFlagValue(commands.AutoPromoteStagesFlag)) + descriptor.AutoPromoteStages = &autoPromoteStages + } + return nil } diff --git a/apptrust/commands/application/create_app_cmd.go b/apptrust/commands/application/create_app_cmd.go index d3489bb..28f5aa1 100644 --- a/apptrust/commands/application/create_app_cmd.go +++ b/apptrust/commands/application/create_app_cmd.go @@ -165,6 +165,7 @@ func validateNoSpecAndFlagsTogether(ctx *components.Context) error { commands.UserOwnersFlag, commands.GroupOwnersFlag, commands.MonitorPolicyFlag, + commands.AutoPromoteStagesFlag, } for _, flag := range otherAppFlags { if ctx.IsFlagSet(flag) { @@ -199,10 +200,11 @@ Common patterns: $ jf apptrust app-create my-app --project=default --business-criticality=high --maturity-level=production $ jf apptrust app-create my-app --project=default --labels="team=core;area=platform" --user-owners="alice;bob" $ jf apptrust app-create my-app --project=default --monitor-policy="type=version_count, value=5" + $ jf apptrust app-create my-app --project=default --auto-promote-stages="DEV;PROD" $ jf apptrust app-create my-app --spec=app-spec.json --spec-vars="ENV=prod" Gotchas: -- --spec is mutually exclusive with --application-name, --project, --desc, --business-criticality, --maturity-level, --labels, --user-owners, --group-owners, --monitor-policy. +- --spec is mutually exclusive with --application-name, --project, --desc, --business-criticality, --maturity-level, --labels, --user-owners, --group-owners, --monitor-policy, --auto-promote-stages. - If --application-name is omitted, the application-key is used as the display name. - --labels uses semicolon separators (not commas) and key=value pairs. - --monitor-policy takes 'type=[, value=]'. 'value' is required (positive integer) when type is "time_frame_in_months" or "version_count", and must be omitted when type is "none". diff --git a/apptrust/commands/application/create_app_cmd_test.go b/apptrust/commands/application/create_app_cmd_test.go index ac1ca7c..6fa6b3b 100644 --- a/apptrust/commands/application/create_app_cmd_test.go +++ b/apptrust/commands/application/create_app_cmd_test.go @@ -7,6 +7,7 @@ import ( "github.com/urfave/cli" + "github.com/jfrog/jfrog-cli-application/apptrust/commands" "github.com/jfrog/jfrog-cli-application/apptrust/model" mockapps "github.com/jfrog/jfrog-cli-application/apptrust/service/applications/mocks" "github.com/jfrog/jfrog-cli-core/v2/plugins/components" @@ -23,6 +24,7 @@ func TestCreateAppCommand_Run_Flags(t *testing.T) { businessCriticality := "high" maturityLevel := "production" monitorPolicyValue := 5 + autoPromoteStages := []string{"DEV", "PROD"} ctx := &components.Context{ Arguments: []string{"app-key"}, @@ -36,6 +38,7 @@ func TestCreateAppCommand_Run_Flags(t *testing.T) { ctx.AddStringFlag("user-owners", "john.doe;jane.smith") ctx.AddStringFlag("group-owners", "devops;security") ctx.AddStringFlag("monitor-policy", "type=version_count, value=5") + ctx.AddStringFlag("auto-promote-stages", "DEV;PROD") ctx.AddStringFlag("url", "https://example.com") requestPayload := &model.AppDescriptor{ @@ -49,9 +52,10 @@ func TestCreateAppCommand_Run_Flags(t *testing.T) { {Key: "env", Value: "prod"}, {Key: "region", Value: "us-east"}, }, - UserOwners: &[]string{"john.doe", "jane.smith"}, - GroupOwners: &[]string{"devops", "security"}, - MonitorPolicy: &model.MonitorPolicy{Type: model.MonitorPolicyTypeVersionCount, Value: &monitorPolicyValue}, + UserOwners: &[]string{"john.doe", "jane.smith"}, + GroupOwners: &[]string{"devops", "security"}, + MonitorPolicy: &model.MonitorPolicy{Type: model.MonitorPolicyTypeVersionCount, Value: &monitorPolicyValue}, + AutoPromoteStages: &autoPromoteStages, } mockAppService := mockapps.NewMockApplicationService(ctrl) @@ -145,6 +149,7 @@ func TestCreateAppCommand_Run_FullSpecFile(t *testing.T) { expectedDescription := "A comprehensive test application" expectedMaturityLevel := "production" expectedBusinessCriticality := "high" + expectedAutoPromoteStages := []string{"DEV", "PROD"} expectedPayload := &model.AppDescriptor{ ApplicationKey: "app-full", ApplicationName: "test-app-full", @@ -152,6 +157,7 @@ func TestCreateAppCommand_Run_FullSpecFile(t *testing.T) { Description: &expectedDescription, MaturityLevel: &expectedMaturityLevel, BusinessCriticality: &expectedBusinessCriticality, + AutoPromoteStages: &expectedAutoPromoteStages, Labels: &[]model.LabelEntry{ {Key: "environment", Value: "production"}, {Key: "environment", Value: "staging"}, @@ -337,3 +343,13 @@ func TestCreateAppCommand_Error_SpecAndFlags(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "the flag --project is not allowed when --spec is provided") } + +func TestCreateAppCommand_Error_SpecAndAutoPromoteStages(t *testing.T) { + ctx := &components.Context{} + ctx.AddStringFlag(commands.SpecFlag, "./testfiles/minimal-spec.json") + ctx.AddStringFlag(commands.AutoPromoteStagesFlag, "DEV;PROD") + + err := validateNoSpecAndFlagsTogether(ctx) + + assert.EqualError(t, err, "the flag --auto-promote-stages is not allowed when --spec is provided.") +} diff --git a/apptrust/commands/application/testfiles/full-spec.json b/apptrust/commands/application/testfiles/full-spec.json index 6c854cf..bcd5f5c 100644 --- a/apptrust/commands/application/testfiles/full-spec.json +++ b/apptrust/commands/application/testfiles/full-spec.json @@ -4,6 +4,10 @@ "description": "A comprehensive test application", "maturity_level": "production", "criticality": "high", + "auto_promote_stages": [ + "DEV", + "PROD" + ], "labels": [ { "key": "environment", diff --git a/apptrust/commands/application/update_app_cmd.go b/apptrust/commands/application/update_app_cmd.go index 0f667da..33d8632 100644 --- a/apptrust/commands/application/update_app_cmd.go +++ b/apptrust/commands/application/update_app_cmd.go @@ -93,7 +93,7 @@ func GetUpdateAppCommand(appContext app.Context) components.Command { return components.Command{ Name: commands.AppUpdate, Description: "Update an existing application", - AIDescription: `Update metadata (display name, description, criticality, maturity, labels, owners) of an existing application identified by its key. + AIDescription: `Update metadata (display name, description, criticality, maturity, labels, owners, auto-promotion stages) of an existing application identified by its key. When to use: - Change display attributes (name, description, criticality, maturity) of an existing application. @@ -110,11 +110,14 @@ Common patterns: $ jf apptrust app-update my-app --remove-labels="env=staging" $ jf apptrust app-update my-app --user-owners="alice;bob" --group-owners="platform-team" $ jf apptrust app-update my-app --monitor-policy="type=version_count, value=5" + $ jf apptrust app-update my-app --auto-promote-stages="DEV;PROD" + $ jf apptrust app-update my-app --auto-promote-stages="" Gotchas: - --labels replaces the full label set; --add-labels and --remove-labels modify incrementally. - --user-owners / --group-owners take a semicolon-separated list and send exactly the owners you specify; there are no incremental add/remove-owner flags (unlike --add-labels / --remove-labels for labels). - --monitor-policy takes 'type=[, value=]'. 'value' is required (positive integer) when type is "time_frame_in_months" or "version_count", and must be omitted when type is "none". When --monitor-policy is not provided, the current policy is left unchanged. +- --auto-promote-stages uses semicolon separators. Pass an empty value to disable auto-promotion; omit the flag to leave the current stages unchanged. - Application key cannot be changed; use app-delete and app-create if you need a different key. Related: jf apptrust app-create, jf apptrust app-delete`, diff --git a/apptrust/commands/application/update_app_cmd_test.go b/apptrust/commands/application/update_app_cmd_test.go index ccb7099..22d318d 100644 --- a/apptrust/commands/application/update_app_cmd_test.go +++ b/apptrust/commands/application/update_app_cmd_test.go @@ -268,6 +268,28 @@ func TestUpdateAppCommand_FlagsSuite(t *testing.T) { MonitorPolicy: &model.MonitorPolicy{Type: model.MonitorPolicyTypeVersionCount, Value: &monitorPolicyValue}, }, }, + { + name: "auto-promote-stages", + ctxSetup: func(ctx *components.Context) { + ctx.Arguments = []string{"app-key"} + ctx.AddStringFlag("auto-promote-stages", "DEV;PROD") + }, + expectsPayload: &model.AppDescriptor{ + ApplicationKey: "app-key", + AutoPromoteStages: &[]string{"DEV", "PROD"}, + }, + }, + { + name: "empty auto-promote-stages clears configuration", + ctxSetup: func(ctx *components.Context) { + ctx.Arguments = []string{"app-key"} + ctx.AddStringFlag("auto-promote-stages", "") + }, + expectsPayload: &model.AppDescriptor{ + ApplicationKey: "app-key", + AutoPromoteStages: &[]string{}, + }, + }, { name: "invalid add-labels format - missing equals", ctxSetup: func(ctx *components.Context) { diff --git a/apptrust/commands/flags.go b/apptrust/commands/flags.go index 6f2ad66..4a6ad7a 100644 --- a/apptrust/commands/flags.go +++ b/apptrust/commands/flags.go @@ -51,6 +51,7 @@ const ( UserOwnersFlag = "user-owners" GroupOwnersFlag = "group-owners" MonitorPolicyFlag = "monitor-policy" + AutoPromoteStagesFlag = "auto-promote-stages" SyncFlag = "sync" PromotionTypeFlag = "promotion-type" DryRunFlag = "dry-run" @@ -113,6 +114,7 @@ var flagsMap = map[string]components.Flag{ UserOwnersFlag: components.NewStringFlag(UserOwnersFlag, "semicolon-separated (;) list of user owners in the form of \"user1;user2;...\" (wrapped by quotes).", func(f *components.StringFlag) { f.Mandatory = false }), GroupOwnersFlag: components.NewStringFlag(GroupOwnersFlag, "semicolon-separated (;) list of group owners in the form of \"group1;group2;...\" (wrapped by quotes).", func(f *components.StringFlag) { f.Mandatory = false }), MonitorPolicyFlag: components.NewStringFlag(MonitorPolicyFlag, "Defines how long application versions remain monitored, in the form of 'type=[, value=]'. Supported types: "+coreutils.ListToText(model.MonitorPolicyTypeValues)+". For '"+model.MonitorPolicyTypeTimeframe+"' or '"+model.MonitorPolicyTypeVersionCount+"', 'value' is the number of months or versions to keep monitoring (a positive integer). For '"+model.MonitorPolicyTypeNone+"', monitoring is disabled and 'value' must be omitted.", func(f *components.StringFlag) { f.Mandatory = false }), + AutoPromoteStagesFlag: components.NewStringFlag(AutoPromoteStagesFlag, "Semicolon-separated (;) lifecycle stages through which new application versions are automatically promoted.", func(f *components.StringFlag) { f.Mandatory = false }), SyncFlag: components.NewBoolFlag(SyncFlag, "Whether to synchronize the operation.", components.WithBoolDefaultValueTrue()), PromotionTypeFlag: components.NewStringFlag(PromotionTypeFlag, "The promotion type. The following values are supported: "+coreutils.ListToText(model.PromotionTypeValues), func(f *components.StringFlag) { f.Mandatory = false; f.DefaultValue = model.PromotionTypeCopy }), DryRunFlag: components.NewBoolFlag(DryRunFlag, "Perform a simulation of the operation.", components.WithBoolDefaultValueFalse()), @@ -315,6 +317,7 @@ var commandFlags = map[string][]string{ UserOwnersFlag, GroupOwnersFlag, MonitorPolicyFlag, + AutoPromoteStagesFlag, SpecFlag, SpecVarsFlag, }, @@ -334,6 +337,7 @@ var commandFlags = map[string][]string{ UserOwnersFlag, GroupOwnersFlag, MonitorPolicyFlag, + AutoPromoteStagesFlag, }, AppDelete: { diff --git a/apptrust/common/keys.go b/apptrust/common/keys.go index 18025d5..f142927 100644 --- a/apptrust/common/keys.go +++ b/apptrust/common/keys.go @@ -20,4 +20,5 @@ var OrderedAppKeys = []string{ "description", "criticality", "maturity_level", + "auto_promote_stages", } diff --git a/apptrust/model/app_descriptor.go b/apptrust/model/app_descriptor.go index c7f18b3..fecba8d 100644 --- a/apptrust/model/app_descriptor.go +++ b/apptrust/model/app_descriptor.go @@ -67,4 +67,5 @@ type AppDescriptor struct { UserOwners *[]string `json:"user_owners,omitempty"` GroupOwners *[]string `json:"group_owners,omitempty"` MonitorPolicy *MonitorPolicy `json:"monitor_policy,omitempty"` + AutoPromoteStages *[]string `json:"auto_promote_stages,omitempty"` } diff --git a/apptrust/model/app_descriptor_test.go b/apptrust/model/app_descriptor_test.go new file mode 100644 index 0000000..480bffb --- /dev/null +++ b/apptrust/model/app_descriptor_test.go @@ -0,0 +1,29 @@ +package model + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAppDescriptorAutoPromoteStagesJSON(t *testing.T) { + t.Run("omitted when flag is not supplied", func(t *testing.T) { + payload, err := json.Marshal(AppDescriptor{ApplicationKey: "app-key"}) + require.NoError(t, err) + + assert.NotContains(t, string(payload), "auto_promote_stages") + }) + + t.Run("includes empty list to disable auto-promotion", func(t *testing.T) { + stages := []string{} + payload, err := json.Marshal(AppDescriptor{ + ApplicationKey: "app-key", + AutoPromoteStages: &stages, + }) + require.NoError(t, err) + + assert.JSONEq(t, `{"application_key":"app-key","auto_promote_stages":[]}`, string(payload)) + }) +}