diff --git a/go.mod b/go.mod index 030bfa7b86c..a7464b7c245 100644 --- a/go.mod +++ b/go.mod @@ -95,6 +95,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 @@ -115,3 +116,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 7b74dc44cff..fb43d449c37 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 707829ab99e..67720af7091 100644 --- a/internal/pipe/announce/announce.go +++ b/internal/pipe/announce/announce.go @@ -6,6 +6,7 @@ import ( "github.com/goreleaser/goreleaser/internal/middleware" "github.com/goreleaser/goreleaser/internal/pipe/reddit" + "github.com/goreleaser/goreleaser/internal/pipe/slack" "github.com/goreleaser/goreleaser/internal/pipe/twitter" "github.com/goreleaser/goreleaser/pkg/context" ) @@ -28,6 +29,7 @@ type Announcer interface { var announcers = []Announcer{ twitter.Pipe{}, // announce to twitter reddit.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 00000000000..e1c37fdc93c --- /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 00000000000..c95691b4fca --- /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 ed50e1cd10b..85cdd8c2bad 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -90,28 +90,27 @@ func (r Repo) String() string { // Homebrew contains the brew section. type Homebrew struct { - Name string `yaml:",omitempty"` - Tap RepoRef `yaml:",omitempty"` - CommitAuthor CommitAuthor `yaml:"commit_author,omitempty"` - CommitMessageTemplate string `yaml:"commit_msg_template,omitempty"` - Folder string `yaml:",omitempty"` - Caveats string `yaml:",omitempty"` - Plist string `yaml:",omitempty"` - Install string `yaml:",omitempty"` - PostInstall string `yaml:"post_install,omitempty"` - Dependencies []HomebrewDependency `yaml:",omitempty"` - Test string `yaml:",omitempty"` - Conflicts []string `yaml:",omitempty"` - Description string `yaml:",omitempty"` - Homepage string `yaml:",omitempty"` - License string `yaml:",omitempty"` - SkipUpload string `yaml:"skip_upload,omitempty"` - DownloadStrategy string `yaml:"download_strategy,omitempty"` - URLTemplate string `yaml:"url_template,omitempty"` - CustomRequire string `yaml:"custom_require,omitempty"` - CustomBlock string `yaml:"custom_block,omitempty"` - IDs []string `yaml:"ids,omitempty"` - Goarm string `yaml:"goarm,omitempty"` + Name string `yaml:",omitempty"` + Tap RepoRef `yaml:",omitempty"` + CommitAuthor CommitAuthor `yaml:"commit_author,omitempty"` + Folder string `yaml:",omitempty"` + Caveats string `yaml:",omitempty"` + Plist string `yaml:",omitempty"` + Install string `yaml:",omitempty"` + PostInstall string `yaml:"post_install,omitempty"` + Dependencies []HomebrewDependency `yaml:",omitempty"` + Test string `yaml:",omitempty"` + Conflicts []string `yaml:",omitempty"` + Description string `yaml:",omitempty"` + Homepage string `yaml:",omitempty"` + License string `yaml:",omitempty"` + SkipUpload string `yaml:"skip_upload,omitempty"` + DownloadStrategy string `yaml:"download_strategy,omitempty"` + URLTemplate string `yaml:"url_template,omitempty"` + CustomRequire string `yaml:"custom_require,omitempty"` + CustomBlock string `yaml:"custom_block,omitempty"` + IDs []string `yaml:"ids,omitempty"` + Goarm string `yaml:"goarm,omitempty"` } // Scoop contains the scoop.sh section. @@ -692,6 +691,7 @@ type GoMod struct { type Announce struct { Twitter Twitter `yaml:"twitter,omitempty"` Reddit Reddit `yaml:"reddit,omitempty"` + Slack Slack `yaml:"slack,omitempty"` } type Twitter struct { @@ -708,6 +708,15 @@ type Reddit struct { Sub string `yaml:"sub,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 8bd97e8dd07..505e3d68288 100644 --- a/pkg/defaults/defaults.go +++ b/pkg/defaults/defaults.go @@ -17,6 +17,7 @@ import ( "github.com/goreleaser/goreleaser/internal/pipe/nfpm" "github.com/goreleaser/goreleaser/internal/pipe/project" "github.com/goreleaser/goreleaser/internal/pipe/reddit" + "github.com/goreleaser/goreleaser/internal/pipe/slack" "github.com/goreleaser/goreleaser/internal/pipe/release" "github.com/goreleaser/goreleaser/internal/pipe/scoop" "github.com/goreleaser/goreleaser/internal/pipe/sign" @@ -59,5 +60,6 @@ var Defaulters = []Defaulter{ scoop.Pipe{}, twitter.Pipe{}, reddit.Pipe{}, + slack.Pipe{}, milestone.Pipe{}, } diff --git a/www/docs/customization/announce.md b/www/docs/customization/announce.md index f2281830a1a..1534cb00c46 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,6 +30,39 @@ 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` + +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!' + + # The name of the channel that the user selected as a destination for webhook messages. + channel: '#channel' + + # Set your Webhook's user name. + username: '' + + # Emoji to use as the icon for this message. Overrides icon_url. + icon_emoji: '' + + # URL to an image to use as the icon for this message. + icon_url: '' +``` + !!! tip Learn more about the [name template engine](/customization/templates/).