Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions pkg/cmd/release/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"os"
"strings"
"time"

"github.com/OctopusDeploy/cli/pkg/apiclient"
Expand Down Expand Up @@ -39,6 +40,7 @@ const (
FlagChannel = "channel"
FlagPackageVersionSpec = "package"
FlagGitResourceRefSpec = "git-resource"
FlagCustomField = "custom-field"

FlagVersion = "version"
FlagAliasReleaseNumberLegacy = "releaseNumber" // alias for FlagVersion
Expand Down Expand Up @@ -124,6 +126,7 @@ type CreateFlags struct {
IgnoreChannelRules *flag.Flag[bool]
PackageVersionSpec *flag.Flag[[]string]
GitResourceRefsSpec *flag.Flag[[]string]
CustomFields *flag.Flag[[]string]
}

func NewCreateFlags() *CreateFlags {
Expand All @@ -140,6 +143,7 @@ func NewCreateFlags() *CreateFlags {
IgnoreChannelRules: flag.New[bool](FlagIgnoreChannelRules, false),
PackageVersionSpec: flag.New[[]string](FlagPackageVersionSpec, false),
GitResourceRefsSpec: flag.New[[]string](FlagGitResourceRefSpec, false),
CustomFields: flag.New[[]string](FlagCustomField, false),
}
}

Expand Down Expand Up @@ -173,6 +177,7 @@ func NewCmdCreate(f factory.Factory) *cobra.Command {
flags.BoolVarP(&createFlags.IgnoreChannelRules.Value, createFlags.IgnoreChannelRules.Name, "", false, "Allow creation of a release where channel rules would otherwise prevent it.")
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")
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")
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")

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

if len(flags.CustomFields.Value) > 0 {
customFields := make(map[string]string)
for _, raw := range flags.CustomFields.Value {
// expect first ':' to split name and value; allow value to contain additional ':' characters
parts := strings.SplitN(raw, ":", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid custom-field value '%s'; expected format name:value", raw)
}
name := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
if name == "" {
return fmt.Errorf("invalid custom-field value '%s'; field name cannot be empty", raw)
}
customFields[name] = value
}
options.CustomFields = customFields
}

if flags.ReleaseNotesFile.Value != "" {
fileContents, err := os.ReadFile(flags.ReleaseNotesFile.Value)
if err != nil {
Expand Down Expand Up @@ -254,6 +277,11 @@ func createRun(cmd *cobra.Command, f factory.Factory, flags *CreateFlags) error
resolvedFlags.ReleaseNotes.Value = options.ReleaseNotes
resolvedFlags.IgnoreExisting.Value = options.IgnoreIfAlreadyExists
resolvedFlags.IgnoreChannelRules.Value = options.IgnoreChannelRules
if len(options.CustomFields) > 0 {
for k, v := range options.CustomFields {
resolvedFlags.CustomFields.Value = append(resolvedFlags.CustomFields.Value, fmt.Sprintf("%s: %s", k, v))
}
}

autoCmd := flag.GenerateAutomationCmd(constants.ExecutableName+" release create",
resolvedFlags.Project,
Expand All @@ -265,6 +293,7 @@ func createRun(cmd *cobra.Command, f factory.Factory, flags *CreateFlags) error
resolvedFlags.IgnoreChannelRules,
resolvedFlags.PackageVersionSpec,
resolvedFlags.GitResourceRefsSpec,
resolvedFlags.CustomFields,
resolvedFlags.Version,
)
cmd.Printf("\nAutomation Command: %s\n", autoCmd)
Expand Down Expand Up @@ -588,9 +617,46 @@ func AskQuestions(octopus *octopusApiClient.Client, stdout io.Writer, asker ques
return err
}
}

if len(selectedChannel.CustomFieldDefinitions) > 0 {
if options.CustomFields == nil { // ensure map initialised
options.CustomFields = make(map[string]string, len(selectedChannel.CustomFieldDefinitions))
}
for _, customFieldDefinition := range selectedChannel.CustomFieldDefinitions {
// skip if already provided via automation
if _, exists := options.CustomFields[customFieldDefinition.FieldName]; exists {
continue
}

customFieldValue, err := askCustomField(customFieldDefinition, asker)
if err != nil {
return err
}
options.CustomFields[customFieldDefinition.FieldName] = customFieldValue
}
}

return nil
}

func askCustomField(customFieldDefinition channels.ChannelCustomFieldDefinition, asker question.Asker) (string, error) {
msg := fmt.Sprint(customFieldDefinition.FieldName)
helpText := customFieldDefinition.Description
var answer string

validator := func(val interface{}) error {
str, _ := val.(string)
if strings.TrimSpace(str) == "" {
return fmt.Errorf("%s is required", customFieldDefinition.FieldName)
}
return nil
}
if err := asker(&survey.Input{Message: msg, Help: helpText}, &answer, survey.WithValidator(validator)); err != nil {
return "", err
}
return answer, nil
}

func askVersion(ask question.Asker, defaultVersion string) (string, error) {
var result string
if err := ask(&survey.Input{
Expand Down
30 changes: 30 additions & 0 deletions pkg/cmd/release/create/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1287,6 +1287,36 @@ func TestReleaseCreate_AutomationMode(t *testing.T) {
assert.Equal(t, "", stdErr.String())
}},

{"release creation specifying custom field", func(t *testing.T, api *testutil.MockHttpServer, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) {
cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) {
defer api.Close()
rootCmd.SetArgs([]string{"release", "create", "--project", cacProject.Name, "--custom-field", "My Field: Some Value"})
return rootCmd.ExecuteC()
})

api.ExpectRequest(t, "GET", "/api/").RespondWith(rootResource)
api.ExpectRequest(t, "GET", "/api/Spaces-1").RespondWith(rootResource)
api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+cacProject.GetName()).RespondWith(cacProject)

req := api.ExpectRequest(t, "POST", "/api/Spaces-1/releases/create/v1")

requestBody, err := testutil.ReadJson[releases.CreateReleaseCommandV1](req.Request.Body)
assert.Nil(t, err)

assert.Equal(t, map[string]string{"My Field": "Some Value"}, requestBody.CustomFields)

req.RespondWith(&releases.CreateReleaseResponseV1{ReleaseID: "Releases-999", ReleaseVersion: "1.2.3"})

// follow-up lookups
releaseInfo := releases.NewRelease("Channels-32", cacProject.ID, "1.2.3")
api.ExpectRequest(t, "GET", "/api/Spaces-1/releases/Releases-999").RespondWith(releaseInfo)
channelInfo := fixtures.NewChannel(space1.ID, "Channels-32", "Alpha channel", cacProject.ID)
api.ExpectRequest(t, "GET", "/api/Spaces-1/channels/Channels-32").RespondWith(channelInfo)

_, err = testutil.ReceivePair(cmdReceiver)
assert.Nil(t, err)
}},

{"release creation specifying project only (bare minimum)", func(t *testing.T, api *testutil.MockHttpServer, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) {
cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) {
defer api.Close()
Expand Down
32 changes: 19 additions & 13 deletions pkg/executor/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import (
"errors"
"fmt"
"strconv"
"strings"

"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/deployments"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/releases"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces"
"strconv"
"strings"
)

// ----- Create Release --------------------------------------
Expand All @@ -21,17 +22,18 @@
// and looking them up for their ID's; we should only deal with strong references at this level

type TaskOptionsCreateRelease struct {
ProjectName string // Required
DefaultPackageVersion string // Optional
GitCommit string // Optional
GitReference string // Required for version controlled projects
Version string // optional
ChannelName string // optional
ReleaseNotes string // optional
IgnoreIfAlreadyExists bool // optional
IgnoreChannelRules bool // optional
PackageVersionOverrides []string // optional
GitResourceRefs []string //optional
ProjectName string // Required
DefaultPackageVersion string // Optional
GitCommit string // Optional
GitReference string // Required for version controlled projects
Version string // optional
ChannelName string // optional
ReleaseNotes string // optional
IgnoreIfAlreadyExists bool // optional
IgnoreChannelRules bool // optional
PackageVersionOverrides []string // optional
GitResourceRefs []string //optional
CustomFields map[string]string // optional
// if the task succeeds, the resulting output will be stored here
Response *releases.CreateReleaseResponseV1
}
Expand Down Expand Up @@ -74,6 +76,10 @@
createReleaseParams.IgnoreIfAlreadyExists = params.IgnoreIfAlreadyExists
createReleaseParams.IgnoreChannelRules = params.IgnoreChannelRules

if len(params.CustomFields) > 0 {
createReleaseParams.CustomFields = params.CustomFields

Check failure on line 80 in pkg/executor/release.go

View workflow job for this annotation

GitHub Actions / test

createReleaseParams.CustomFields undefined (type *releases.CreateReleaseCommandV1 has no field or method CustomFields)
}

createReleaseResponse, err := releases.CreateReleaseV1(octopus, createReleaseParams)
if err != nil {
return err
Expand Down
Loading