Skip to content

Commit 2e2340a

Browse files
feat:Add support to view/add/update project tags (#567)
1 parent 82b8981 commit 2e2340a

9 files changed

Lines changed: 339 additions & 21 deletions

File tree

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.88.0
9+
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.89.1
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 & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +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.86.0 h1:yGoohDFkruQ13mxoK21J+U+IUkxoU5jSxGEpknW/E5Y=
50-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.86.0/go.mod h1:VkTXDoIPbwGFi5+goo1VSwFNdMVo784cVtJdKIEvfus=
51-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.86.1-0.20251107005245-10b49a98c515 h1:7L4l7zszH5TKP14BXCzgqT86xmHPQ4vQSmrqHc4Bj7E=
52-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.86.1-0.20251107005245-10b49a98c515/go.mod h1:VkTXDoIPbwGFi5+goo1VSwFNdMVo784cVtJdKIEvfus=
53-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.88.0 h1:doBP2Xk11l3tNqJfrF0cYqX0df3UHIQiQCts1qAux+w=
54-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.88.0/go.mod h1:VkTXDoIPbwGFi5+goo1VSwFNdMVo784cVtJdKIEvfus=
49+
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.89.1 h1:EDo4CdA7jYZBHiaKu8nZRf9ndonJXC70OlU29+6KXKc=
50+
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.89.1/go.mod h1:VkTXDoIPbwGFi5+goo1VSwFNdMVo784cVtJdKIEvfus=
5551
github.com/bmatcuk/doublestar/v4 v4.4.0 h1:LmAwNwhjEbYtyVLzjcP/XeVw4nhuScHGkF/XWXnvIic=
5652
github.com/bmatcuk/doublestar/v4 v4.4.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
5753
github.com/briandowns/spinner v1.19.0 h1:s8aq38H+Qju89yhp89b4iIiMzMm8YN3p6vGpwyh/a8E=

pkg/cmd/project/create/create.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const (
2121
FlagDescription = "description"
2222
FlagLifecycle = "lifecycle"
2323
FlagConfigAsCode = "process-vcs"
24+
FlagTag = "tag"
2425
)
2526

2627
type CreateFlags struct {
@@ -29,6 +30,7 @@ type CreateFlags struct {
2930
Description *flag.Flag[string]
3031
Lifecycle *flag.Flag[string]
3132
ConfigAsCode *flag.Flag[bool]
33+
Tag *flag.Flag[[]string]
3234

3335
ProjectConvertFlags *convert.ConvertFlags
3436
}
@@ -40,6 +42,7 @@ func NewCreateFlags() *CreateFlags {
4042
Description: flag.New[string](FlagDescription, false),
4143
Lifecycle: flag.New[string](FlagLifecycle, false),
4244
ConfigAsCode: flag.New[bool](FlagConfigAsCode, false),
45+
Tag: flag.New[[]string](FlagTag, false),
4346
ProjectConvertFlags: convert.NewConvertFlags(),
4447
}
4548
}
@@ -69,6 +72,7 @@ func NewCmdCreate(f factory.Factory) *cobra.Command {
6972
flags.StringVarP(&createFlags.Group.Value, createFlags.Group.Name, "g", "", "Project group of the project")
7073
flags.StringVarP(&createFlags.Lifecycle.Value, createFlags.Lifecycle.Name, "l", "", "Lifecycle of the project")
7174
flags.BoolVar(&createFlags.ConfigAsCode.Value, createFlags.ConfigAsCode.Name, false, "Use Config As Code for the project")
75+
flags.StringArrayVarP(&createFlags.Tag.Value, createFlags.Tag.Name, "t", []string{}, "Tag to apply to project, must use canonical name: <tag_set>/<tag_name>")
7276
convert.RegisterCacFlags(flags, createFlags.ProjectConvertFlags)
7377
flags.SortFlags = false
7478

@@ -84,6 +88,17 @@ func createRun(opts *CreateOptions) error {
8488
return err
8589
}
8690
} else {
91+
// Validate tags when running with --no-prompt
92+
if len(opts.Tag.Value) > 0 {
93+
tagSets, err := opts.GetAllTagsCallback()
94+
if err != nil {
95+
return err
96+
}
97+
if err := selectors.ValidateTags(opts.Tag.Value, tagSets); err != nil {
98+
return err
99+
}
100+
}
101+
87102
optsArray = append(optsArray, opts)
88103
if opts.ConfigAsCode.Value {
89104
opts.ConvertOptions.Project.Value = opts.Name.Value
@@ -131,6 +146,17 @@ func PromptMissing(opts *CreateOptions) ([]cmd.Dependable, error) {
131146

132147
nestedOpts = append(nestedOpts, opts)
133148

149+
tagSets, err := opts.GetAllTagsCallback()
150+
if err != nil {
151+
return nil, err
152+
}
153+
154+
tags, err := selectors.Tags(opts.Ask, []string{}, opts.Tag.Value, tagSets)
155+
if err != nil {
156+
return nil, err
157+
}
158+
opts.Tag.Value = tags
159+
134160
configAsCodeOpts, err := PromptForConfigAsCode(opts)
135161
opts.ConvertOptions.Project.Value = opts.Name.Value
136162
if err != nil {

pkg/cmd/project/create/create_opts.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,26 @@ import (
77
"github.com/OctopusDeploy/cli/pkg/cmd/project/shared"
88
"github.com/OctopusDeploy/cli/pkg/output"
99
"github.com/OctopusDeploy/cli/pkg/util/flag"
10+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
1011
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projectgroups"
1112
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects"
13+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tagsets"
1214
)
1315

1416
type ConvertProjectToConfigAsCodeCallback func() (cmd.Dependable, error)
1517

1618
type GetAllGroupsCallback func() ([]*projectgroups.ProjectGroup, error)
1719

20+
type GetAllTagSetsCallback func() ([]*tagsets.TagSet, error)
21+
1822
type CreateOptions struct {
1923
*CreateFlags
2024
*cmd.Dependencies
2125
*projectConvert.ConvertOptions
2226
GetAllGroupsCallback shared.GetAllGroupsCallback
2327
CreateProjectGroupCallback shared.CreateProjectGroupCallback
2428
ConvertProjectCallback ConvertProjectToConfigAsCodeCallback
29+
GetAllTagsCallback GetAllTagSetsCallback
2530
}
2631

2732
func NewCreateOptions(createFlags *CreateFlags, dependencies *cmd.Dependencies) *CreateOptions {
@@ -34,6 +39,7 @@ func NewCreateOptions(createFlags *CreateFlags, dependencies *cmd.Dependencies)
3439
GetAllGroupsCallback: func() ([]*projectgroups.ProjectGroup, error) { return shared.GetAllGroups(*dependencies.Client) },
3540
CreateProjectGroupCallback: func() (string, cmd.Dependable, error) { return shared.CreateProjectGroup(dependencies) },
3641
ConvertProjectCallback: func() (cmd.Dependable, error) { return convertProjectCallback(convertOptions) },
42+
GetAllTagsCallback: getAllTagSetsCallback(dependencies.Client),
3743
}
3844
}
3945

@@ -57,6 +63,7 @@ func (co *CreateOptions) Commit() error {
5763

5864
project := projects.NewProject(co.Name.Value, lifecycle.GetID(), projectGroup.GetID())
5965
project.Description = co.Description.Value
66+
project.ProjectTags = co.Tag.Value
6067

6168
createdProject, err := co.Client.Projects.Add(project)
6269
if err != nil {
@@ -76,7 +83,20 @@ func (co *CreateOptions) Commit() error {
7683

7784
func (co *CreateOptions) GenerateAutomationCmd() {
7885
if !co.NoPrompt {
79-
autoCmd := flag.GenerateAutomationCmd(co.CmdPath, co.Name, co.Description, co.Group, co.Lifecycle)
86+
autoCmd := flag.GenerateAutomationCmd(co.CmdPath, co.Name, co.Description, co.Group, co.Lifecycle, co.Tag)
8087
fmt.Fprintf(co.Out, "%s\n", autoCmd)
8188
}
8289
}
90+
91+
func getAllTagSetsCallback(client *client.Client) GetAllTagSetsCallback {
92+
return func() ([]*tagsets.TagSet, error) {
93+
query := tagsets.TagSetsQuery{
94+
Scopes: []string{string(tagsets.TagSetScopeProject)},
95+
}
96+
result, err := tagsets.Get(client, client.GetSpaceID(), query)
97+
if err != nil {
98+
return nil, err
99+
}
100+
return result.Items, nil
101+
}
102+
}

pkg/cmd/project/list/list.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ func NewCmdList(f factory.Factory) *cobra.Command {
2929
}
3030

3131
type ProjectAsJson struct {
32-
Id string `json:"Id"`
33-
Name string `json:"Name"`
34-
Description string `json:"Description"`
32+
Id string `json:"Id"`
33+
Name string `json:"Name"`
34+
Description string `json:"Description"`
35+
ProjectTags []string `json:"ProjectTags,omitempty"`
3536
}
3637

3738
func listRun(cmd *cobra.Command, f factory.Factory) error {
@@ -51,12 +52,13 @@ func listRun(cmd *cobra.Command, f factory.Factory) error {
5152
Id: p.GetID(),
5253
Name: p.GetName(),
5354
Description: p.Description,
55+
ProjectTags: p.ProjectTags,
5456
}
5557
},
5658
Table: output.TableDefinition[*projects.Project]{
57-
Header: []string{"NAME", "DESCRIPTION"},
59+
Header: []string{"NAME", "DESCRIPTION", "TAGS"},
5860
Row: func(p *projects.Project) []string {
59-
return []string{output.Bold(p.Name), p.Description}
61+
return []string{output.Bold(p.Name), p.Description, output.FormatAsList(p.ProjectTags)}
6062
},
6163
},
6264
Basic: func(p *projects.Project) string {

pkg/cmd/project/project.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
cmdDisconnect "github.com/OctopusDeploy/cli/pkg/cmd/project/disconnect"
1313
cmdEnable "github.com/OctopusDeploy/cli/pkg/cmd/project/enable"
1414
cmdList "github.com/OctopusDeploy/cli/pkg/cmd/project/list"
15+
cmdTag "github.com/OctopusDeploy/cli/pkg/cmd/project/tag"
1516
cmdVariables "github.com/OctopusDeploy/cli/pkg/cmd/project/variables"
1617
cmdView "github.com/OctopusDeploy/cli/pkg/cmd/project/view"
1718
"github.com/OctopusDeploy/cli/pkg/constants"
@@ -47,6 +48,7 @@ func NewCmdProject(f factory.Factory) *cobra.Command {
4748
cmd.AddCommand(cmdVariables.NewCmdVariables(f))
4849
cmd.AddCommand(cmdClone.NewCmdClone(f))
4950
cmd.AddCommand(cmdBranch.NewCmdBranch(f))
51+
cmd.AddCommand(cmdTag.NewCmdTag(f))
5052

5153
return cmd
5254
}

pkg/cmd/project/tag/tag.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package tag
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
"github.com/MakeNowJust/heredoc/v2"
8+
"github.com/OctopusDeploy/cli/pkg/cmd"
9+
"github.com/OctopusDeploy/cli/pkg/constants"
10+
"github.com/OctopusDeploy/cli/pkg/factory"
11+
"github.com/OctopusDeploy/cli/pkg/output"
12+
"github.com/OctopusDeploy/cli/pkg/question"
13+
"github.com/OctopusDeploy/cli/pkg/question/selectors"
14+
"github.com/OctopusDeploy/cli/pkg/util/flag"
15+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects"
16+
"github.com/spf13/cobra"
17+
)
18+
19+
const (
20+
FlagTag = "tag"
21+
FlagProject = "project"
22+
)
23+
24+
type TagFlags struct {
25+
Tag *flag.Flag[[]string]
26+
Project *flag.Flag[string]
27+
}
28+
29+
func NewTagFlags() *TagFlags {
30+
return &TagFlags{
31+
Tag: flag.New[[]string](FlagTag, false),
32+
Project: flag.New[string](FlagProject, false),
33+
}
34+
}
35+
36+
func NewCmdTag(f factory.Factory) *cobra.Command {
37+
createFlags := NewTagFlags()
38+
39+
cmd := &cobra.Command{
40+
Use: "tag",
41+
Short: "Override tags for a project",
42+
Long: "Override tags for a project in Octopus Deploy",
43+
Example: heredoc.Docf("$ %s project tag Project-1", constants.ExecutableName),
44+
RunE: func(c *cobra.Command, _ []string) error {
45+
opts := NewTagOptions(createFlags, cmd.NewDependencies(f, c))
46+
47+
return createRun(opts)
48+
},
49+
}
50+
51+
flags := cmd.Flags()
52+
flags.StringArrayVarP(&createFlags.Tag.Value, createFlags.Tag.Name, "t", []string{}, "Tag to apply to project, must use canonical name: <tag_set>/<tag_name>")
53+
flags.StringVar(&createFlags.Project.Value, createFlags.Project.Name, "", "Name or ID of the project you wish to update")
54+
55+
return cmd
56+
}
57+
58+
func createRun(opts *TagOptions) error {
59+
var optsArray []cmd.Dependable
60+
var err error
61+
if !opts.NoPrompt {
62+
optsArray, err = PromptMissing(opts)
63+
if err != nil {
64+
return err
65+
}
66+
} else {
67+
// Validate tags when running with --no-prompt
68+
if len(opts.Tag.Value) > 0 {
69+
tagSets, err := opts.GetAllTagsCallback()
70+
if err != nil {
71+
return err
72+
}
73+
if err := selectors.ValidateTags(opts.Tag.Value, tagSets); err != nil {
74+
return err
75+
}
76+
}
77+
optsArray = append(optsArray, opts)
78+
}
79+
80+
for _, o := range optsArray {
81+
if err := o.Commit(); err != nil {
82+
return err
83+
}
84+
}
85+
86+
if !opts.NoPrompt {
87+
fmt.Fprintln(opts.Out, "\nAutomation Commands:")
88+
for _, o := range optsArray {
89+
o.GenerateAutomationCmd()
90+
}
91+
}
92+
93+
return nil
94+
}
95+
96+
func PromptMissing(opts *TagOptions) ([]cmd.Dependable, error) {
97+
nestedOpts := []cmd.Dependable{}
98+
99+
project, err := AskProjects(opts.Ask, opts.Out, opts.Project.Value, opts.GetProjectsCallback, opts.GetProjectCallback)
100+
if err != nil {
101+
return nil, err
102+
}
103+
opts.project = project
104+
opts.Project.Value = project.Name
105+
106+
tagSets, err := opts.GetAllTagsCallback()
107+
if err != nil {
108+
return nil, err
109+
}
110+
111+
tags, err := selectors.Tags(opts.Ask, opts.project.ProjectTags, opts.Tag.Value, tagSets)
112+
if err != nil {
113+
return nil, err
114+
}
115+
opts.Tag.Value = tags
116+
117+
nestedOpts = append(nestedOpts, opts)
118+
return nestedOpts, nil
119+
}
120+
121+
func AskProjects(ask question.Asker, out io.Writer, value string, getProjectsCallback GetProjectsCallback, getProjectCallback GetProjectCallback) (*projects.Project, error) {
122+
if value != "" {
123+
project, err := getProjectCallback(value)
124+
if err != nil {
125+
return nil, err
126+
}
127+
return project, nil
128+
}
129+
130+
// Check if there's only one project
131+
projs, err := getProjectsCallback()
132+
if err != nil {
133+
return nil, err
134+
}
135+
if len(projs) == 1 {
136+
fmt.Fprintf(out, "Selecting only available project '%s'.\n", output.Cyan(projs[0].Name))
137+
return projs[0], nil
138+
}
139+
140+
project, err := selectors.Select(ask, "Select the Project you would like to update", getProjectsCallback, func(item *projects.Project) string {
141+
return item.Name
142+
})
143+
if err != nil {
144+
return nil, err
145+
}
146+
147+
return project, nil
148+
}

0 commit comments

Comments
 (0)