Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 5 additions & 3 deletions cmd/gen-docs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package main

import (
"fmt"
"github.com/OctopusDeploy/cli/pkg/config"
"github.com/spf13/viper"
"io"
"os"
"os/user"
Expand All @@ -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"
Expand Down Expand Up @@ -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.NewOutputPrinter(os.Stdout, os.Stderr)))

cmd := root.NewCmdRoot(f, clientFactory, askProvider)
cmd.DisableAutoGenTag = true
Expand Down
14 changes: 10 additions & 4 deletions cmd/octopus/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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.NewOutputPrinter(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)
Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/release/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added for usage example

}
}

Expand Down
4 changes: 4 additions & 0 deletions pkg/cmd/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
11 changes: 6 additions & 5 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 24 additions & 11 deletions pkg/factory/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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 {
Expand All @@ -36,15 +38,22 @@ type Factory interface {
BuildVersion() string
GetHttpClient() (*http.Client, error)
GetConfigProvider() (config.IConfigProvider, error)
GetServiceMessageProvider() servicemessages.Provider
}

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,
}
}

Expand Down Expand Up @@ -97,6 +106,10 @@ func (f *factory) GetConfigProvider() (config.IConfigProvider, error) {
return f.config, 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
// call an API that expects a spinner while you're in automation mode.
var NoSpinner Spinner = &noSpinner{}
Expand Down
78 changes: 78 additions & 0 deletions pkg/servicemessages/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package servicemessages

import (
"fmt"
"io"
"os"
"strings"

"github.com/OctopusDeploy/cli/pkg/constants"
"github.com/spf13/viper"
)

type Provider interface {
ServiceMessage(messageName string, values any)
}

type provider struct {
printer *OutputPrinter
}

func NewProvider(printer *OutputPrinter) Provider {
return &provider{
printer: printer,
}
}

func (p *provider) ServiceMessage(messageName string, values any) {
serviceMessageEnabled := viper.GetBool(constants.FlagEnableServiceMessages)
if !serviceMessageEnabled {
return
}

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.Info(fmt.Sprintf("##teamcity[%s %s]\n", messageName, t))
case map[string]string:
mapMsg := p.mapToStringMsg(t, messageName)
p.printer.Info(mapMsg)
default:
p.printer.Error("Unsupported service message value type")
}
}

type OutputPrinter struct {
Out io.Writer
Err io.Writer
}

func NewOutputPrinter(out io.Writer, err io.Writer) *OutputPrinter {
return &OutputPrinter{
Out: out,
Err: err,
}
}

func (p *OutputPrinter) Info(msg string) {
fmt.Fprint(p.Out, msg)
}

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()
}
63 changes: 63 additions & 0 deletions pkg/servicemessages/provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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 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]", ""},
{"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 {
setupArgs(t, constants.FlagEnableServiceMessages, tt.servicemessages)
setupEnvVar(t, "TEAMCITY_VERSION", "2021.1", tt.teamCityEnv)
t.Run(tt.name, func(t *testing.T) {
NewProvider(NewOutputPrinter(tt.stdout, tt.stderr)).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)
}
}
})
}
}

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, "")
}
}
21 changes: 14 additions & 7 deletions test/testutil/fakefactory.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package testutil

import (
"bytes"
"errors"
"net/http"
"net/url"
Expand All @@ -10,6 +11,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"
)
Expand Down Expand Up @@ -54,17 +56,19 @@ 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
}

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?)
Expand Down Expand Up @@ -127,3 +131,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 {
return f.serviceMessageProvider
}