diff --git a/go.mod b/go.mod index 15e8ef823f4c..85ebee3fd8f3 100644 --- a/go.mod +++ b/go.mod @@ -94,6 +94,7 @@ require ( github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/slack-go/slack v0.9.4 github.com/spf13/pflag v1.0.5 // indirect github.com/xanzy/ssh-agent v0.3.0 // indirect go.opencensus.io v0.23.0 // indirect @@ -114,3 +115,5 @@ require ( gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) + +require github.com/gorilla/websocket v1.4.2 // indirect diff --git a/go.sum b/go.sum index 1033f62d78bf..bc55c146c4e7 100644 --- a/go.sum +++ b/go.sum @@ -244,6 +244,7 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= @@ -351,6 +352,7 @@ github.com/goreleaser/fileglob v1.2.0/go.mod h1:rFyb2pXaK3YdnYnSjn6lifw0h2Q6s8Of github.com/goreleaser/nfpm/v2 v2.6.0 h1:bwDU9o4/CVTSpqASJA7+r+rkqpTGamQKYHMRH3wDlRE= github.com/goreleaser/nfpm/v2 v2.6.0/go.mod h1:qaMnjBaZz/2vInOIWx0IbuKuaZpaVB6O8oLG0u4qH1Y= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= @@ -532,6 +534,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/slack-go/slack v0.9.4 h1:C+FC3zLxLxUTQjDy2RZeMHYon005zsCROiZNWVo+opQ= +github.com/slack-go/slack v0.9.4/go.mod h1:wWL//kk0ho+FcQXcBTmEafUI5dz4qz5f4mMk8oIkioQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= diff --git a/internal/pipe/announce/announce.go b/internal/pipe/announce/announce.go index a12b8eafbb15..a486b97f3065 100644 --- a/internal/pipe/announce/announce.go +++ b/internal/pipe/announce/announce.go @@ -3,6 +3,7 @@ package announce import ( "fmt" + "github.com/goreleaser/goreleaser/internal/pipe/slack" "github.com/goreleaser/goreleaser/internal/middleware" "github.com/goreleaser/goreleaser/internal/pipe/twitter" @@ -26,6 +27,7 @@ type Announcer interface { // nolint: gochecknoglobals var announcers = []Announcer{ twitter.Pipe{}, // announce to twitter + slack.Pipe{}, // announce to slack } // Run the pipe. diff --git a/internal/pipe/slack/slack.go b/internal/pipe/slack/slack.go new file mode 100644 index 000000000000..e1c37fdc93c8 --- /dev/null +++ b/internal/pipe/slack/slack.go @@ -0,0 +1,71 @@ +package slack + +import ( + "fmt" + "github.com/slack-go/slack" + + "github.com/apex/log" + "github.com/caarlos0/env/v6" + "github.com/goreleaser/goreleaser/internal/pipe" + "github.com/goreleaser/goreleaser/internal/tmpl" + "github.com/goreleaser/goreleaser/pkg/context" +) + +const ( + defaultUsername = `GoReleaser` + defaultMessageTemplate = `{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .GitURL }}/releases/tag/{{ .Tag }}` +) + +type Pipe struct{} + +func (Pipe) String() string { return "slack" } + +type Config struct { + Webhook string `env:"SLACK_WEBHOOK,notEmpty"` +} + +func (Pipe) Default(ctx *context.Context) error { + if ctx.Config.Announce.Slack.MessageTemplate == "" { + ctx.Config.Announce.Slack.MessageTemplate = defaultMessageTemplate + } + if ctx.Config.Announce.Slack.Username == "" { + ctx.Config.Announce.Slack.Username = defaultUsername + } + return nil +} + +func (Pipe) Announce(ctx *context.Context) error { + if ctx.SkipAnnounce { + return pipe.ErrSkipAnnounceEnabled + } + if !ctx.Config.Announce.Slack.Enabled { + return pipe.ErrSkipDisabledPipe + } + + msg, err := tmpl.New(ctx).Apply(ctx.Config.Announce.Slack.MessageTemplate) + if err != nil { + return fmt.Errorf("announce: failed to announce to slack: %w", err) + } + + var cfg Config + if err := env.Parse(&cfg); err != nil { + return fmt.Errorf("announce: failed to announce to slack: %w", err) + } + + log.Infof("posting: '%s'", msg) + + wm := &slack.WebhookMessage{ + Username: ctx.Config.Announce.Slack.Username, + IconEmoji: ctx.Config.Announce.Slack.IconEmoji, + IconURL: ctx.Config.Announce.Slack.IconURL, + Channel: ctx.Config.Announce.Slack.Channel, + Text: msg, + } + + err = slack.PostWebhook(cfg.Webhook, wm) + if err != nil { + return fmt.Errorf("announce: failed to announce to slack: %w", err) + } + + return nil +} diff --git a/internal/pipe/slack/slack_test.go b/internal/pipe/slack/slack_test.go new file mode 100644 index 000000000000..c95691b4fca8 --- /dev/null +++ b/internal/pipe/slack/slack_test.go @@ -0,0 +1,62 @@ +package slack + +import ( + "testing" + + "github.com/goreleaser/goreleaser/internal/testlib" + "github.com/goreleaser/goreleaser/pkg/config" + "github.com/goreleaser/goreleaser/pkg/context" + "github.com/stretchr/testify/require" +) + +func TestStringer(t *testing.T) { + require.Equal(t, Pipe{}.String(), "slack") +} + +func TestDefault(t *testing.T) { + ctx := context.New(config.Project{}) + require.NoError(t, Pipe{}.Default(ctx)) + require.Equal(t, ctx.Config.Announce.Slack.MessageTemplate, defaultMessageTemplate) +} + +func TestAnnounceDisabled(t *testing.T) { + ctx := context.New(config.Project{}) + require.NoError(t, Pipe{}.Default(ctx)) + testlib.AssertSkipped(t, Pipe{}.Announce(ctx)) +} + +func TestAnnounceInvalidTemplate(t *testing.T) { + ctx := context.New(config.Project{ + Announce: config.Announce{ + Slack: config.Slack{ + Enabled: true, + MessageTemplate: "{{ .Foo }", + }, + }, + }) + require.EqualError(t, Pipe{}.Announce(ctx), `announce: failed to announce to slack: template: tmpl:1: unexpected "}" in operand`) +} + +func TestAnnounceMissingEnv(t *testing.T) { + ctx := context.New(config.Project{ + Announce: config.Announce{ + Slack: config.Slack{ + Enabled: true, + }, + }, + }) + require.NoError(t, Pipe{}.Default(ctx)) + require.EqualError(t, Pipe{}.Announce(ctx), `announce: failed to announce to slack: env: environment variable "SLACK_WEBHOOK" should not be empty`) +} + +func TestAnnounceSkipAnnounce(t *testing.T) { + ctx := context.New(config.Project{ + Announce: config.Announce{ + Slack: config.Slack{ + Enabled: true, + }, + }, + }) + ctx.SkipAnnounce = true + testlib.AssertSkipped(t, Pipe{}.Announce(ctx)) +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 3b289383a968..c2b37f5615de 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -690,6 +690,7 @@ type GoMod struct { type Announce struct { Twitter Twitter `yaml:"twitter,omitempty"` + Slack Slack `yaml:"slack,omitempty"` } type Twitter struct { @@ -697,6 +698,15 @@ type Twitter struct { MessageTemplate string `yaml:"message_template,omitempty"` } +type Slack struct { + Enabled bool `yaml:"enabled,omitempty"` + MessageTemplate string `yaml:"message_template,omitempty"` + Channel string `yaml:"channel,omitempty"` + Username string `yaml:"username,omitempty"` + IconEmoji string `yaml:"icon_emoji,omitempty"` + IconURL string `yaml:"icon_url,omitempty"` +} + // Load config file. func Load(file string) (config Project, err error) { f, err := os.Open(file) // #nosec diff --git a/pkg/defaults/defaults.go b/pkg/defaults/defaults.go index 24da35b4675d..290b073c3223 100644 --- a/pkg/defaults/defaults.go +++ b/pkg/defaults/defaults.go @@ -4,6 +4,7 @@ package defaults import ( "fmt" + "github.com/goreleaser/goreleaser/internal/pipe/slack" "github.com/goreleaser/goreleaser/internal/pipe/archive" "github.com/goreleaser/goreleaser/internal/pipe/artifactory" @@ -57,5 +58,6 @@ var Defaulters = []Defaulter{ brew.Pipe{}, scoop.Pipe{}, twitter.Pipe{}, + slack.Pipe{}, milestone.Pipe{}, } diff --git a/www/docs/customization/announce.md b/www/docs/customization/announce.md index cd3b3681e090..395855379e16 100644 --- a/www/docs/customization/announce.md +++ b/www/docs/customization/announce.md @@ -2,7 +2,7 @@ title: Announce --- -GoReleaser can also announce new releases, currently, to Twitter only. +GoReleaser can also announce new releases, currently, to Twitter and Slack only. It runs at the very end of the pipeline. @@ -30,5 +30,30 @@ announce: message_template: 'Awesome project {{.Tag}} is out!' ``` +## Slack + +For it to work, you'll need to [create a new Incoming Webhook](https://api.slack.com/messaging/webhooks), and set some environment variables on your pipeline: + +- `SLACK_WEBHOOK` +- `SLACK_CHANNEL` +- `SLACK_USERNAME` +- `SLACK_ICON_EMOJI` +- `SLACK_ICON_URL` + +Then, you can add something like the following to your `.goreleaser.yml` config: + +```yaml +# .goreleaser.yml +announce: + slack: + # Wether its enabled or not. + # Defaults to false. + enabled: true + + # Message template to use while publishing. + # Defaults to `{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .GitURL }}/releases/tag/{{ .Tag }}` + message_template: 'Awesome project {{.Tag}} is out!' +``` + !!! tip Learn more about the [name template engine](/customization/templates/).