-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Nested Commands & Project Group Create #109
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package cmd | ||
|
||
type NestedOpts interface { | ||
Commit() error | ||
GenerateAutomationCmd() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -2,8 +2,12 @@ package create | |||
|
||||
import ( | ||||
"fmt" | ||||
"io" | ||||
|
||||
"github.com/AlecAivazis/survey/v2" | ||||
"github.com/MakeNowJust/heredoc/v2" | ||||
"github.com/OctopusDeploy/cli/pkg/cmd" | ||||
projectGroupCreate "github.com/OctopusDeploy/cli/pkg/cmd/projectgroup/create" | ||||
"github.com/OctopusDeploy/cli/pkg/factory" | ||||
"github.com/OctopusDeploy/cli/pkg/output" | ||||
"github.com/OctopusDeploy/cli/pkg/question" | ||||
|
@@ -13,7 +17,6 @@ import ( | |||
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" | ||||
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces" | ||||
"github.com/spf13/cobra" | ||||
"io" | ||||
) | ||||
|
||||
const ( | ||||
|
@@ -38,6 +41,44 @@ type CreateOptions struct { | |||
Space *spaces.Space | ||||
NoPrompt bool | ||||
Ask question.Asker | ||||
CmdPath string | ||||
} | ||||
|
||||
func (co *CreateOptions) Commit() error { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the commit pattern for each command |
||||
lifecycle, err := co.Client.Lifecycles.GetByIDOrName(co.Lifecycle.Value) | ||||
if err != nil { | ||||
return err | ||||
} | ||||
|
||||
projectGroup, err := co.Client.ProjectGroups.GetByIDOrName(co.Group.Value) | ||||
if err != nil { | ||||
return err | ||||
} | ||||
|
||||
project := projects.NewProject(co.Name.Value, lifecycle.ID, projectGroup.ID) | ||||
project.Description = co.Description.Value | ||||
|
||||
createdProject, err := co.Client.Projects.Add(project) | ||||
if err != nil { | ||||
return err | ||||
} | ||||
|
||||
_, err = fmt.Fprintf(co.Out, "\nSuccessfully created project %s (%s), with lifecycle %s in project group %s.\n", createdProject.Name, createdProject.Slug, co.Lifecycle.Value, co.Group.Value) | ||||
if err != nil { | ||||
return err | ||||
} | ||||
|
||||
link := output.Bluef("%s/app#/%s/projects/%s", co.Host, co.Space.GetID(), createdProject.GetID()) | ||||
fmt.Fprintf(co.Out, "View this project on Octopus Deploy: %s\n", link) | ||||
|
||||
return nil | ||||
} | ||||
|
||||
func (co *CreateOptions) GenerateAutomationCmd() { | ||||
if !co.NoPrompt { | ||||
autoCmd := flag.GenerateAutomationCmd(co.CmdPath, co.Name, co.Description, co.Group, co.Lifecycle) | ||||
fmt.Fprintf(co.Out, "%s\n", autoCmd) | ||||
} | ||||
} | ||||
|
||||
func NewCreateFlags() *CreateFlags { | ||||
|
@@ -66,6 +107,7 @@ func NewCmdCreate(f factory.Factory) *cobra.Command { | |||
if err != nil { | ||||
return err | ||||
} | ||||
opts.CmdPath = cmd.CommandPath() | ||||
opts.Out = cmd.OutOrStdout() | ||||
opts.Client = client | ||||
opts.Host = f.GetCurrentHost() | ||||
|
@@ -77,52 +119,43 @@ func NewCmdCreate(f factory.Factory) *cobra.Command { | |||
} | ||||
|
||||
flags := cmd.Flags() | ||||
flags.StringVarP(&opts.CreateFlags.Name.Value, opts.CreateFlags.Name.Name, "n", "", "Name of the project") | ||||
flags.StringVarP(&opts.CreateFlags.Description.Value, opts.CreateFlags.Description.Name, "d", "", "Description of the project") | ||||
flags.StringVarP(&opts.CreateFlags.Group.Value, opts.CreateFlags.Group.Name, "g", "", "Project group of the project") | ||||
flags.StringVarP(&opts.CreateFlags.Lifecycle.Value, opts.CreateFlags.Lifecycle.Name, "l", "", "Lifecycle of the project") | ||||
flags.StringVarP(&opts.Name.Value, opts.Name.Name, "n", "", "Name of the project") | ||||
flags.StringVarP(&opts.Description.Value, opts.Description.Name, "d", "", "Description of the project") | ||||
flags.StringVarP(&opts.Group.Value, opts.Group.Name, "g", "", "Project group of the project") | ||||
flags.StringVarP(&opts.Lifecycle.Value, opts.Lifecycle.Name, "l", "", "Lifecycle of the project") | ||||
flags.SortFlags = false | ||||
|
||||
return cmd | ||||
} | ||||
|
||||
func createRun(opts *CreateOptions) error { | ||||
if !opts.NoPrompt { | ||||
if err := PromptMissing(opts); err != nil { | ||||
optsArray, err := PromptMissing(opts) | ||||
if err != nil { | ||||
return err | ||||
} | ||||
} | ||||
|
||||
lifecycle, err := opts.Client.Lifecycles.GetByIDOrName(opts.CreateFlags.Lifecycle.Value) | ||||
if err != nil { | ||||
return err | ||||
} | ||||
|
||||
projectGroup, err := opts.Client.ProjectGroups.GetByIDOrName(opts.CreateFlags.Group.Value) | ||||
if err != nil { | ||||
return err | ||||
} | ||||
|
||||
project := projects.NewProject(opts.CreateFlags.Name.Value, lifecycle.ID, projectGroup.ID) | ||||
project.Description = opts.CreateFlags.Description.Value | ||||
for _, o := range optsArray { | ||||
if err := o.Commit(); err != nil { | ||||
return err | ||||
} | ||||
} | ||||
|
||||
createdProject, err := opts.Client.Projects.Add(project) | ||||
if err != nil { | ||||
return err | ||||
} | ||||
if !opts.NoPrompt { | ||||
fmt.Fprintln(opts.Out, "\nAutomation Commands:") | ||||
for _, o := range optsArray { | ||||
o.GenerateAutomationCmd() | ||||
} | ||||
} | ||||
|
||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||
_, err = fmt.Fprintf(opts.Out, "Successfully created project %s (%s), with lifecycle %s in project group %s.\n", createdProject.Name, createdProject.Slug, opts.Lifecycle.Value, opts.Group.Value) | ||||
if err != nil { | ||||
return err | ||||
} | ||||
|
||||
link := output.Bluef("%s/app#/%s/projects/%s", opts.Host, opts.Space.GetID(), createdProject.GetID()) | ||||
fmt.Fprintf(opts.Out, "\nView this project on Octopus Deploy: %s\n", link) | ||||
|
||||
return nil | ||||
} | ||||
|
||||
func PromptMissing(opts *CreateOptions) error { | ||||
func PromptMissing(opts *CreateOptions) ([]cmd.NestedOpts, error) { | ||||
nestedOpts := []cmd.NestedOpts{} | ||||
|
||||
if opts.Name.Value == "" { | ||||
if err := opts.Ask(&survey.Input{ | ||||
Message: "Name", | ||||
|
@@ -132,25 +165,51 @@ func PromptMissing(opts *CreateOptions) error { | |||
survey.MinLength(1), | ||||
survey.Required, | ||||
))); err != nil { | ||||
return err | ||||
return nil, err | ||||
} | ||||
} | ||||
|
||||
if opts.Lifecycle.Value == "" { | ||||
lc, err := selectors.Lifecycle("You have not specified a Lifecycle for this project. Please select one:", opts.Client, opts.Ask) | ||||
if err != nil { | ||||
return err | ||||
return nil, err | ||||
} | ||||
opts.Lifecycle.Value = lc.Name | ||||
} | ||||
|
||||
if opts.Group.Value == "" { | ||||
g, err := selectors.ProjectGroup("You have not specified a Project group for this project. Please select one:", opts.Client, opts.Ask) | ||||
if err != nil { | ||||
return err | ||||
var shouldCreateNewProjectGroup bool | ||||
opts.Ask(&survey.Confirm{ | ||||
Message: "Would you like to create a new Project Group?", | ||||
Default: false, | ||||
}, &shouldCreateNewProjectGroup) | ||||
|
||||
if shouldCreateNewProjectGroup { | ||||
optValues := projectGroupCreate.NewCreateFlags() | ||||
projectGroupCreateOpts := projectGroupCreate.CreateOptions{ | ||||
Host: opts.Host, | ||||
Ask: opts.Ask, | ||||
Out: opts.Out, | ||||
CreateFlags: optValues, | ||||
Client: opts.Client, | ||||
Space: opts.Space, | ||||
NoPrompt: opts.NoPrompt, | ||||
CmdPath: "octopus project-group create", | ||||
ShowMessagePrefix: true, | ||||
} | ||||
projectGroupCreate.PromptMissing(&projectGroupCreateOpts) | ||||
opts.Group.Value = projectGroupCreateOpts.Name.Value | ||||
nestedOpts = append(nestedOpts, &projectGroupCreateOpts) | ||||
} else { | ||||
g, err := selectors.ProjectGroup("You have not specified a Project group for this project. Please select one:", opts.Client, opts.Ask) | ||||
if err != nil { | ||||
return nil, err | ||||
} | ||||
opts.Group.Value = g.Name | ||||
} | ||||
opts.Group.Value = g.Name | ||||
|
||||
} | ||||
|
||||
return nil | ||||
nestedOpts = append(nestedOpts, opts) | ||||
return nestedOpts, nil | ||||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
package create | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
|
||
"github.com/AlecAivazis/survey/v2" | ||
"github.com/MakeNowJust/heredoc/v2" | ||
"github.com/OctopusDeploy/cli/pkg/factory" | ||
"github.com/OctopusDeploy/cli/pkg/output" | ||
"github.com/OctopusDeploy/cli/pkg/question" | ||
"github.com/OctopusDeploy/cli/pkg/util/flag" | ||
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" | ||
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projectgroups" | ||
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
const ( | ||
FlagName = "name" | ||
FlagDescription = "description" | ||
) | ||
|
||
type CreateFlags struct { | ||
Name *flag.Flag[string] | ||
Description *flag.Flag[string] | ||
} | ||
|
||
type CreateOptions struct { | ||
*CreateFlags | ||
Out io.Writer | ||
Client *client.Client | ||
Host string | ||
Space *spaces.Space | ||
NoPrompt bool | ||
Ask question.Asker | ||
CmdPath string | ||
ShowMessagePrefix bool | ||
} | ||
|
||
func (co *CreateOptions) Commit() error { | ||
projectGroup := projectgroups.NewProjectGroup(co.Name.Value) | ||
projectGroup.Description = co.Description.Value | ||
|
||
createdGroupProject, err := co.Client.ProjectGroups.Add(projectGroup) | ||
if err != nil { | ||
return err | ||
} | ||
_, err = fmt.Fprintf(co.Out, "\nSuccessfully created project group %s.\n", createdGroupProject.Name) | ||
if err != nil { | ||
return err | ||
} | ||
link := output.Bluef("%s/app#/%s/projectGroups/%s", co.Host, co.Space.GetID(), createdGroupProject.GetID()) | ||
fmt.Fprintf(co.Out, "View this project group on Octopus Deploy: %s\n", link) | ||
return nil | ||
} | ||
|
||
func (co *CreateOptions) GenerateAutomationCmd() { | ||
if !co.NoPrompt { | ||
autoCmd := flag.GenerateAutomationCmd(co.CmdPath, co.Name, co.Description) | ||
fmt.Fprintf(co.Out, "%s\n", autoCmd) | ||
} | ||
} | ||
|
||
func NewCreateFlags() *CreateFlags { | ||
return &CreateFlags{ | ||
Name: flag.New[string](FlagName, false), | ||
Description: flag.New[string](FlagDescription, false), | ||
} | ||
} | ||
|
||
func NewCmdCreate(f factory.Factory) *cobra.Command { | ||
opts := &CreateOptions{ | ||
Ask: f.Ask, | ||
CreateFlags: NewCreateFlags(), | ||
} | ||
cmd := &cobra.Command{ | ||
Use: "create", | ||
Short: "Creates a new project group in Octopus Deploy", | ||
Long: "Creates a new project group in Octopus Deploy.", | ||
Example: heredoc.Doc(` | ||
$ octopus project-group create | ||
`), | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
client, err := f.GetSpacedClient() | ||
if err != nil { | ||
return err | ||
} | ||
opts.CmdPath = cmd.CommandPath() | ||
opts.Out = cmd.OutOrStdout() | ||
opts.Client = client | ||
opts.Host = f.GetCurrentHost() | ||
opts.NoPrompt = !f.IsPromptEnabled() | ||
opts.Space = f.GetCurrentSpace() | ||
|
||
return createRun(opts) | ||
}, | ||
} | ||
|
||
flags := cmd.Flags() | ||
flags.StringVarP(&opts.Name.Value, opts.CreateFlags.Name.Name, "n", "", "Name of the project group") | ||
flags.StringVarP(&opts.Description.Value, opts.CreateFlags.Description.Name, "d", "", "Description of the project group") | ||
flags.SortFlags = false | ||
|
||
return cmd | ||
} | ||
|
||
func createRun(opts *CreateOptions) error { | ||
if !opts.NoPrompt { | ||
if err := PromptMissing(opts); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
if err := opts.Commit(); err != nil { | ||
return err | ||
} | ||
if !opts.NoPrompt { | ||
fmt.Fprint(opts.Out, "Automation Command: ") | ||
opts.GenerateAutomationCmd() | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func PromptMissing(opts *CreateOptions) error { | ||
messagePrefix := "" | ||
if opts.ShowMessagePrefix { | ||
messagePrefix = "Project Group " | ||
} | ||
|
||
if opts.Name.Value == "" { | ||
if err := opts.Ask(&survey.Input{ | ||
Message: messagePrefix + "Name", | ||
Help: "A short, memorable, unique name for this project group.", | ||
}, &opts.Name.Value, survey.WithValidator(survey.ComposeValidators( | ||
survey.MaxLength(200), | ||
survey.MinLength(1), | ||
survey.Required, | ||
))); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
if opts.Description.Value == "" { | ||
if err := opts.Ask(&survey.Input{ | ||
Message: messagePrefix + "Description", | ||
Help: "A short, memorable, unique name for this project.", | ||
}, &opts.Description.Value); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package projectgroup | ||
|
||
import ( | ||
"fmt" | ||
"github.com/MakeNowJust/heredoc/v2" | ||
createCmd "github.com/OctopusDeploy/cli/pkg/cmd/projectgroup/create" | ||
"github.com/OctopusDeploy/cli/pkg/constants" | ||
"github.com/OctopusDeploy/cli/pkg/constants/annotations" | ||
"github.com/OctopusDeploy/cli/pkg/factory" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
func NewCmdProjectGroup(f factory.Factory) *cobra.Command { | ||
cmd := &cobra.Command{ | ||
Use: "project-group <command>", | ||
Short: "Manage project groups", | ||
Long: `Work with Octopus Deploy project groups.`, | ||
Example: fmt.Sprintf(heredoc.Doc(` | ||
$ %s project-group list | ||
$ %s project-group ls | ||
`), constants.ExecutableName, constants.ExecutableName), | ||
Annotations: map[string]string{ | ||
annotations.IsCore: "true", | ||
}, | ||
} | ||
|
||
cmd.AddCommand(createCmd.NewCmdCreate(f)) | ||
|
||
return cmd | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does the same job as the
TaskExecutor
abstraction.The advantage this has is that it's simpler because you're sticking commands directly in the collection, rather than having a separation between gather input / perform action.
The disadvantage this has is that the questions are bound (via a closure) to the commit. You can't ask the question differently in a nested command that you could at the root level.
The reason I added that separation for TaskExecutor was to avoid circular package references, to help provide some code structure as the project grew, and to allow for nested questions to be asked differently, but share the underlying 'send to server' bit.
I don't mind that you've gone a different way here, but it would be good to understand.
If the team would like to go with this approach, then it obsoletes the TaskExecutor design. We should pay down our tech-debt and delete all that stuff.