Skip to content

Commit 2e307e8

Browse files
authored
Add Version-Release command (#27)
1 parent 6d7e672 commit 2e307e8

11 files changed

+465
-61
lines changed

apptrust/commands/flags.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const (
1212
VersionCreate = "version-create"
1313
VersionPromote = "version-promote"
1414
VersionDelete = "version-delete"
15+
VersionRelease = "version-release"
1516
PackageBind = "package-bind"
1617
PackageUnbind = "package-unbind"
1718
AppCreate = "app-create"
@@ -47,6 +48,7 @@ const (
4748
DryRunFlag = "dry-run"
4849
ExcludeReposFlag = "exclude-repos"
4950
IncludeReposFlag = "include-repos"
51+
PropsFlag = "props"
5052
)
5153

5254
// Flag keys mapped to their corresponding components.Flag definition.
@@ -79,6 +81,7 @@ var flagsMap = map[string]components.Flag{
7981
DryRunFlag: components.NewBoolFlag(DryRunFlag, "Perform a simulation of the operation.", components.WithBoolDefaultValueFalse()),
8082
ExcludeReposFlag: components.NewStringFlag(ExcludeReposFlag, "Semicolon-separated list of repositories to exclude.", func(f *components.StringFlag) { f.Mandatory = false }),
8183
IncludeReposFlag: components.NewStringFlag(IncludeReposFlag, "Semicolon-separated list of repositories to include.", func(f *components.StringFlag) { f.Mandatory = false }),
84+
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 }),
8285
}
8386

