Skip to content

Commit 4aac03a

Browse files
authored
feat: Add support for setting custom fields on releases (#545)
1 parent 15b801c commit 4aac03a

File tree

5 files changed

+118
-18
lines changed

5 files changed

+118
-18
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/AlecAivazis/survey/v2 v2.3.7
77
github.com/MakeNowJust/heredoc/v2 v2.0.1
88
github.com/OctopusDeploy/go-octodiff v1.0.0
9-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.81.0
9+
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.82.0
1010
github.com/bmatcuk/doublestar/v4 v4.4.0
1111
github.com/briandowns/spinner v1.19.0
1212
github.com/google/uuid v1.3.0

go.sum

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,8 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63n
4646
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
4747
github.com/OctopusDeploy/go-octodiff v1.0.0 h1:U+ORg6azniwwYo+O44giOw6TiD5USk8S4VDhOQ0Ven0=
4848
github.com/OctopusDeploy/go-octodiff v1.0.0/go.mod h1:Mze0+EkOWTgTmi8++fyUc6r0aLZT7qD9gX+31t8MmIU=
49-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.80.2 h1:fsCyBGYEE0hN2xLfc0/q3FP5GM5udioL0Gx3CyBdrJ4=
50-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.80.2/go.mod h1:ZCOnCz9ae/uuOk7AIQ9NzjnzFbuN8Q7H3oj2Eq4QSgQ=
51-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.81.0 h1:uw6MnuuAn4XRVavwlT8ZzgTuDktne0WVk3jEIfOl7eA=
52-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.81.0/go.mod h1:J1UdIilp41MRuFl+5xZm88ywFqJGYCCqxqod+/ZH8ko=
49+
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.82.0 h1:4Pc2W74VKp7Qm0uV0Dv99QKqRWg8WriVikdZPBpIZgY=
50+
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.82.0/go.mod h1:J1UdIilp41MRuFl+5xZm88ywFqJGYCCqxqod+/ZH8ko=
5351
github.com/bmatcuk/doublestar/v4 v4.4.0 h1:LmAwNwhjEbYtyVLzjcP/XeVw4nhuScHGkF/XWXnvIic=
5452
github.com/bmatcuk/doublestar/v4 v4.4.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
5553
github.com/briandowns/spinner v1.19.0 h1:s8aq38H+Qju89yhp89b4iIiMzMm8YN3p6vGpwyh/a8E=

pkg/cmd/release/create/create.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"io"
88
"os"
9+
"strings"
910
"time"
1011

1112
"github.com/OctopusDeploy/cli/pkg/apiclient"
@@ -39,6 +40,7 @@ const (
3940
FlagChannel = "channel"
4041
FlagPackageVersionSpec = "package"
4142
FlagGitResourceRefSpec = "git-resource"
43+
FlagCustomField = "custom-field"
4244

4345
FlagVersion = "version"
4446
FlagAliasReleaseNumberLegacy = "releaseNumber" // alias for FlagVersion
@@ -124,6 +126,7 @@ type CreateFlags struct {
124126
IgnoreChannelRules *flag.Flag[bool]
125127
PackageVersionSpec *flag.Flag[[]string]
126128
GitResourceRefsSpec *flag.Flag[[]string]
129+
CustomFields *flag.Flag[[]string]
127130
}
128131

129132
func NewCreateFlags() *CreateFlags {
@@ -140,6 +143,7 @@ func NewCreateFlags() *CreateFlags {
140143
IgnoreChannelRules: flag.New[bool](FlagIgnoreChannelRules, false),
141144
PackageVersionSpec: flag.New[[]string](FlagPackageVersionSpec, false),
142145
GitResourceRefsSpec: flag.New[[]string](FlagGitResourceRefSpec, false),
146+
CustomFields: flag.New[[]string](FlagCustomField, false),
143147
}
144148
}
145149

@@ -173,6 +177,7 @@ func NewCmdCreate(f factory.Factory) *cobra.Command {
173177
flags.BoolVarP(&createFlags.IgnoreChannelRules.Value, createFlags.IgnoreChannelRules.Name, "", false, "Allow creation of a release where channel rules would otherwise prevent it.")
174178
flags.StringArrayVarP(&createFlags.PackageVersionSpec.Value, createFlags.PackageVersionSpec.Name, "", []string{}, "Version specification for a specific package.\nFormat as {package}:{version}, {step}:{version} or {package-ref-name}:{packageOrStep}:{version}\nYou may specify this multiple times")
175179
flags.StringArrayVarP(&createFlags.GitResourceRefsSpec.Value, createFlags.GitResourceRefsSpec.Name, "", []string{}, "Git reference for a specific Git resource.\nFormat as {step}:{git-ref}, {step}:{git-resource-name}:{git-ref}\nYou may specify this multiple times")
180+
flags.StringArrayVarP(&createFlags.CustomFields.Value, createFlags.CustomFields.Name, "", []string{}, "Custom field value to set on the release.\nFormat as {name}:{value}. You may specify multiple times")
176181

177182
// we want the help text to display in the above order, rather than alphabetical
178183
flags.SortFlags = false
@@ -221,6 +226,24 @@ func createRun(cmd *cobra.Command, f factory.Factory, flags *CreateFlags) error
221226
GitResourceRefs: flags.GitResourceRefsSpec.Value,
222227
}
223228

229+
if len(flags.CustomFields.Value) > 0 {
230+
customFields := make(map[string]string)
231+
for _, raw := range flags.CustomFields.Value {
232+
// expect first ':' to split name and value; allow value to contain additional ':' characters
233+
parts := strings.SplitN(raw, ":", 2)
234+
if len(parts) != 2 {
235+
return fmt.Errorf("invalid custom-field value '%s'; expected format name:value", raw)
236+
}
237+
name := strings.TrimSpace(parts[0])
238+
value := strings.TrimSpace(parts[1])
239+
if name == "" {
240+
return fmt.Errorf("invalid custom-field value '%s'; field name cannot be empty", raw)
241+
}
242+
customFields[name] = value
243+
}
244+
options.CustomFields = customFields
245+
}
246+
224247
if flags.ReleaseNotesFile.Value != "" {
225248
fileContents, err := os.ReadFile(flags.ReleaseNotesFile.Value)
226249
if err != nil {
@@ -254,6 +277,11 @@ func createRun(cmd *cobra.Command, f factory.Factory, flags *CreateFlags) error
254277
resolvedFlags.ReleaseNotes.Value = options.ReleaseNotes
255278
resolvedFlags.IgnoreExisting.Value = options.IgnoreIfAlreadyExists
256279
resolvedFlags.IgnoreChannelRules.Value = options.IgnoreChannelRules
280+
if len(options.CustomFields) > 0 {
281+
for k, v := range options.CustomFields {
282+
resolvedFlags.CustomFields.Value = append(resolvedFlags.CustomFields.Value, fmt.Sprintf("%s: %s", k, v))
283+
}
284+
}
257285

258286
autoCmd := flag.GenerateAutomationCmd(constants.ExecutableName+" release create",
259287
resolvedFlags.Project,
@@ -265,6 +293,7 @@ func createRun(cmd *cobra.Command, f factory.Factory, flags *CreateFlags) error
265293
resolvedFlags.IgnoreChannelRules,
266294
resolvedFlags.PackageVersionSpec,
267295
resolvedFlags.GitResourceRefsSpec,
296+
resolvedFlags.CustomFields,
268297
resolvedFlags.Version,
269298
)
270299
cmd.Printf("\nAutomation Command: %s\n", autoCmd)
@@ -588,9 +617,46 @@ func AskQuestions(octopus *octopusApiClient.Client, stdout io.Writer, asker ques
588617
return err
589618
}
590619
}
620+
621+
if len(selectedChannel.CustomFieldDefinitions) > 0 {
622+
if options.CustomFields == nil { // ensure map initialised
623+
options.CustomFields = make(map[string]string, len(selectedChannel.CustomFieldDefinitions))
624+
}
625+
for _, customFieldDefinition := range selectedChannel.CustomFieldDefinitions {
626+
// skip if already provided via automation
627+
if _, exists := options.CustomFields[customFieldDefinition.FieldName]; exists {
628+
continue
629+
}
630+
631+
customFieldValue, err := askCustomField(customFieldDefinition, asker)
632+
if err != nil {
633+
return err
634+
}
635+
options.CustomFields[customFieldDefinition.FieldName] = customFieldValue
636+
}
637+
}
638+
591639
return nil
592640
}
593641

642+
func askCustomField(customFieldDefinition channels.ChannelCustomFieldDefinition, asker question.Asker) (string, error) {
643+
msg := fmt.Sprint(customFieldDefinition.FieldName)
644+
helpText := customFieldDefinition.Description
645+
var answer string
646+
647+
validator := func(val interface{}) error {
648+
str, _ := val.(string)
649+
if strings.TrimSpace(str) == "" {
650+
return fmt.Errorf("%s is required", customFieldDefinition.FieldName)
651+
}
652+
return nil
653+
}
654+
if err := asker(&survey.Input{Message: msg, Help: helpText}, &answer, survey.WithValidator(validator)); err != nil {
655+
return "", err
656+
}
657+
return answer, nil
658+
}
659+
594660
func askVersion(ask question.Asker, defaultVersion string) (string, error) {
595661
var result string
596662
if err := ask(&survey.Input{

pkg/cmd/release/create/create_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,6 +1287,36 @@ func TestReleaseCreate_AutomationMode(t *testing.T) {
12871287
assert.Equal(t, "", stdErr.String())
12881288
}},
12891289

1290+
{"release creation specifying custom field", func(t *testing.T, api *testutil.MockHttpServer, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) {
1291+
cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) {
1292+
defer api.Close()
1293+
rootCmd.SetArgs([]string{"release", "create", "--project", cacProject.Name, "--custom-field", "My Field: Some Value"})
1294+
return rootCmd.ExecuteC()
1295+
})
1296+
1297+
api.ExpectRequest(t, "GET", "/api/").RespondWith(rootResource)
1298+
api.ExpectRequest(t, "GET", "/api/Spaces-1").RespondWith(rootResource)
1299+
api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+cacProject.GetName()).RespondWith(cacProject)
1300+
1301+
req := api.ExpectRequest(t, "POST", "/api/Spaces-1/releases/create/v1")
1302+
1303+
requestBody, err := testutil.ReadJson[releases.CreateReleaseCommandV1](req.Request.Body)
1304+
assert.Nil(t, err)
1305+
1306+
assert.Equal(t, map[string]string{"My Field": "Some Value"}, requestBody.CustomFields)
1307+
1308+
req.RespondWith(&releases.CreateReleaseResponseV1{ReleaseID: "Releases-999", ReleaseVersion: "1.2.3"})
1309+
1310+
// follow-up lookups
1311+
releaseInfo := releases.NewRelease("Channels-32", cacProject.ID, "1.2.3")
1312+
api.ExpectRequest(t, "GET", "/api/Spaces-1/releases/Releases-999").RespondWith(releaseInfo)
1313+
channelInfo := fixtures.NewChannel(space1.ID, "Channels-32", "Alpha channel", cacProject.ID)
1314+
api.ExpectRequest(t, "GET", "/api/Spaces-1/channels/Channels-32").RespondWith(channelInfo)
1315+
1316+
_, err = testutil.ReceivePair(cmdReceiver)
1317+
assert.Nil(t, err)
1318+
}},
1319+
12901320
{"release creation specifying project only (bare minimum)", func(t *testing.T, api *testutil.MockHttpServer, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) {
12911321
cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) {
12921322
defer api.Close()

pkg/executor/release.go

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ package executor
33
import (
44
"errors"
55
"fmt"
6+
"strconv"
7+
"strings"
8+
69
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
710
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/deployments"
811
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/releases"
912
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces"
10-
"strconv"
11-
"strings"
1213
)
1314

1415
// ----- Create Release --------------------------------------
@@ -21,17 +22,18 @@ type TaskResultCreateRelease struct {
2122
// and looking them up for their ID's; we should only deal with strong references at this level
2223

2324
type TaskOptionsCreateRelease struct {
24-
ProjectName string // Required
25-
DefaultPackageVersion string // Optional
26-
GitCommit string // Optional
27-
GitReference string // Required for version controlled projects
28-
Version string // optional
29-
ChannelName string // optional
30-
ReleaseNotes string // optional
31-
IgnoreIfAlreadyExists bool // optional
32-
IgnoreChannelRules bool // optional
33-
PackageVersionOverrides []string // optional
34-
GitResourceRefs []string //optional
25+
ProjectName string // Required
26+
DefaultPackageVersion string // Optional
27+
GitCommit string // Optional
28+
GitReference string // Required for version controlled projects
29+
Version string // optional
30+
ChannelName string // optional
31+
ReleaseNotes string // optional
32+
IgnoreIfAlreadyExists bool // optional
33+
IgnoreChannelRules bool // optional
34+
PackageVersionOverrides []string // optional
35+
GitResourceRefs []string //optional
36+
CustomFields map[string]string // optional
3537
// if the task succeeds, the resulting output will be stored here
3638
Response *releases.CreateReleaseResponseV1
3739
}
@@ -74,6 +76,10 @@ func releaseCreate(octopus *client.Client, space *spaces.Space, input any) error
7476
createReleaseParams.IgnoreIfAlreadyExists = params.IgnoreIfAlreadyExists
7577
createReleaseParams.IgnoreChannelRules = params.IgnoreChannelRules
7678

79+
if len(params.CustomFields) > 0 {
80+
createReleaseParams.CustomFields = params.CustomFields
81+
}
82+
7783
createReleaseResponse, err := releases.CreateReleaseV1(octopus, createReleaseParams)
7884
if err != nil {
7985
return err

0 commit comments

Comments
 (0)