mirror of
https://github.com/goreleaser/goreleaser.git
synced 2025-01-24 04:16:27 +02:00
905a1640f1
* feat(announce): added Slack notification options This feature adds support for specifying a richer content in Slack announcements. We may now specify "blocks" and "attachments" to produce better-looking announcement messages. * fixes #2986 The goreleaser configuration only exposes the top-level structures and does not check the validity of the Slack API internal structures. This way, we do not inject hard dependencies on changes in the Slack API. Notice: untyped config parsing introduces a little hack to have yaml and JSON marshaling work together properly. This hack won't be necessary with yaml.v3. How this has been tested? ------------------------- * Added unit tests for the config parsing * Added a (skipped) e2e test. For now, this requires a valid Slack webhook, so I've been able to test this manually. Signed-off-by: Frederic BIDON <fredbi@yahoo.com> * added more unit tests Signed-off-by: Frederic BIDON <fredbi@yahoo.com> * removed yaml.v2 hack Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
351 lines
7.9 KiB
Go
351 lines
7.9 KiB
Go
package slack
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/goreleaser/goreleaser/internal/yaml"
|
|
"github.com/goreleaser/goreleaser/pkg/config"
|
|
"github.com/goreleaser/goreleaser/pkg/context"
|
|
"github.com/slack-go/slack"
|
|
"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 TestAnnounceInvalidTemplate(t *testing.T) {
|
|
ctx := context.New(config.Project{
|
|
Announce: config.Announce{
|
|
Slack: config.Slack{
|
|
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{},
|
|
},
|
|
})
|
|
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 TestSkip(t *testing.T) {
|
|
t.Run("skip", func(t *testing.T) {
|
|
require.True(t, Pipe{}.Skip(context.New(config.Project{})))
|
|
})
|
|
|
|
t.Run("dont skip", func(t *testing.T) {
|
|
ctx := context.New(config.Project{
|
|
Announce: config.Announce{
|
|
Slack: config.Slack{
|
|
Enabled: true,
|
|
},
|
|
},
|
|
})
|
|
require.False(t, Pipe{}.Skip(ctx))
|
|
})
|
|
}
|
|
|
|
const testVersion = "v1.2.3"
|
|
|
|
func TestParseRichText(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("parse only - full slack config with blocks and attachments", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var project config.Project
|
|
require.NoError(t, yaml.Unmarshal(goodRichSlackConf(), &project))
|
|
|
|
ctx := context.New(project)
|
|
ctx.Version = testVersion
|
|
|
|
blocks, attachments, err := parseAdvancedFormatting(ctx)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, blocks.BlockSet, 4)
|
|
require.Len(t, attachments, 2)
|
|
})
|
|
|
|
t.Run("parse only - slack config with bad blocks", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var project config.Project
|
|
require.NoError(t, yaml.Unmarshal(badBlocksSlackConf(), &project))
|
|
|
|
ctx := context.New(project)
|
|
ctx.Version = testVersion
|
|
|
|
_, _, err := parseAdvancedFormatting(ctx)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "json")
|
|
})
|
|
|
|
t.Run("parse only - slack config with bad attachments", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var project config.Project
|
|
require.NoError(t, yaml.Unmarshal(badAttachmentsSlackConf(), &project))
|
|
|
|
ctx := context.New(project)
|
|
ctx.Version = testVersion
|
|
|
|
_, _, err := parseAdvancedFormatting(ctx)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "json")
|
|
})
|
|
}
|
|
|
|
func TestRichText(t *testing.T) {
|
|
t.Parallel()
|
|
os.Setenv("SLACK_WEBHOOK", slackTestHook())
|
|
|
|
t.Run("e2e - full slack config with blocks and attachments", func(t *testing.T) {
|
|
t.SkipNow() // requires a valid webhook for integration testing
|
|
t.Parallel()
|
|
|
|
var project config.Project
|
|
require.NoError(t, yaml.Unmarshal(goodRichSlackConf(), &project))
|
|
|
|
ctx := context.New(project)
|
|
ctx.Version = testVersion
|
|
|
|
require.NoError(t, Pipe{}.Announce(ctx))
|
|
})
|
|
|
|
t.Run("slack config with bad blocks", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var project config.Project
|
|
require.NoError(t, yaml.Unmarshal(badBlocksSlackConf(), &project))
|
|
|
|
ctx := context.New(project)
|
|
ctx.Version = testVersion
|
|
|
|
err := Pipe{}.Announce(ctx)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "json")
|
|
})
|
|
}
|
|
|
|
func TestUnmarshall(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("happy unmarshal", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.New(config.Project{})
|
|
ctx.Version = testVersion
|
|
|
|
var blocks slack.Blocks
|
|
require.NoError(t, unmarshal(ctx, []interface{}{map[string]interface{}{"type": "divider"}}, &blocks))
|
|
})
|
|
|
|
t.Run("unmarshal fails on MarshalJSON", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.New(config.Project{})
|
|
ctx.Version = testVersion
|
|
|
|
var blocks slack.Blocks
|
|
require.Error(t, unmarshal(ctx, []interface{}{map[string]interface{}{"type": func() {}}}, &blocks))
|
|
})
|
|
|
|
t.Run("unmarshal happy to resolve template", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var project config.Project
|
|
require.NoError(t, yaml.Unmarshal(goodTemplateSlackConf(), &project))
|
|
ctx := context.New(project)
|
|
ctx.Version = testVersion
|
|
|
|
var blocks slack.Blocks
|
|
require.NoError(t, unmarshal(ctx, ctx.Config.Announce.Slack.Blocks, &blocks))
|
|
|
|
require.Len(t, blocks.BlockSet, 1)
|
|
header, ok := blocks.BlockSet[0].(*slack.HeaderBlock)
|
|
require.True(t, ok)
|
|
require.Contains(t, header.Text.Text, testVersion)
|
|
})
|
|
|
|
t.Run("unmarshal fails on resolve template", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var project config.Project
|
|
require.NoError(t, yaml.Unmarshal(badTemplateSlackConf(), &project))
|
|
ctx := context.New(project)
|
|
ctx.Version = testVersion
|
|
|
|
var blocks slack.Blocks
|
|
require.Error(t, unmarshal(ctx, ctx.Config.Announce.Slack.Blocks, &blocks))
|
|
})
|
|
}
|
|
|
|
func slackTestHook() string {
|
|
// redacted: replace this by a real Slack Web Incoming Hook to test the featue end to end.
|
|
const hook = "https://hooks.slack.com/services/*********/***********/************************"
|
|
|
|
return hook
|
|
}
|
|
|
|
func goodRichSlackConf() []byte {
|
|
const conf = `
|
|
project_name: test
|
|
announce:
|
|
slack:
|
|
enabled: true
|
|
message_template: fallback
|
|
channel: my_channel
|
|
blocks:
|
|
- type: header
|
|
text:
|
|
type: plain_text
|
|
text: '{{ .Version }}'
|
|
- type: section
|
|
text:
|
|
type: mrkdwn
|
|
text: |
|
|
Heading
|
|
=======
|
|
|
|
# Other Heading
|
|
|
|
*Bold*
|
|
_italic_
|
|
~Strikethrough~
|
|
|
|
## Heading 2
|
|
### Heading 3
|
|
* List item 1
|
|
* List item 2
|
|
|
|
- List item 3
|
|
- List item 4
|
|
|
|
[link](https://example.com)
|
|
<https://example.com|link>
|
|
|
|
:)
|
|
|
|
:star:
|
|
|
|
- type: divider
|
|
- type: section
|
|
text:
|
|
type: mrkdwn
|
|
text: |
|
|
my release
|
|
attachments:
|
|
-
|
|
title: Release artifacts
|
|
color: '#2eb886'
|
|
text: |
|
|
*Helm chart packages*
|
|
- fallback: full changelog
|
|
color: '#2eb886'
|
|
title: Full Change Log
|
|
text: |
|
|
* this link
|
|
* that link
|
|
`
|
|
|
|
buf := bytes.NewBufferString(conf)
|
|
|
|
return bytes.ReplaceAll(buf.Bytes(), []byte("\t"), []byte(" "))
|
|
}
|
|
|
|
func badBlocksSlackConf() []byte {
|
|
const conf = `
|
|
project_name: test
|
|
announce:
|
|
slack:
|
|
enabled: true
|
|
message_template: fallback
|
|
channel: my_channel
|
|
blocks:
|
|
- type: header
|
|
text: invalid # <- wrong type for Slack API
|
|
`
|
|
|
|
buf := bytes.NewBufferString(conf)
|
|
|
|
return bytes.ReplaceAll(buf.Bytes(), []byte("\t"), []byte(" "))
|
|
}
|
|
|
|
func badAttachmentsSlackConf() []byte {
|
|
const conf = `
|
|
project_name: test
|
|
announce:
|
|
slack:
|
|
enabled: true
|
|
message_template: fallback
|
|
channel: my_channel
|
|
attachments:
|
|
-
|
|
title:
|
|
- Release artifacts
|
|
- wrong # <- title is not an array
|
|
color: '#2eb886'
|
|
text: |
|
|
*Helm chart packages*
|
|
`
|
|
|
|
buf := bytes.NewBufferString(conf)
|
|
|
|
return bytes.ReplaceAll(buf.Bytes(), []byte("\t"), []byte(" "))
|
|
}
|
|
|
|
func goodTemplateSlackConf() []byte {
|
|
const conf = `
|
|
project_name: test
|
|
announce:
|
|
slack:
|
|
enabled: true
|
|
message_template: '{{ .Version }}'
|
|
channel: my_channel
|
|
blocks:
|
|
- type: header
|
|
text:
|
|
type: plain_text
|
|
text: '{{ .Version }}'
|
|
`
|
|
|
|
buf := bytes.NewBufferString(conf)
|
|
|
|
return bytes.ReplaceAll(buf.Bytes(), []byte("\t"), []byte(" "))
|
|
}
|
|
|
|
func badTemplateSlackConf() []byte {
|
|
const conf = `
|
|
project_name: test
|
|
announce:
|
|
slack:
|
|
enabled: true
|
|
message_template: fallback
|
|
channel: my_channel
|
|
blocks:
|
|
- type: header
|
|
text:
|
|
type: plain_text
|
|
text: '{{ .Wrong }}'
|
|
`
|
|
|
|
buf := bytes.NewBufferString(conf)
|
|
|
|
return bytes.ReplaceAll(buf.Bytes(), []byte("\t"), []byte(" "))
|
|
}
|