8487
var commandFlags = map[string][]string{
@@ -105,6 +108,18 @@ var commandFlags = map[string][]string{
105108
DryRunFlag,
106109
ExcludeReposFlag,
107110
IncludeReposFlag,
111+
PropsFlag,
112+
},
113+
VersionRelease: {
114+
url,
115+
user,
116+
accessToken,
117+
serverId,
118+
SyncFlag,
119+
PromotionTypeFlag,
120+
ExcludeReposFlag,
121+
IncludeReposFlag,
122+
PropsFlag,
108123
},
109124
VersionDelete: {
110125
url,

apptrust/commands/version/promote_app_version_cmd.go

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -70,35 +70,24 @@ func (pv *promoteAppVersionCommand) prepareAndRunCommand(ctx *components.Context
7070
func (pv *promoteAppVersionCommand) buildRequestPayload(ctx *components.Context) (*model.PromoteAppVersionRequest, error) {
7171
stage := ctx.Arguments[2]
7272

73-
var includedRepos []string
74-
var excludedRepos []string
75-
76-
if includeReposStr := ctx.GetStringFlagValue(commands.IncludeReposFlag); includeReposStr != "" {
77-
includedRepos = utils.ParseSliceFlag(includeReposStr)
78-
}
79-
80-
if excludeReposStr := ctx.GetStringFlagValue(commands.ExcludeReposFlag); excludeReposStr != "" {
81-
excludedRepos = utils.ParseSliceFlag(excludeReposStr)
82-
}
83-
84-
// Validate promotion type flag
85-
promotionType := ctx.GetStringFlagValue(commands.PromotionTypeFlag)
86-
validatedPromotionType, err := utils.ValidateEnumFlag(commands.PromotionTypeFlag, promotionType, model.PromotionTypeCopy, model.PromotionTypeValues)
73+
promotionType, includedRepos, excludedRepos, err := BuildPromotionParams(ctx)
8774
if err != nil {
8875
return nil, err
8976
}
9077

91-
// If dry-run is true, override with dry_run
92-
dryRun := ctx.GetBoolFlagValue(commands.DryRunFlag)
93-
if dryRun {
94-
validatedPromotionType = model.PromotionTypeDryRun
78+
artifactProps, err := ParseArtifactProps(ctx)
79+
if err != nil {
80+
return nil, err
9581
}
9682

9783
return &model.PromoteAppVersionRequest{
98-
Stage: stage,
99-
PromotionType: validatedPromotionType,
100-
IncludedRepositoryKeys: includedRepos,
101-
ExcludedRepositoryKeys: excludedRepos,
84+
Stage: stage,
85+
CommonPromoteAppVersion: model.CommonPromoteAppVersion{
86+
PromotionType: promotionType,
87+
IncludedRepositoryKeys: includedRepos,
88+
ExcludedRepositoryKeys: excludedRepos,
89+
ArtifactAdditionalProperties: artifactProps,
90+
},
10291
}, nil
10392
}
10493

apptrust/commands/version/promote_app_version_cmd_test.go

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"errors"
55
"testing"
66

7-
"github.com/jfrog/jfrog-cli-application/apptrust/commands"
87
mockversions "github.com/jfrog/jfrog-cli-application/apptrust/service/versions/mocks"
98
"go.uber.org/mock/gomock"
109

@@ -89,25 +88,3 @@ func TestPromoteAppVersionCommand_Run_Error(t *testing.T) {
8988
assert.Error(t, err)
9089
assert.Contains(t, err.Error(), "service error occurred")
9190
}
92-
93-
func TestPromoteAppVersionCommand_ServerDetails(t *testing.T) {
94-
ctrl := gomock.NewController(t)
95-
defer ctrl.Finish()
96-
97-
serverDetails := &config.ServerDetails{}
98-
cmd := &promoteAppVersionCommand{
99-
serverDetails: serverDetails,
100-
}
101-
102-
details, err := cmd.ServerDetails()
103-
assert.NoError(t, err)
104-
assert.Equal(t, serverDetails, details)
105-
}
106-
107-
func TestPromoteAppVersionCommand_CommandName(t *testing.T) {
108-
ctrl := gomock.NewController(t)
109-
defer ctrl.Finish()
110-
111-
cmd := &promoteAppVersionCommand{}
112-
assert.Equal(t, commands.VersionPromote, cmd.CommandName())
113-
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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+
)
19+
20+
type releaseAppVersionCommand struct {
21+
versionService versions.VersionService
22+
serverDetails *coreConfig.ServerDetails
23+
applicationKey string
24+
version string
25+
requestPayload *model.ReleaseAppVersionRequest
26+
sync bool
27+
}
28+
29+
func (rv *releaseAppVersionCommand) Run() error {
30+
ctx, err := service.NewContext(*rv.serverDetails)
31+
if err != nil {
32+
return err
33+
}
34+
35+
return rv.versionService.ReleaseAppVersion(ctx, rv.applicationKey, rv.version, rv.requestPayload, rv.sync)
36+
}
37+
38+
func (rv *releaseAppVersionCommand) ServerDetails() (*coreConfig.ServerDetails, error) {
39+
return rv.serverDetails, nil
40+
}
41+
42+
func (rv *releaseAppVersionCommand) CommandName() string {
43+
return commands.VersionRelease
44+
}
45+
46+
func (rv *releaseAppVersionCommand) prepareAndRunCommand(ctx *components.Context) error {
47+
if len(ctx.Arguments) != 2 {
48+
return pluginsCommon.WrongNumberOfArgumentsHandler(ctx)
49+
}
50+
51+
// Extract from arguments
52+
rv.applicationKey = ctx.Arguments[0]
53+
rv.version = ctx.Arguments[1]
54+
55+
// Extract sync flag value
56+
rv.sync = ctx.GetBoolTFlagValue(commands.SyncFlag)
57+
58+
serverDetails, err := utils.ServerDetailsByFlags(ctx)
59+
if err != nil {
60+
return err
61+
}
62+
rv.serverDetails = serverDetails
63+
rv.requestPayload, err = rv.buildRequestPayload(ctx)
64+
if errorutils.CheckError(err) != nil {
65+
return err
66+
}
67+
return commonCLiCommands.Exec(rv)
68+
}
69+
70+
func (rv *releaseAppVersionCommand) buildRequestPayload(ctx *components.Context) (*model.ReleaseAppVersionRequest, error) {
71+
promotionType, includedRepos, excludedRepos, err := BuildPromotionParams(ctx)
72+
if err != nil {
73+
return nil, err
74+
}
75+
76+
artifactProps, err := ParseArtifactProps(ctx)
77+
if err != nil {
78+
return nil, err
79+
}
80+
81+
return model.NewReleaseAppVersionRequest(
82+
promotionType,
83+
includedRepos,
84+
excludedRepos,
85+
artifactProps,
86+
), nil
87+
}
88+
89+
func GetReleaseAppVersionCommand(appContext app.Context) components.Command {
90+
cmd := &releaseAppVersionCommand{
91+
versionService: appContext.GetVersionService(),
92+
}
93+
return components.Command{
94+
Name: commands.VersionRelease,
95+
Description: "Release application version.",
96+
Category: common.CategoryVersion,
97+
Aliases: []string{"vr"},
98+
Arguments: []components.Argument{
99+
{
100+
Name: "application-key",
101+
Description: "The application key.",
102+
Optional: false,
103+
},
104+
{
105+
Name: "version",
106+
Description: "The version to release.",
107+
Optional: false,
108+
},
109+
},
110+
Flags: commands.GetCommandFlags(commands.VersionRelease),
111+
Action: cmd.prepareAndRunCommand,
112+
}
113+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package version
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
mockversions "github.com/jfrog/jfrog-cli-application/apptrust/service/versions/mocks"
8+
"go.uber.org/mock/gomock"
9+
10+
"github.com/jfrog/jfrog-cli-application/apptrust/model"
11+
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
func TestReleaseAppVersionCommand_Run(t *testing.T) {
16+
tests := []struct {
17+
name string
18+
sync bool
19+
}{
20+
{
21+
name: "sync flag true",
22+
sync: true,
23+
},
24+
{
25+
name: "sync flag false",
26+
sync: false,
27+
},
28+
}
29+
30+
for _, tt := range tests {
31+
t.Run(tt.name, func(t *testing.T) {
32+
ctrl := gomock.NewController(t)
33+
defer ctrl.Finish()
34+
35+
serverDetails := &config.ServerDetails{Url: "https://example.com"}
36+
applicationKey := "app-key"
37+
version := "1.0.0"
38+
requestPayload := model.NewReleaseAppVersionRequest(
39+
model.PromotionTypeCopy,
40+
nil, // includedRepos
41+
nil, // excludedRepos
42+
nil, // artifactProps
43+
)
44+
45+
mockVersionService := mockversions.NewMockVersionService(ctrl)
46+
mockVersionService.EXPECT().ReleaseAppVersion(gomock.Any(), applicationKey, version, requestPayload, tt.sync).
47+
Return(nil).Times(1)
48+
49+
cmd := &releaseAppVersionCommand{
50+
versionService: mockVersionService,
51+
serverDetails: serverDetails,
52+
applicationKey: applicationKey,
53+
version: version,
54+
requestPayload: requestPayload,
55+
sync: tt.sync,
56+
}
57+
58+
err := cmd.Run()
59+
assert.NoError(t, err)
60+
})
61+
}
62+
}
63+
64+
func TestReleaseAppVersionCommand_Run_Error(t *testing.T) {
65+
ctrl := gomock.NewController(t)
66+
defer ctrl.Finish()
67+
68+
serverDetails := &config.ServerDetails{Url: "https://example.com"}
69+
applicationKey := "app-key"
70+
version := "1.0.0"
71+
requestPayload := model.NewReleaseAppVersionRequest(
72+
model.PromotionTypeCopy,
73+
nil, // includedRepos
74+
nil, // excludedRepos
75+
nil, // artifactProps
76+
)
77+
expectedError := errors.New("service error occurred")
78+
79+
mockVersionService := mockversions.NewMockVersionService(ctrl)
80+
mockVersionService.EXPECT().ReleaseAppVersion(gomock.Any(), applicationKey, version, requestPayload, false).
81+
Return(expectedError).Times(1)
82+
83+
cmd := &releaseAppVersionCommand{
84+
versionService: mockVersionService,
85+
serverDetails: serverDetails,
86+
applicationKey: applicationKey,
87+
version: version,
88+
requestPayload: requestPayload,
89+
sync: false,
90+
}
91+
92+
err := cmd.Run()
93+
assert.Error(t, err)
94+
assert.Contains(t, err.Error(), "service error occurred")
95+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package version
2+
3+
import (
4+
"github.com/jfrog/jfrog-cli-application/apptrust/commands"
5+
"github.com/jfrog/jfrog-cli-application/apptrust/commands/utils"
6+
"github.com/jfrog/jfrog-cli-application/apptrust/model"
7+
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
8+
"github.com/jfrog/jfrog-client-go/utils/errorutils"
9+
)
10+
11+
// BuildPromotionParams extracts common promotion parameters from command context
12+
// Used by both promote and release commands
13+
func BuildPromotionParams(ctx *components.Context) (string, []string, []string, error) {
14+
var includedRepos []string
15+
var excludedRepos []string
16+
17+
if includeReposStr := ctx.GetStringFlagValue(commands.IncludeReposFlag); includeReposStr != "" {
18+
includedRepos = utils.ParseSliceFlag(includeReposStr)
19+
}
20+
21+
if excludeReposStr := ctx.GetStringFlagValue(commands.ExcludeReposFlag); excludeReposStr != "" {
22+
excludedRepos = utils.ParseSliceFlag(excludeReposStr)
23+
}
24+
25+
promotionType := ctx.GetStringFlagValue(commands.PromotionTypeFlag)
26+
27+
validatedPromotionType, err := utils.ValidateEnumFlag(commands.PromotionTypeFlag, promotionType, model.PromotionTypeCopy, model.PromotionTypeValues)
28+
if err != nil {
29+
return "", nil, nil, err
30+
}
31+
32+
// If dry-run is true, override with dry_run
33+
dryRun := ctx.GetBoolFlagValue(commands.DryRunFlag)
34+
if dryRun {
35+
validatedPromotionType = model.PromotionTypeDryRun
36+
}
37+
38+
return validatedPromotionType, includedRepos, excludedRepos, nil
39+
}
40+
41+
// ParseArtifactProps extracts artifact properties from command context
42+
func ParseArtifactProps(ctx *components.Context) (map[string]string, error) {
43+
if propsStr := ctx.GetStringFlagValue(commands.PropsFlag); propsStr != "" {
44+
props, err := utils.ParseMapFlag(propsStr)
45+
if err != nil {
46+
return nil, errorutils.CheckErrorf("failed to parse properties: %s", err.Error())
47+
}
48+
return props, nil
49+
}
50+
return nil, nil
51+
}

0 commit comments

Comments
 (0)