Skip to content

Commit e1ccf99

Browse files
authored
feat: service message flag support (#574)
## Description service message flag support Related Shortcut Issue: - [Issue 96147](https://app.shortcut.com/octopusdeploy/story/96147) --------- Signed-off-by: Chen Keinan <hen.keinan@gmail.com>
1 parent 8d40d68 commit e1ccf99

9 files changed

Lines changed: 205 additions & 30 deletions

File tree

cmd/gen-docs/main.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package main
22

33
import (
44
"fmt"
5-
"github.com/OctopusDeploy/cli/pkg/config"
6-
"github.com/spf13/viper"
75
"io"
86
"os"
97
"os/user"
@@ -13,6 +11,10 @@ import (
1311
"text/template"
1412
"time"
1513

14+
"github.com/OctopusDeploy/cli/pkg/config"
15+
"github.com/OctopusDeploy/cli/pkg/servicemessages"
16+
"github.com/spf13/viper"
17+
1618
"github.com/AlecAivazis/survey/v2"
1719
version "github.com/OctopusDeploy/cli"
1820
"github.com/OctopusDeploy/cli/pkg/apiclient"
@@ -100,7 +102,7 @@ func run(args []string) error {
100102
buildVersion := strings.TrimSpace(version.Version)
101103
viper := viper.GetViper()
102104
c := config.New(viper)
103-
f := factory.New(clientFactory, askProvider, s, buildVersion, c)
105+
f := factory.New(clientFactory, askProvider, s, buildVersion, c, servicemessages.NewProvider(servicemessages.NewOutputPrinter(os.Stdout, os.Stderr)))
104106

105107
cmd := root.NewCmdRoot(f, clientFactory, askProvider)
106108
cmd.DisableAutoGenTag = true

cmd/octopus/main.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ package main
33
import (
44
_ "embed"
55
"fmt"
6-
"github.com/OctopusDeploy/cli/pkg/util"
76
"os"
87
"strings"
98
"time"
109

10+
"github.com/OctopusDeploy/cli/pkg/util"
11+
1112
"github.com/AlecAivazis/survey/v2/terminal"
1213
version "github.com/OctopusDeploy/cli"
14+
"github.com/OctopusDeploy/cli/pkg/servicemessages"
1315
"github.com/briandowns/spinner"
1416
"github.com/spf13/viper"
1517

@@ -64,13 +66,17 @@ func main() {
6466

6567
c := config.New(viper)
6668

67-
f := factory.New(clientFactory, askProvider, s, buildVersion, c)
69+
terminalOut := terminal.NewAnsiStdout(os.Stdout)
70+
terminalErr := terminal.NewAnsiStderr(os.Stderr)
71+
72+
serviceMessageProvider := servicemessages.NewProvider(servicemessages.NewOutputPrinter(terminalOut, terminalErr))
73+
f := factory.New(clientFactory, askProvider, s, buildVersion, c, serviceMessageProvider)
6874

6975
cmd := root.NewCmdRoot(f, clientFactory, askProvider)
7076

7177
// if we don't do this then cmd.Print will get sent to stderr
72-
cmd.SetOut(terminal.NewAnsiStdout(os.Stdout))
73-
cmd.SetErr(terminal.NewAnsiStderr(os.Stderr))
78+
cmd.SetOut(terminalOut)
79+
cmd.SetErr(terminalErr)
7480

7581
if err := cmd.Execute(); err != nil {
7682
cmd.PrintErr(err)

pkg/cmd/release/create/create.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ func createRun(cmd *cobra.Command, f factory.Factory, flags *CreateFlags) error
344344
} else {
345345
cmd.Printf("Successfully created release version %s\n", releaseVersion)
346346
}
347+
f.GetServiceMessageProvider().ServiceMessage("setParameter", map[string]string{"name": "octo.releaseNumber", "value": releaseVersion})
347348
}
348349
}
349350

pkg/cmd/root/root.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ func NewCmdRoot(f factory.Factory, clientFactory apiclient.ClientFactory, askPro
9393

9494
cmdPFlags.BoolP(constants.FlagNoPrompt, "", false, "Disable prompting in interactive mode")
9595

96+
// Enable service messages flag is hidden as it's intended for internal CI/CD use only
97+
cmdPFlags.BoolP(constants.FlagEnableServiceMessages,"", false, "Enable service messages for integration with Octopus CI/CD")
98+
cmdPFlags.MarkHidden(constants.FlagEnableServiceMessages)
9699
// Legacy flags brought across from the .NET CLI.
97100
// Consumers of these flags will have to explicitly check for them as well as the new
98101
// flags. The pflag documentation says you can use SetNormalizeFunc to translate/alias flag
@@ -106,6 +109,7 @@ func NewCmdRoot(f factory.Factory, clientFactory apiclient.ClientFactory, askPro
106109

107110
_ = viper.BindPFlag(constants.ConfigNoPrompt, cmdPFlags.Lookup(constants.FlagNoPrompt))
108111
_ = viper.BindPFlag(constants.ConfigSpace, cmdPFlags.Lookup(constants.FlagSpace))
112+
_ = viper.BindPFlag(constants.FlagEnableServiceMessages, cmdPFlags.Lookup(constants.FlagEnableServiceMessages))
109113
// if we attempt to check the flags before Execute is called, cobra hasn't parsed anything yet,
110114
// so we'll get bad values. PersistentPreRun is a convenient callback for setting up our
111115
// environment after parsing but before execution.

pkg/constants/constants.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ const (
66

77
// flags for command line switches
88
const (
9-
FlagHelp = "help"
10-
FlagSpace = "space"
11-
FlagOutputFormat = "output-format"
12-
FlagOutputFormatLegacy = "outputFormat"
13-
FlagNoPrompt = "no-prompt"
9+
FlagHelp = "help"
10+
FlagSpace = "space"
11+
FlagOutputFormat = "output-format"
12+
FlagOutputFormatLegacy = "outputFormat"
13+
FlagNoPrompt = "no-prompt"
14+
FlagEnableServiceMessages = "enable-service-messages"
1415
)
1516

1617
// flags for storing things in the go context

pkg/factory/factory.go

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/OctopusDeploy/cli/pkg/apiclient"
88
"github.com/OctopusDeploy/cli/pkg/config"
99
"github.com/OctopusDeploy/cli/pkg/question"
10+
"github.com/OctopusDeploy/cli/pkg/servicemessages"
1011
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
1112
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces"
1213
)
@@ -18,11 +19,12 @@ type Spinner interface {
1819
}
1920

2021
type factory struct {
21-
client apiclient.ClientFactory
22-
asker question.AskProvider
23-
spinner Spinner
24-
buildVersion string
25-
config config.IConfigProvider
22+
client apiclient.ClientFactory
23+
asker question.AskProvider
24+
spinner Spinner
25+
buildVersion string
26+
config config.IConfigProvider
27+
serviceMessageProvider servicemessages.Provider
2628
}
2729

2830
type Factory interface {
@@ -36,15 +38,22 @@ type Factory interface {
3638
BuildVersion() string
3739
GetHttpClient() (*http.Client, error)
3840
GetConfigProvider() (config.IConfigProvider, error)
41+
GetServiceMessageProvider() servicemessages.Provider
3942
}
4043

41-
func New(clientFactory apiclient.ClientFactory, asker question.AskProvider, s Spinner, buildVersion string, config config.IConfigProvider) Factory {
44+
func New(clientFactory apiclient.ClientFactory,
45+
asker question.AskProvider,
46+
s Spinner,
47+
buildVersion string,
48+
config config.IConfigProvider,
49+
serviceMessageProvider servicemessages.Provider) Factory {
4250
return &factory{
43-
client: clientFactory,
44-
asker: asker,
45-
spinner: s,
46-
buildVersion: buildVersion,
47-
config: config,
51+
client: clientFactory,
52+
asker: asker,
53+
spinner: s,
54+
buildVersion: buildVersion,
55+
config: config,
56+
serviceMessageProvider: serviceMessageProvider,
4857
}
4958
}
5059

@@ -97,6 +106,10 @@ func (f *factory) GetConfigProvider() (config.IConfigProvider, error) {
97106
return f.config, nil
98107
}
99108

109+
func (f *factory) GetServiceMessageProvider() servicemessages.Provider {
110+
return f.serviceMessageProvider
111+
}
112+
100113
// NoSpinner is a static singleton "does nothing" stand-in for spinner if you want to
101114
// call an API that expects a spinner while you're in automation mode.
102115
var NoSpinner Spinner = &noSpinner{}

pkg/servicemessages/provider.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package servicemessages
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"strings"
8+
9+
"github.com/OctopusDeploy/cli/pkg/constants"
10+
"github.com/spf13/viper"
11+
)
12+
13+
type Provider interface {
14+
ServiceMessage(messageName string, values any)
15+
}
16+
17+
type provider struct {
18+
printer *OutputPrinter
19+
}
20+
21+
func NewProvider(printer *OutputPrinter) Provider {
22+
return &provider{
23+
printer: printer,
24+
}
25+
}
26+
27+
func (p *provider) ServiceMessage(messageName string, values any) {
28+
serviceMessageEnabled := viper.GetBool(constants.FlagEnableServiceMessages)
29+
if !serviceMessageEnabled {
30+
return
31+
}
32+
33+
teamCityEnvVar := os.Getenv("TEAMCITY_VERSION")
34+
if teamCityEnvVar == "" {
35+
p.printer.Error("service messages are only supported in TeamCity builds")
36+
return
37+
}
38+
39+
switch t := values.(type) {
40+
case string:
41+
p.printer.Info(fmt.Sprintf("##teamcity[%s %s]\n", messageName, t))
42+
case map[string]string:
43+
mapMsg := p.mapToStringMsg(t, messageName)
44+
p.printer.Info(mapMsg)
45+
default:
46+
p.printer.Error("Unsupported service message value type")
47+
}
48+
}
49+
50+
type OutputPrinter struct {
51+
Out io.Writer
52+
Err io.Writer
53+
}
54+
55+
func NewOutputPrinter(out io.Writer, err io.Writer) *OutputPrinter {
56+
return &OutputPrinter{
57+
Out: out,
58+
Err: err,
59+
}
60+
}
61+
62+
func (p *OutputPrinter) Info(msg string) {
63+
fmt.Fprint(p.Out, msg)
64+
}
65+
66+
func (p *OutputPrinter) Error(msg string) {
67+
fmt.Fprint(p.Err, msg)
68+
}
69+
70+
func (p *provider) mapToStringMsg(m map[string]string, messageName string) string {
71+
var builder strings.Builder
72+
builder.WriteString(fmt.Sprintf("##teamcity[%s", messageName))
73+
for key, value := range m {
74+
builder.WriteString(fmt.Sprintf(" %s=%s", key, value))
75+
}
76+
builder.WriteString("]")
77+
return builder.String()
78+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package servicemessages
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/OctopusDeploy/cli/pkg/constants"
8+
"github.com/spf13/viper"
9+
)
10+
11+
func TestServiceMessage(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
servicemessages bool
15+
teamCityEnv bool
16+
messsageName string
17+
key string
18+
value any
19+
stdout *bytes.Buffer
20+
stderr *bytes.Buffer
21+
want string
22+
wantErr string
23+
}{
24+
{"service message flag is not enabled", false, false, "testMessage", "key1", "value1", &bytes.Buffer{}, &bytes.Buffer{}, "", ""},
25+
{"service message enabled with teamcity envvar and map value", true, true, "testMessage", "key1", map[string]string{"key": "value"}, &bytes.Buffer{}, &bytes.Buffer{}, "##teamcity[testMessage key=value]", ""},
26+
{"service message enabled without teamcity envvar", true, false, "testMessage", "key1", "value1", &bytes.Buffer{}, &bytes.Buffer{}, "", "service messages are only supported in TeamCity builds"},
27+
{"service message enabled with teamcity envvar and string value", true, true, "testMessage", "key1", "value", &bytes.Buffer{}, &bytes.Buffer{}, "##teamcity[testMessage value]\n", ""},
28+
{"service message enabled with teamcity envvar and unsupported value", true, true, "testMessage", "key1", []string{"dsdsd"}, &bytes.Buffer{}, &bytes.Buffer{}, "", "Unsupported service message value type"},
29+
}
30+
31+
for _, tt := range tests {
32+
setupArgs(t, constants.FlagEnableServiceMessages, tt.servicemessages)
33+
setupEnvVar(t, "TEAMCITY_VERSION", "2021.1", tt.teamCityEnv)
34+
t.Run(tt.name, func(t *testing.T) {
35+
NewProvider(NewOutputPrinter(tt.stdout, tt.stderr)).ServiceMessage(tt.messsageName, tt.value)
36+
if tt.want != "" {
37+
got := tt.stdout.String()
38+
if got != tt.want {
39+
t.Errorf("Expected output:\n%s\nGot:\n%s", tt.want, got)
40+
}
41+
}
42+
if tt.wantErr != "" {
43+
e := tt.stderr.String()
44+
if e != tt.wantErr {
45+
t.Errorf("Expected error output:\n%s\nGot:\n%s", tt.wantErr, e)
46+
}
47+
}
48+
})
49+
}
50+
}
51+
52+
func setupArgs(t *testing.T, key string, value bool) {
53+
viper.Reset()
54+
viper.Set(constants.FlagEnableServiceMessages, value)
55+
}
56+
57+
func setupEnvVar(t *testing.T, key, value string, set bool) {
58+
if set {
59+
t.Setenv(key, value)
60+
} else {
61+
t.Setenv(key, "")
62+
}
63+
}

test/testutil/fakefactory.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package testutil
22

33
import (
4+
"bytes"
45
"errors"
56
"net/http"
67
"net/url"
@@ -10,6 +11,7 @@ import (
1011
"github.com/OctopusDeploy/cli/pkg/config"
1112
"github.com/OctopusDeploy/cli/pkg/factory"
1213
"github.com/OctopusDeploy/cli/pkg/question"
14+
"github.com/OctopusDeploy/cli/pkg/servicemessages"
1315
octopusApiClient "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
1416
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces"
1517
)
@@ -54,17 +56,19 @@ func NewMockFactoryWithSpaceAndPrompt(api *MockHttpServer, space *spaces.Space,
5456
result := NewMockFactory(api)
5557
result.CurrentSpace = space
5658
result.AskProvider = askProvider
59+
result.serviceMessageProvider = servicemessages.NewProvider(servicemessages.NewOutputPrinter(&bytes.Buffer{}, &bytes.Buffer{}))
5760
return result
5861
}
5962

6063
type MockFactory struct {
61-
api *MockHttpServer // must not be nil
62-
SystemClient *octopusApiClient.Client // nil; lazily created like with the real factory
63-
SpaceScopedClient *octopusApiClient.Client // nil; lazily created like with the real factory
64-
CurrentSpace *spaces.Space
65-
RawSpinner factory.Spinner
66-
AskProvider question.AskProvider
67-
ConfigProvider config.IConfigProvider
64+
api *MockHttpServer // must not be nil
65+
SystemClient *octopusApiClient.Client // nil; lazily created like with the real factory
66+
SpaceScopedClient *octopusApiClient.Client // nil; lazily created like with the real factory
67+
CurrentSpace *spaces.Space
68+
RawSpinner factory.Spinner
69+
AskProvider question.AskProvider
70+
ConfigProvider config.IConfigProvider
71+
serviceMessageProvider servicemessages.Provider
6872
}
6973

7074
// refactor this later if there's ever a need for unit tests to vary the server url or API key (why would there be?)
@@ -127,3 +131,6 @@ func (f *MockFactory) Ask(p survey.Prompt, response interface{}, opts ...survey.
127131
func (f *MockFactory) GetConfigProvider() (config.IConfigProvider, error) {
128132
return f.ConfigProvider, nil
129133
}
134+
func (f *MockFactory) GetServiceMessageProvider() servicemessages.Provider {
135+
return f.serviceMessageProvider
136+
}

0 commit comments

Comments
 (0)