Skip to content

Commit eda6038

Browse files
committed
add release cmd
1 parent b4270b9 commit eda6038

File tree

7 files changed

+389
-1
lines changed

7 files changed

+389
-1
lines changed

apptrust/commands/flags.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const (
1111
Ping = "ping"
1212
CreateAppVersion = "version-create"
1313
PromoteAppVersion = "version-promote"
14+
ReleaseAppVersion = "version-release"
1415
DeleteAppVersion = "version-delete"
1516
PackageBind = "package-bind"
1617
PackageUnbind = "package-unbind"
@@ -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.
@@ -75,10 +77,11 @@ var flagsMap = map[string]components.Flag{
7577
GroupOwnersFlag: components.NewStringFlag(GroupOwnersFlag, "Comma-separated list of group owners.", func(f *components.StringFlag) { f.Mandatory = false }),
7678
SigningKeyFlag: components.NewStringFlag(SigningKeyFlag, "The GPG/RSA key-pair name given in Artifactory.", func(f *components.StringFlag) { f.Mandatory = false }),
7779
SyncFlag: components.NewBoolFlag(SyncFlag, "Whether to synchronize the operation.", components.WithBoolDefaultValueTrue()),
78-
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 }),
80+
PromotionTypeFlag: components.NewStringFlag(PromotionTypeFlag, "The promotion type. The following values are supported: "+coreutils.ListToText(model.PromotionTypeValues)+" and dry_run for simulation", func(f *components.StringFlag) { f.Mandatory = false; f.DefaultValue = model.PromotionTypeCopy }),
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 format \"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{
@@ -106,6 +109,17 @@ var commandFlags = map[string][]string{
106109
ExcludeReposFlag,
107110
IncludeReposFlag,
108111
},
112+
ReleaseAppVersion: {
113+
url,
114+
user,
115+
accessToken,
116+
serverId,
117+
SyncFlag,
118+
PromotionTypeFlag,
119+
ExcludeReposFlag,
120+
IncludeReposFlag,
121+
PropsFlag,
122+
},
109123
DeleteAppVersion: {
110124
url,
111125
user,
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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.ReleaseAppVersion
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.GetBoolFlagValue(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+
var includedRepos []string
72+
var excludedRepos []string
73+
var artifactProps map[string]string
74+
75+
if includeReposStr := ctx.GetStringFlagValue(commands.IncludeReposFlag); includeReposStr != "" {
76+
includedRepos = utils.ParseSliceFlag(includeReposStr)
77+
}
78+
79+
if excludeReposStr := ctx.GetStringFlagValue(commands.ExcludeReposFlag); excludeReposStr != "" {
80+
excludedRepos = utils.ParseSliceFlag(excludeReposStr)
81+
}
82+
83+
if propsStr := ctx.GetStringFlagValue(commands.PropsFlag); propsStr != "" {
84+
var err error
85+
artifactProps, err = utils.ParseMapFlag(propsStr)
86+
if err != nil {
87+
return nil, err
88+
}
89+
}
90+
91+
// Validate promotion type flag
92+
promotionType := ctx.GetStringFlagValue(commands.PromotionTypeFlag)
93+
94+
// For validation, we need to add the dry_run option
95+
allowedValues := append([]string{}, model.PromotionTypeValues...)
96+
allowedValues = append(allowedValues, model.PromotionTypeDryRun)
97+
98+
validatedPromotionType, err := utils.ValidateEnumFlag(commands.PromotionTypeFlag, promotionType, model.PromotionTypeCopy, allowedValues)
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
return &model.ReleaseAppVersionRequest{
104+
PromotionType: validatedPromotionType,
105+
IncludedRepositoryKeys: includedRepos,
106+
ExcludedRepositoryKeys: excludedRepos,
107+
ArtifactAdditionalProperties: artifactProps,
108+
}, nil
109+
}
110+
111+
func GetReleaseAppVersionCommand(appContext app.Context) components.Command {
112+
cmd := &ReleaseAppVersionCommand{versionService: appContext.GetVersionService()}
113+
return components.Command{
114+
Name: commands.ReleaseAppVersion,
115+
Description: "Release application version",
116+
Category: common.CategoryVersion,
117+
Aliases: []string{"vr"},
118+
Arguments: []components.Argument{
119+
{
120+
Name: "application-key",
121+
Description: "The application key",
122+
Optional: false,
123+
},
124+
{
125+
Name: "version",
126+
Description: "The version to release",
127+
Optional: false,
128+
},
129+
},
130+
Flags: commands.GetCommandFlags(commands.ReleaseAppVersion),
131+
Action: cmd.prepareAndRunCommand,
132+
}
133+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package version
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/jfrog/jfrog-cli-application/apptrust/commands"
8+
mockversions "github.com/jfrog/jfrog-cli-application/apptrust/service/versions/mocks"
9+
"go.uber.org/mock/gomock"
10+
11+
"github.com/jfrog/jfrog-cli-application/apptrust/model"
12+
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
func TestReleaseAppVersionCommand_Run(t *testing.T) {
17+
tests := []struct {
18+
name string
19+
sync bool
20+
}{
21+
{
22+
name: "sync flag true",
23+
sync: true,
24+
},
25+
{
26+
name: "sync flag false",
27+
sync: false,
28+
},
29+
}
30+
31+
for _, tt := range tests {
32+
t.Run(tt.name, func(t *testing.T) {
33+
ctrl := gomock.NewController(t)
34+
defer ctrl.Finish()
35+
36+
serverDetails := &config.ServerDetails{Url: "https://example.com"}
37+
applicationKey := "app-key"
38+
version := "1.0.0"
39+
requestPayload := &model.ReleaseAppVersionRequest{
40+
PromotionType: model.PromotionTypeCopy,
41+
}
42+
43+
mockVersionService := mockversions.NewMockVersionService(ctrl)
44+
mockVersionService.EXPECT().ReleaseAppVersion(gomock.Any(), applicationKey, version, requestPayload, tt.sync).
45+
Return(nil).Times(1)
46+
47+
cmd := &ReleaseAppVersionCommand{
48+
versionService: mockVersionService,
49+
serverDetails: serverDetails,
50+
applicationKey: applicationKey,
51+
version: version,
52+
requestPayload: requestPayload,
53+
sync: tt.sync,
54+
}
55+
56+
err := cmd.Run()
57+
assert.NoError(t, err)
58+
})
59+
}
60+
}
61+
62+
func TestReleaseAppVersionCommand_Run_Error(t *testing.T) {
63+
ctrl := gomock.NewController(t)
64+
defer ctrl.Finish()
65+
66+
serverDetails := &config.ServerDetails{Url: "https://example.com"}
67+
applicationKey := "app-key"
68+
version := "1.0.0"
69+
requestPayload := &model.ReleaseAppVersionRequest{
70+
PromotionType: model.PromotionTypeCopy,
71+
}
72+
sync := true
73+
expectedError := errors.New("service error occurred")
74+
75+
mockVersionService := mockversions.NewMockVersionService(ctrl)
76+
mockVersionService.EXPECT().ReleaseAppVersion(gomock.Any(), applicationKey, version, requestPayload, sync).
77+
Return(expectedError).Times(1)
78+
79+
cmd := &ReleaseAppVersionCommand{
80+
versionService: mockVersionService,
81+
serverDetails: serverDetails,
82+
applicationKey: applicationKey,
83+
version: version,
84+
requestPayload: requestPayload,
85+
sync: sync,
86+
}
87+
88+
err := cmd.Run()
89+
assert.Error(t, err)
90+
assert.Contains(t, err.Error(), "service error occurred")
91+
}
92+
93+
func TestReleaseAppVersionCommand_ServerDetails(t *testing.T) {
94+
ctrl := gomock.NewController(t)
95+
defer ctrl.Finish()
96+
97+
serverDetails := &config.ServerDetails{}
98+
cmd := &ReleaseAppVersionCommand{
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 TestReleaseAppVersionCommand_CommandName(t *testing.T) {
108+
ctrl := gomock.NewController(t)
109+
defer ctrl.Finish()
110+
111+
cmd := &ReleaseAppVersionCommand{}
112+
assert.Equal(t, commands.ReleaseAppVersion, cmd.CommandName())
113+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package model
2+
3+
// ReleaseAppVersionRequest represents a request to release an app version to the release stage
4+
type ReleaseAppVersionRequest struct {
5+
PromotionType string `json:"promotion_type,omitempty"`
6+
IncludedRepositoryKeys []string `json:"included_repository_keys,omitempty"`
7+
ExcludedRepositoryKeys []string `json:"excluded_repository_keys,omitempty"`
8+
ArtifactAdditionalProperties map[string]string `json:"artifact_additional_properties,omitempty"`
9+
}

apptrust/service/versions/version_service.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
type VersionService interface {
1515
CreateAppVersion(ctx service.Context, request *model.CreateAppVersionRequest) error
1616
PromoteAppVersion(ctx service.Context, applicationKey string, version string, payload *model.PromoteAppVersionRequest, sync bool) error
17+
ReleaseAppVersion(ctx service.Context, applicationKey string, version string, request *model.ReleaseAppVersionRequest, sync bool) error
1718
DeleteAppVersion(ctx service.Context, applicationKey string, version string) error
1819
}
1920

@@ -52,6 +53,21 @@ func (vs *versionService) PromoteAppVersion(ctx service.Context, applicationKey,
5253
return nil
5354
}
5455

56+
func (vs *versionService) ReleaseAppVersion(ctx service.Context, applicationKey, version string, request *model.ReleaseAppVersionRequest, sync bool) error {
57+
endpoint := fmt.Sprintf("/v1/applications/%s/versions/%s/release", applicationKey, version)
58+
response, responseBody, err := ctx.GetHttpClient().Post(endpoint, request, map[string]string{"async": strconv.FormatBool(!sync)})
59+
if err != nil {
60+
return err
61+
}
62+
63+
if response.StatusCode >= 400 {
64+
return fmt.Errorf("failed to release app version. Status code: %d. \n%s",
65+
response.StatusCode, responseBody)
66+
}
67+
68+
return nil
69+
}
70+
5571
func (vs *versionService) DeleteAppVersion(ctx service.Context, applicationKey, version string) error {
5672
url := fmt.Sprintf("/v1/applications/%s/versions/%s", applicationKey, version)
5773
response, responseBody, err := ctx.GetHttpClient().Delete(url, nil)

0 commit comments

Comments
 (0)