From 606ac3404a93fb1673ae20b53430eb709c7cbfec Mon Sep 17 00:00:00 2001 From: Chen Keinan Date: Sun, 11 Jan 2026 18:24:30 +0200 Subject: [PATCH 1/5] feat: service message flag support Signed-off-by: Chen Keinan --- cmd/gen-docs/main.go | 8 +++-- cmd/octopus/main.go | 14 +++++--- pkg/cmd/login/login.go | 3 +- pkg/cmd/root/root.go | 4 +++ pkg/constants/constants.go | 11 +++--- pkg/factory/factory.go | 35 ++++++++++++------ pkg/servicemessages/provider.go | 63 +++++++++++++++++++++++++++++++++ test/testutil/fakefactory.go | 19 ++++++---- 8 files changed, 126 insertions(+), 31 deletions(-) create mode 100644 pkg/servicemessages/provider.go diff --git a/cmd/gen-docs/main.go b/cmd/gen-docs/main.go index 1a3011aa..df15b402 100644 --- a/cmd/gen-docs/main.go +++ b/cmd/gen-docs/main.go @@ -2,8 +2,6 @@ package main import ( "fmt" - "github.com/OctopusDeploy/cli/pkg/config" - "github.com/spf13/viper" "io" "os" "os/user" @@ -13,6 +11,10 @@ import ( "text/template" "time" + "github.com/OctopusDeploy/cli/pkg/config" + "github.com/OctopusDeploy/cli/pkg/servicemessages" + "github.com/spf13/viper" + "github.com/AlecAivazis/survey/v2" version "github.com/OctopusDeploy/cli" "github.com/OctopusDeploy/cli/pkg/apiclient" @@ -100,7 +102,7 @@ func run(args []string) error { buildVersion := strings.TrimSpace(version.Version) viper := viper.GetViper() c := config.New(viper) - f := factory.New(clientFactory, askProvider, s, buildVersion, c) + f := factory.New(clientFactory, askProvider, s, buildVersion, c, servicemessages.NewProvider(servicemessages.NewPrinter(os.Stdout, os.Stderr))) cmd := root.NewCmdRoot(f, clientFactory, askProvider) cmd.DisableAutoGenTag = true diff --git a/cmd/octopus/main.go b/cmd/octopus/main.go index 26f2dfb7..15a4f3cc 100644 --- a/cmd/octopus/main.go +++ b/cmd/octopus/main.go @@ -3,13 +3,15 @@ package main import ( _ "embed" "fmt" - "github.com/OctopusDeploy/cli/pkg/util" "os" "strings" "time" + "github.com/OctopusDeploy/cli/pkg/util" + "github.com/AlecAivazis/survey/v2/terminal" version "github.com/OctopusDeploy/cli" + "github.com/OctopusDeploy/cli/pkg/servicemessages" "github.com/briandowns/spinner" "github.com/spf13/viper" @@ -64,13 +66,17 @@ func main() { c := config.New(viper) - f := factory.New(clientFactory, askProvider, s, buildVersion, c) + terminalOut := terminal.NewAnsiStdout(os.Stdout) + terminalErr := terminal.NewAnsiStderr(os.Stderr) + + serviceMessageProvider := servicemessages.NewProvider(servicemessages.NewPrinter(terminalOut, terminalErr)) + f := factory.New(clientFactory, askProvider, s, buildVersion, c, serviceMessageProvider) cmd := root.NewCmdRoot(f, clientFactory, askProvider) // if we don't do this then cmd.Print will get sent to stderr - cmd.SetOut(terminal.NewAnsiStdout(os.Stdout)) - cmd.SetErr(terminal.NewAnsiStderr(os.Stderr)) + cmd.SetOut(terminalOut) + cmd.SetErr(terminalErr) if err := cmd.Execute(); err != nil { cmd.PrintErr(err) diff --git a/pkg/cmd/login/login.go b/pkg/cmd/login/login.go index 5844a1d7..ac120ce5 100644 --- a/pkg/cmd/login/login.go +++ b/pkg/cmd/login/login.go @@ -6,11 +6,12 @@ import ( "encoding/json" "errors" "fmt" - "github.com/AlecAivazis/survey/v2" "io" "net/http" "time" + "github.com/AlecAivazis/survey/v2" + "github.com/MakeNowJust/heredoc/v2" "github.com/OctopusDeploy/cli/pkg/apiclient" "github.com/OctopusDeploy/cli/pkg/config" diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 630a96d7..ea7698b1 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -93,6 +93,9 @@ func NewCmdRoot(f factory.Factory, clientFactory apiclient.ClientFactory, askPro cmdPFlags.BoolP(constants.FlagNoPrompt, "", false, "Disable prompting in interactive mode") + // Enable service messages flag is hidden as it's intended for internal CI/CD use only + cmdPFlags.BoolP(constants.FlagEnableServiceMessages,"", false, "Enable service messages for integration with Octopus CI/CD") + cmdPFlags.MarkHidden(constants.FlagEnableServiceMessages) // Legacy flags brought across from the .NET CLI. // Consumers of these flags will have to explicitly check for them as well as the new // 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 _ = viper.BindPFlag(constants.ConfigNoPrompt, cmdPFlags.Lookup(constants.FlagNoPrompt)) _ = viper.BindPFlag(constants.ConfigSpace, cmdPFlags.Lookup(constants.FlagSpace)) + _ = viper.BindPFlag(constants.FlagEnableServiceMessages, cmdPFlags.Lookup(constants.FlagEnableServiceMessages)) // if we attempt to check the flags before Execute is called, cobra hasn't parsed anything yet, // so we'll get bad values. PersistentPreRun is a convenient callback for setting up our // environment after parsing but before execution. diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 3d7505cf..39b2ccf0 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -6,11 +6,12 @@ const ( // flags for command line switches const ( - FlagHelp = "help" - FlagSpace = "space" - FlagOutputFormat = "output-format" - FlagOutputFormatLegacy = "outputFormat" - FlagNoPrompt = "no-prompt" + FlagHelp = "help" + FlagSpace = "space" + FlagOutputFormat = "output-format" + FlagOutputFormatLegacy = "outputFormat" + FlagNoPrompt = "no-prompt" + FlagEnableServiceMessages = "enable-service-messages" ) // flags for storing things in the go context diff --git a/pkg/factory/factory.go b/pkg/factory/factory.go index 477e4f77..17af12d6 100644 --- a/pkg/factory/factory.go +++ b/pkg/factory/factory.go @@ -7,6 +7,7 @@ import ( "github.com/OctopusDeploy/cli/pkg/apiclient" "github.com/OctopusDeploy/cli/pkg/config" "github.com/OctopusDeploy/cli/pkg/question" + "github.com/OctopusDeploy/cli/pkg/servicemessages" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces" ) @@ -18,11 +19,12 @@ type Spinner interface { } type factory struct { - client apiclient.ClientFactory - asker question.AskProvider - spinner Spinner - buildVersion string - config config.IConfigProvider + client apiclient.ClientFactory + asker question.AskProvider + spinner Spinner + buildVersion string + config config.IConfigProvider + serviceMessageProvider servicemessages.Provider } type Factory interface { @@ -36,15 +38,22 @@ type Factory interface { BuildVersion() string GetHttpClient() (*http.Client, error) GetConfigProvider() (config.IConfigProvider, error) + GetServiceMessageProvider() (servicemessages.Provider, error) } -func New(clientFactory apiclient.ClientFactory, asker question.AskProvider, s Spinner, buildVersion string, config config.IConfigProvider) Factory { +func New(clientFactory apiclient.ClientFactory, + asker question.AskProvider, + s Spinner, + buildVersion string, + config config.IConfigProvider, + serviceMessageProvider servicemessages.Provider) Factory { return &factory{ - client: clientFactory, - asker: asker, - spinner: s, - buildVersion: buildVersion, - config: config, + client: clientFactory, + asker: asker, + spinner: s, + buildVersion: buildVersion, + config: config, + serviceMessageProvider: serviceMessageProvider, } } @@ -97,6 +106,10 @@ func (f *factory) GetConfigProvider() (config.IConfigProvider, error) { return f.config, nil } +func (f *factory) GetServiceMessageProvider() (servicemessages.Provider, error) { + return f.serviceMessageProvider, nil +} + // NoSpinner is a static singleton "does nothing" stand-in for spinner if you want to // call an API that expects a spinner while you're in automation mode. var NoSpinner Spinner = &noSpinner{} diff --git a/pkg/servicemessages/provider.go b/pkg/servicemessages/provider.go new file mode 100644 index 00000000..289fb32c --- /dev/null +++ b/pkg/servicemessages/provider.go @@ -0,0 +1,63 @@ +package servicemessages + +import ( + "fmt" + "io" + "os" + + "github.com/spf13/viper" +) + +type Provider interface { + ServiceMessage(messageName string, values any) +} + +type provider struct { + printer *Printer +} + +func NewProvider(printer *Printer) Provider { + return &provider{ + printer: printer, + } +} + +func (p *provider) ServiceMessage(messageName string, values any) { + serviceMessageEnabled := viper.GetBool("enable-service-messages") + teamCityEnvVar := os.Getenv("TEAMCITY_VERSION") + + if serviceMessageEnabled && teamCityEnvVar == "" { + p.printer.Error("service messages are only supported in TeamCity builds") + return + } + switch t := values.(type) { + case string: + p.printer.Println(fmt.Sprintf("##teamcity[%s %s]\n", messageName, t)) + case map[string]string: + for key, value := range t { + p.printer.Println(fmt.Sprintf("##teamcity[%s %s=%s]\n", messageName, key, value)) + } + default: + p.printer.Error("Unsupported service message value type") + } +} + +type Printer struct { + Out io.Writer + Err io.Writer +} + +func NewPrinter(out io.Writer, err io.Writer) *Printer { + return &Printer{ + Out: out, + Err: err, + } +} + +func (p *Printer) Println(msg string) { + fmt.Fprintln(p.Out, msg) +} + +func (p *Printer) Error(msg string) { + fmt.Fprintln(p.Err, msg) +} diff --git a/test/testutil/fakefactory.go b/test/testutil/fakefactory.go index f38744ec..a71ee5ff 100644 --- a/test/testutil/fakefactory.go +++ b/test/testutil/fakefactory.go @@ -10,6 +10,7 @@ import ( "github.com/OctopusDeploy/cli/pkg/config" "github.com/OctopusDeploy/cli/pkg/factory" "github.com/OctopusDeploy/cli/pkg/question" + "github.com/OctopusDeploy/cli/pkg/servicemessages" octopusApiClient "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces" ) @@ -58,13 +59,14 @@ func NewMockFactoryWithSpaceAndPrompt(api *MockHttpServer, space *spaces.Space, } type MockFactory struct { - api *MockHttpServer // must not be nil - SystemClient *octopusApiClient.Client // nil; lazily created like with the real factory - SpaceScopedClient *octopusApiClient.Client // nil; lazily created like with the real factory - CurrentSpace *spaces.Space - RawSpinner factory.Spinner - AskProvider question.AskProvider - ConfigProvider config.IConfigProvider + api *MockHttpServer // must not be nil + SystemClient *octopusApiClient.Client // nil; lazily created like with the real factory + SpaceScopedClient *octopusApiClient.Client // nil; lazily created like with the real factory + CurrentSpace *spaces.Space + RawSpinner factory.Spinner + AskProvider question.AskProvider + ConfigProvider config.IConfigProvider + serviceMessageProvider servicemessages.Provider } // 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 +129,6 @@ func (f *MockFactory) Ask(p survey.Prompt, response interface{}, opts ...survey. func (f *MockFactory) GetConfigProvider() (config.IConfigProvider, error) { return f.ConfigProvider, nil } +func (f *MockFactory) GetServiceMessageProvider() (servicemessages.Provider, error) { + return f.serviceMessageProvider, nil +} From f44056741af8f5537477cd7b79d6f6dd4b123bee Mon Sep 17 00:00:00 2001 From: Chen Keinan Date: Sun, 11 Jan 2026 21:04:42 +0200 Subject: [PATCH 2/5] test: add service message test Signed-off-by: Chen Keinan --- pkg/cmd/login/login.go | 3 +- pkg/servicemessages/provider.go | 15 +++++--- pkg/servicemessages/provider_test.go | 57 ++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 pkg/servicemessages/provider_test.go diff --git a/pkg/cmd/login/login.go b/pkg/cmd/login/login.go index ac120ce5..5844a1d7 100644 --- a/pkg/cmd/login/login.go +++ b/pkg/cmd/login/login.go @@ -6,12 +6,11 @@ import ( "encoding/json" "errors" "fmt" + "github.com/AlecAivazis/survey/v2" "io" "net/http" "time" - "github.com/AlecAivazis/survey/v2" - "github.com/MakeNowJust/heredoc/v2" "github.com/OctopusDeploy/cli/pkg/apiclient" "github.com/OctopusDeploy/cli/pkg/config" diff --git a/pkg/servicemessages/provider.go b/pkg/servicemessages/provider.go index 289fb32c..c58675f2 100644 --- a/pkg/servicemessages/provider.go +++ b/pkg/servicemessages/provider.go @@ -5,6 +5,7 @@ import ( "io" "os" + "github.com/OctopusDeploy/cli/pkg/constants" "github.com/spf13/viper" ) @@ -23,13 +24,17 @@ func NewProvider(printer *Printer) Provider { } func (p *provider) ServiceMessage(messageName string, values any) { - serviceMessageEnabled := viper.GetBool("enable-service-messages") - teamCityEnvVar := os.Getenv("TEAMCITY_VERSION") + serviceMessageEnabled := viper.GetBool(constants.FlagEnableServiceMessages) + if !serviceMessageEnabled { + return + } - if serviceMessageEnabled && teamCityEnvVar == "" { + teamCityEnvVar := os.Getenv("TEAMCITY_VERSION") + if teamCityEnvVar == "" { p.printer.Error("service messages are only supported in TeamCity builds") return } + switch t := values.(type) { case string: p.printer.Println(fmt.Sprintf("##teamcity[%s %s]\n", messageName, t)) @@ -55,9 +60,9 @@ func NewPrinter(out io.Writer, err io.Writer) *Printer { } func (p *Printer) Println(msg string) { - fmt.Fprintln(p.Out, msg) + fmt.Fprint(p.Out, msg) } func (p *Printer) Error(msg string) { - fmt.Fprintln(p.Err, msg) + fmt.Fprint(p.Err, msg) } diff --git a/pkg/servicemessages/provider_test.go b/pkg/servicemessages/provider_test.go new file mode 100644 index 00000000..6485da96 --- /dev/null +++ b/pkg/servicemessages/provider_test.go @@ -0,0 +1,57 @@ +package servicemessages + +import ( + "bytes" + "testing" + + "github.com/OctopusDeploy/cli/pkg/constants" + "github.com/spf13/viper" +) + +func TestServiceMessage(t *testing.T) { + tests := []struct { + name string + servicemessages bool + teamCityEnv bool + messsageName string + key string + value any + stdout *bytes.Buffer + stderr *bytes.Buffer + want string + wantErr string + }{ + {"service message is not set", false, false, "testMessage", "key1", "value1", &bytes.Buffer{}, &bytes.Buffer{}, "", ""}, + {"service message enabled non string value with teamcity", true, true, "testMessage", "key1", map[string]string{"key": "value"}, &bytes.Buffer{}, &bytes.Buffer{}, "##teamcity[testMessage key=value]\n", ""}, + {"service message enabled without teamcity", true, false, "testMessage", "key1", "value1", &bytes.Buffer{}, &bytes.Buffer{}, "", "service messages are only supported in TeamCity builds"}, + {"service message enabled string value with teamcity", true, true, "testMessage", "key1", "value", &bytes.Buffer{}, &bytes.Buffer{}, "##teamcity[testMessage value]\n", ""}, + {"service message enabled unsupported value with teamcity", true, true, "testMessage", "key1", []string{"dsdsd"}, &bytes.Buffer{}, &bytes.Buffer{}, "", "Unsupported service message value type"}, + } + + for _, tt := range tests { + viper.Reset() + viper.Set(constants.FlagEnableServiceMessages, tt.servicemessages) + if tt.teamCityEnv { + t.Setenv("TEAMCITY_VERSION", "2021.1") + } else { + t.Setenv("TEAMCITY_VERSION", "") + } + t.Run(tt.name, func(t *testing.T) { + serviceMessageProvider := NewProvider(NewPrinter(tt.stdout, tt.stderr)) + serviceMessageProvider.ServiceMessage(tt.messsageName, tt.value) + + if tt.want != "" { + got := tt.stdout.String() + if got != tt.want { + t.Errorf("Expected output:\n%s\nGot:\n%s", tt.want, got) + } + } + if tt.wantErr != "" { + e := tt.stderr.String() + if e != tt.wantErr { + t.Errorf("Expected error output:\n%s\nGot:\n%s", tt.wantErr, e) + } + } + }) + } +} From 0ad7508834bfe3ba345b0d2d00b7d873d0ecf3c0 Mon Sep 17 00:00:00 2001 From: Chen Keinan Date: Mon, 12 Jan 2026 08:19:08 +0200 Subject: [PATCH 3/5] refactor: print struct and func name Signed-off-by: Chen Keinan --- cmd/gen-docs/main.go | 2 +- cmd/octopus/main.go | 2 +- pkg/servicemessages/provider.go | 18 +++++++------- pkg/servicemessages/provider_test.go | 36 ++++++++++++++++------------ 4 files changed, 32 insertions(+), 26 deletions(-) diff --git a/cmd/gen-docs/main.go b/cmd/gen-docs/main.go index df15b402..fd7314d0 100644 --- a/cmd/gen-docs/main.go +++ b/cmd/gen-docs/main.go @@ -102,7 +102,7 @@ func run(args []string) error { buildVersion := strings.TrimSpace(version.Version) viper := viper.GetViper() c := config.New(viper) - f := factory.New(clientFactory, askProvider, s, buildVersion, c, servicemessages.NewProvider(servicemessages.NewPrinter(os.Stdout, os.Stderr))) + f := factory.New(clientFactory, askProvider, s, buildVersion, c, servicemessages.NewProvider(servicemessages.NewOutputPrinter(os.Stdout, os.Stderr))) cmd := root.NewCmdRoot(f, clientFactory, askProvider) cmd.DisableAutoGenTag = true diff --git a/cmd/octopus/main.go b/cmd/octopus/main.go index 15a4f3cc..d336c3a0 100644 --- a/cmd/octopus/main.go +++ b/cmd/octopus/main.go @@ -69,7 +69,7 @@ func main() { terminalOut := terminal.NewAnsiStdout(os.Stdout) terminalErr := terminal.NewAnsiStderr(os.Stderr) - serviceMessageProvider := servicemessages.NewProvider(servicemessages.NewPrinter(terminalOut, terminalErr)) + serviceMessageProvider := servicemessages.NewProvider(servicemessages.NewOutputPrinter(terminalOut, terminalErr)) f := factory.New(clientFactory, askProvider, s, buildVersion, c, serviceMessageProvider) cmd := root.NewCmdRoot(f, clientFactory, askProvider) diff --git a/pkg/servicemessages/provider.go b/pkg/servicemessages/provider.go index c58675f2..4e937b99 100644 --- a/pkg/servicemessages/provider.go +++ b/pkg/servicemessages/provider.go @@ -14,10 +14,10 @@ type Provider interface { } type provider struct { - printer *Printer + printer *OutputPrinter } -func NewProvider(printer *Printer) Provider { +func NewProvider(printer *OutputPrinter) Provider { return &provider{ printer: printer, } @@ -37,32 +37,32 @@ func (p *provider) ServiceMessage(messageName string, values any) { switch t := values.(type) { case string: - p.printer.Println(fmt.Sprintf("##teamcity[%s %s]\n", messageName, t)) + p.printer.Info(fmt.Sprintf("##teamcity[%s %s]\n", messageName, t)) case map[string]string: for key, value := range t { - p.printer.Println(fmt.Sprintf("##teamcity[%s %s=%s]\n", messageName, key, value)) + p.printer.Info(fmt.Sprintf("##teamcity[%s %s=%s]\n", messageName, key, value)) } default: p.printer.Error("Unsupported service message value type") } } -type Printer struct { +type OutputPrinter struct { Out io.Writer Err io.Writer } -func NewPrinter(out io.Writer, err io.Writer) *Printer { - return &Printer{ +func NewOutputPrinter(out io.Writer, err io.Writer) *OutputPrinter { + return &OutputPrinter{ Out: out, Err: err, } } -func (p *Printer) Println(msg string) { +func (p *OutputPrinter) Info(msg string) { fmt.Fprint(p.Out, msg) } -func (p *Printer) Error(msg string) { +func (p *OutputPrinter) Error(msg string) { fmt.Fprint(p.Err, msg) } diff --git a/pkg/servicemessages/provider_test.go b/pkg/servicemessages/provider_test.go index 6485da96..4975591c 100644 --- a/pkg/servicemessages/provider_test.go +++ b/pkg/servicemessages/provider_test.go @@ -21,25 +21,18 @@ func TestServiceMessage(t *testing.T) { want string wantErr string }{ - {"service message is not set", false, false, "testMessage", "key1", "value1", &bytes.Buffer{}, &bytes.Buffer{}, "", ""}, - {"service message enabled non string value with teamcity", true, true, "testMessage", "key1", map[string]string{"key": "value"}, &bytes.Buffer{}, &bytes.Buffer{}, "##teamcity[testMessage key=value]\n", ""}, - {"service message enabled without teamcity", true, false, "testMessage", "key1", "value1", &bytes.Buffer{}, &bytes.Buffer{}, "", "service messages are only supported in TeamCity builds"}, - {"service message enabled string value with teamcity", true, true, "testMessage", "key1", "value", &bytes.Buffer{}, &bytes.Buffer{}, "##teamcity[testMessage value]\n", ""}, - {"service message enabled unsupported value with teamcity", true, true, "testMessage", "key1", []string{"dsdsd"}, &bytes.Buffer{}, &bytes.Buffer{}, "", "Unsupported service message value type"}, + {"service message flag is not enabled", false, false, "testMessage", "key1", "value1", &bytes.Buffer{}, &bytes.Buffer{}, "", ""}, + {"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]\n", ""}, + {"service message enabled without teamcity envvar", true, false, "testMessage", "key1", "value1", &bytes.Buffer{}, &bytes.Buffer{}, "", "service messages are only supported in TeamCity builds"}, + {"service message enabled with teamcity envvar and string value", true, true, "testMessage", "key1", "value", &bytes.Buffer{}, &bytes.Buffer{}, "##teamcity[testMessage value]\n", ""}, + {"service message enabled with teamcity envvar and unsupported value", true, true, "testMessage", "key1", []string{"dsdsd"}, &bytes.Buffer{}, &bytes.Buffer{}, "", "Unsupported service message value type"}, } for _, tt := range tests { - viper.Reset() - viper.Set(constants.FlagEnableServiceMessages, tt.servicemessages) - if tt.teamCityEnv { - t.Setenv("TEAMCITY_VERSION", "2021.1") - } else { - t.Setenv("TEAMCITY_VERSION", "") - } + setupArgs(t, constants.FlagEnableServiceMessages, tt.servicemessages) + setupEnvVar(t, "TEAMCITY_VERSION", "2021.1", tt.teamCityEnv) t.Run(tt.name, func(t *testing.T) { - serviceMessageProvider := NewProvider(NewPrinter(tt.stdout, tt.stderr)) - serviceMessageProvider.ServiceMessage(tt.messsageName, tt.value) - + NewProvider(NewOutputPrinter(tt.stdout, tt.stderr)).ServiceMessage(tt.messsageName, tt.value) if tt.want != "" { got := tt.stdout.String() if got != tt.want { @@ -55,3 +48,16 @@ func TestServiceMessage(t *testing.T) { }) } } + +func setupArgs(t *testing.T, key string, value bool) { + viper.Reset() + viper.Set(constants.FlagEnableServiceMessages, value) +} + +func setupEnvVar(t *testing.T, key, value string, set bool) { + if set { + t.Setenv(key, value) + } else { + t.Setenv(key, "") + } +} From fc4decb01904ef567966193a5bc6bd20364761a7 Mon Sep 17 00:00:00 2001 From: Chen Keinan Date: Mon, 12 Jan 2026 10:52:56 +0200 Subject: [PATCH 4/5] feat: add service msg example Signed-off-by: Chen Keinan --- pkg/cmd/release/create/create.go | 1 + pkg/factory/factory.go | 6 +++--- pkg/servicemessages/provider.go | 16 +++++++++++++--- pkg/servicemessages/provider_test.go | 2 +- test/testutil/fakefactory.go | 4 ++-- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/release/create/create.go b/pkg/cmd/release/create/create.go index c363e691..78f01393 100644 --- a/pkg/cmd/release/create/create.go +++ b/pkg/cmd/release/create/create.go @@ -344,6 +344,7 @@ func createRun(cmd *cobra.Command, f factory.Factory, flags *CreateFlags) error } else { cmd.Printf("Successfully created release version %s\n", releaseVersion) } + f.GetServiceMessageProvider().ServiceMessage("setParameter", map[string]string{"name": "octo.releaseNumber", "value": releaseVersion}) } } diff --git a/pkg/factory/factory.go b/pkg/factory/factory.go index 17af12d6..85bda911 100644 --- a/pkg/factory/factory.go +++ b/pkg/factory/factory.go @@ -38,7 +38,7 @@ type Factory interface { BuildVersion() string GetHttpClient() (*http.Client, error) GetConfigProvider() (config.IConfigProvider, error) - GetServiceMessageProvider() (servicemessages.Provider, error) + GetServiceMessageProvider() servicemessages.Provider } func New(clientFactory apiclient.ClientFactory, @@ -106,8 +106,8 @@ func (f *factory) GetConfigProvider() (config.IConfigProvider, error) { return f.config, nil } -func (f *factory) GetServiceMessageProvider() (servicemessages.Provider, error) { - return f.serviceMessageProvider, nil +func (f *factory) GetServiceMessageProvider() servicemessages.Provider { + return f.serviceMessageProvider } // NoSpinner is a static singleton "does nothing" stand-in for spinner if you want to diff --git a/pkg/servicemessages/provider.go b/pkg/servicemessages/provider.go index 4e937b99..d42cc116 100644 --- a/pkg/servicemessages/provider.go +++ b/pkg/servicemessages/provider.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "os" + "strings" "github.com/OctopusDeploy/cli/pkg/constants" "github.com/spf13/viper" @@ -39,9 +40,8 @@ func (p *provider) ServiceMessage(messageName string, values any) { case string: p.printer.Info(fmt.Sprintf("##teamcity[%s %s]\n", messageName, t)) case map[string]string: - for key, value := range t { - p.printer.Info(fmt.Sprintf("##teamcity[%s %s=%s]\n", messageName, key, value)) - } + mapMsg := p.mapToStringMsg(t, messageName) + p.printer.Info(mapMsg) default: p.printer.Error("Unsupported service message value type") } @@ -66,3 +66,13 @@ func (p *OutputPrinter) Info(msg string) { func (p *OutputPrinter) Error(msg string) { fmt.Fprint(p.Err, msg) } + +func (p *provider) mapToStringMsg(m map[string]string, messageName string) string { + var builder strings.Builder + builder.WriteString(fmt.Sprintf("##teamcity[%s", messageName)) + for key, value := range m { + builder.WriteString(fmt.Sprintf(" %s=%s", key, value)) + } + builder.WriteString("]") + return builder.String() +} diff --git a/pkg/servicemessages/provider_test.go b/pkg/servicemessages/provider_test.go index 4975591c..521ceec8 100644 --- a/pkg/servicemessages/provider_test.go +++ b/pkg/servicemessages/provider_test.go @@ -22,7 +22,7 @@ func TestServiceMessage(t *testing.T) { wantErr string }{ {"service message flag is not enabled", false, false, "testMessage", "key1", "value1", &bytes.Buffer{}, &bytes.Buffer{}, "", ""}, - {"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]\n", ""}, + {"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]", ""}, {"service message enabled without teamcity envvar", true, false, "testMessage", "key1", "value1", &bytes.Buffer{}, &bytes.Buffer{}, "", "service messages are only supported in TeamCity builds"}, {"service message enabled with teamcity envvar and string value", true, true, "testMessage", "key1", "value", &bytes.Buffer{}, &bytes.Buffer{}, "##teamcity[testMessage value]\n", ""}, {"service message enabled with teamcity envvar and unsupported value", true, true, "testMessage", "key1", []string{"dsdsd"}, &bytes.Buffer{}, &bytes.Buffer{}, "", "Unsupported service message value type"}, diff --git a/test/testutil/fakefactory.go b/test/testutil/fakefactory.go index a71ee5ff..71fa9177 100644 --- a/test/testutil/fakefactory.go +++ b/test/testutil/fakefactory.go @@ -129,6 +129,6 @@ func (f *MockFactory) Ask(p survey.Prompt, response interface{}, opts ...survey. func (f *MockFactory) GetConfigProvider() (config.IConfigProvider, error) { return f.ConfigProvider, nil } -func (f *MockFactory) GetServiceMessageProvider() (servicemessages.Provider, error) { - return f.serviceMessageProvider, nil +func (f *MockFactory) GetServiceMessageProvider() servicemessages.Provider { + return f.serviceMessageProvider } From 868e72a4103077784f8b40b36316f07cd6fdf24b Mon Sep 17 00:00:00 2001 From: Chen Keinan Date: Mon, 12 Jan 2026 11:57:44 +0200 Subject: [PATCH 5/5] test: fix mock Signed-off-by: Chen Keinan --- test/testutil/fakefactory.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/testutil/fakefactory.go b/test/testutil/fakefactory.go index 71fa9177..25ac3cbf 100644 --- a/test/testutil/fakefactory.go +++ b/test/testutil/fakefactory.go @@ -1,6 +1,7 @@ package testutil import ( + "bytes" "errors" "net/http" "net/url" @@ -55,6 +56,7 @@ func NewMockFactoryWithSpaceAndPrompt(api *MockHttpServer, space *spaces.Space, result := NewMockFactory(api) result.CurrentSpace = space result.AskProvider = askProvider + result.serviceMessageProvider = servicemessages.NewProvider(servicemessages.NewOutputPrinter(&bytes.Buffer{}, &bytes.Buffer{})) return result }