You've already forked goreleaser
mirror of
https://github.com/goreleaser/goreleaser.git
synced 2025-07-17 01:42:37 +02:00
feat(announce): added Slack notification options (#2988)
* 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>
This commit is contained in:
@ -1,6 +1,7 @@
|
|||||||
package slack
|
package slack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
@ -47,12 +48,22 @@ func (Pipe) Announce(ctx *context.Context) error {
|
|||||||
|
|
||||||
log.Infof("posting: '%s'", msg)
|
log.Infof("posting: '%s'", msg)
|
||||||
|
|
||||||
|
// optional processing of advanced formatting options
|
||||||
|
blocks, attachments, err := parseAdvancedFormatting(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
wm := &slack.WebhookMessage{
|
wm := &slack.WebhookMessage{
|
||||||
Username: ctx.Config.Announce.Slack.Username,
|
Username: ctx.Config.Announce.Slack.Username,
|
||||||
IconEmoji: ctx.Config.Announce.Slack.IconEmoji,
|
IconEmoji: ctx.Config.Announce.Slack.IconEmoji,
|
||||||
IconURL: ctx.Config.Announce.Slack.IconURL,
|
IconURL: ctx.Config.Announce.Slack.IconURL,
|
||||||
Channel: ctx.Config.Announce.Slack.Channel,
|
Channel: ctx.Config.Announce.Slack.Channel,
|
||||||
Text: msg,
|
Text: msg,
|
||||||
|
|
||||||
|
// optional enrichments
|
||||||
|
Blocks: blocks,
|
||||||
|
Attachments: attachments,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = slack.PostWebhook(cfg.Webhook, wm)
|
err = slack.PostWebhook(cfg.Webhook, wm)
|
||||||
@ -62,3 +73,43 @@ func (Pipe) Announce(ctx *context.Context) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseAdvancedFormatting(ctx *context.Context) (*slack.Blocks, []slack.Attachment, error) {
|
||||||
|
var blocks *slack.Blocks
|
||||||
|
if in := ctx.Config.Announce.Slack.Blocks; len(in) > 0 {
|
||||||
|
blocks = &slack.Blocks{BlockSet: make([]slack.Block, 0, len(in))}
|
||||||
|
|
||||||
|
if err := unmarshal(ctx, in, blocks); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("announce: slack blocks: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var attachments []slack.Attachment
|
||||||
|
if in := ctx.Config.Announce.Slack.Attachments; len(in) > 0 {
|
||||||
|
attachments = make([]slack.Attachment, 0, len(in))
|
||||||
|
|
||||||
|
if err := unmarshal(ctx, in, &attachments); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("announce: slack attachments: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks, attachments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshal(ctx *context.Context, in interface{}, target interface{}) error {
|
||||||
|
jazon, err := json.Marshal(in)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("announce: failed to marshal input as JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tplApplied, err := tmpl.New(ctx).Apply(string(jazon))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("announce: failed to evaluate template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal([]byte(tplApplied), target); err != nil {
|
||||||
|
return fmt.Errorf("announce: failed to unmarshal into target: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
package slack
|
package slack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goreleaser/goreleaser/internal/yaml"
|
||||||
"github.com/goreleaser/goreleaser/pkg/config"
|
"github.com/goreleaser/goreleaser/pkg/config"
|
||||||
"github.com/goreleaser/goreleaser/pkg/context"
|
"github.com/goreleaser/goreleaser/pkg/context"
|
||||||
|
"github.com/slack-go/slack"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -55,3 +59,292 @@ func TestSkip(t *testing.T) {
|
|||||||
require.False(t, Pipe{}.Skip(ctx))
|
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(" "))
|
||||||
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@ -985,6 +986,8 @@ type Slack struct {
|
|||||||
Username string `yaml:"username,omitempty"`
|
Username string `yaml:"username,omitempty"`
|
||||||
IconEmoji string `yaml:"icon_emoji,omitempty"`
|
IconEmoji string `yaml:"icon_emoji,omitempty"`
|
||||||
IconURL string `yaml:"icon_url,omitempty"`
|
IconURL string `yaml:"icon_url,omitempty"`
|
||||||
|
Blocks []SlackBlock `yaml:"blocks,omitempty"`
|
||||||
|
Attachments []SlackAttachment `yaml:"attachments,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Discord struct {
|
type Discord struct {
|
||||||
@ -1058,3 +1061,47 @@ func LoadReader(fd io.Reader) (config Project, err error) {
|
|||||||
log.WithField("config", config).Debug("loaded config file")
|
log.WithField("config", config).Debug("loaded config file")
|
||||||
return config, err
|
return config, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SlackBlock represents the untyped structure of a rich slack message layout.
|
||||||
|
type SlackBlock struct {
|
||||||
|
Internal interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML is a custom unmarshaler that unmarshals a YAML slack block as untyped interface{}.
|
||||||
|
func (a *SlackBlock) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
var yamlv2 interface{}
|
||||||
|
if err := unmarshal(&yamlv2); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Internal = yamlv2
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON marshals a slack block as JSON.
|
||||||
|
func (a SlackBlock) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(a.Internal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SlackAttachment represents the untyped structure of a slack message attachment.
|
||||||
|
type SlackAttachment struct {
|
||||||
|
Internal interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML is a custom unmarshaler that unmarshals a YAML slack attachment as untyped interface{}.
|
||||||
|
func (a *SlackAttachment) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
var yamlv2 interface{}
|
||||||
|
if err := unmarshal(&yamlv2); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Internal = yamlv2
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON marshals a slack attachment as JSON.
|
||||||
|
func (a SlackAttachment) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(a.Internal)
|
||||||
|
}
|
||||||
|
246
pkg/config/config_slack_test.go
Normal file
246
pkg/config/config_slack_test.go
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUnmarshalSlackBlocks(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("valid blocks", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
prop, err := LoadReader(goodBlocksSlackConf())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedBlocks := []SlackBlock{
|
||||||
|
{
|
||||||
|
Internal: map[string]interface{}{
|
||||||
|
"type": "header",
|
||||||
|
"text": map[string]interface{}{
|
||||||
|
"type": "plain_text",
|
||||||
|
"text": "{{ .Version }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Internal: map[string]interface{}{
|
||||||
|
"text": map[string]interface{}{
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": "Heading\n=======\n\n**Bold**\n",
|
||||||
|
},
|
||||||
|
"type": "section",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// assert Unmarshal from YAML
|
||||||
|
require.Equal(t, expectedBlocks, prop.Announce.Slack.Blocks)
|
||||||
|
|
||||||
|
jazon, err := json.Marshal(prop.Announce.Slack.Blocks)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var untyped []SlackBlock
|
||||||
|
require.NoError(t, json.Unmarshal(jazon, &untyped))
|
||||||
|
|
||||||
|
// assert that JSON Marshal didn't alter the struct
|
||||||
|
require.Equal(t, expectedBlocks, prop.Announce.Slack.Blocks)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid blocks", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := LoadReader(badBlocksSlackConf())
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalSlackAttachments(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("valid attachments", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
prop, err := LoadReader(goodAttachmentsSlackConf())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedAttachments := []SlackAttachment{
|
||||||
|
{
|
||||||
|
Internal: map[string]interface{}{
|
||||||
|
"color": "#46a64f",
|
||||||
|
"fields": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"short": false,
|
||||||
|
"title": "field 1",
|
||||||
|
"value": "value 1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"footer": "a footer",
|
||||||
|
"mrkdwn_in": []interface{}{
|
||||||
|
"text",
|
||||||
|
},
|
||||||
|
"pretext": "optional",
|
||||||
|
"text": "another",
|
||||||
|
"title": "my_title",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// assert Unmarshal from YAML
|
||||||
|
require.Equal(t, expectedAttachments, prop.Announce.Slack.Attachments)
|
||||||
|
|
||||||
|
jazon, err := json.Marshal(prop.Announce.Slack.Attachments)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var untyped []SlackAttachment
|
||||||
|
require.NoError(t, json.Unmarshal(jazon, &untyped))
|
||||||
|
|
||||||
|
// assert that JSON Marshal didn't alter the struct
|
||||||
|
require.Equal(t, expectedAttachments, prop.Announce.Slack.Attachments)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid attachments", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := LoadReader(badAttachmentsSlackConf())
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalYAMLSlackBlocks(t *testing.T) {
|
||||||
|
// func (a *SlackAttachment) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const testError = "testError"
|
||||||
|
erf := func(_ interface{}) error {
|
||||||
|
return errors.New(testError)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("SlackBlock.UnmarshalYAML error case", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var block SlackBlock
|
||||||
|
err := block.UnmarshalYAML(erf)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorContains(t, err, testError)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SlackAttachment.UnmarshalYAML error case", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var attachment SlackAttachment
|
||||||
|
err := attachment.UnmarshalYAML(erf)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorContains(t, err, testError)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func goodBlocksSlackConf() io.Reader {
|
||||||
|
const conf = `
|
||||||
|
announce:
|
||||||
|
slack:
|
||||||
|
enabled: true
|
||||||
|
username: my_user
|
||||||
|
message_template: fallback
|
||||||
|
channel: my_channel
|
||||||
|
blocks:
|
||||||
|
- type: header
|
||||||
|
text:
|
||||||
|
type: plain_text
|
||||||
|
text: '{{ .Version }}'
|
||||||
|
- type: section
|
||||||
|
text:
|
||||||
|
type: mrkdwn
|
||||||
|
text: |
|
||||||
|
Heading
|
||||||
|
=======
|
||||||
|
|
||||||
|
**Bold**
|
||||||
|
`
|
||||||
|
|
||||||
|
buf := bytes.NewBufferString(conf)
|
||||||
|
|
||||||
|
return bytes.NewReader(bytes.ReplaceAll(buf.Bytes(), []byte("\t"), []byte(" ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func badBlocksSlackConf() io.Reader {
|
||||||
|
const conf = `
|
||||||
|
announce:
|
||||||
|
slack:
|
||||||
|
enabled: true
|
||||||
|
username: my_user
|
||||||
|
message_template: fallback
|
||||||
|
channel: my_channel
|
||||||
|
blocks:
|
||||||
|
type: header
|
||||||
|
text:
|
||||||
|
type: plain_text
|
||||||
|
text: '{{ .Version }}'
|
||||||
|
type: section
|
||||||
|
text:
|
||||||
|
type: mrkdwn
|
||||||
|
text: |
|
||||||
|
**Bold**
|
||||||
|
`
|
||||||
|
|
||||||
|
buf := bytes.NewBufferString(conf)
|
||||||
|
|
||||||
|
return bytes.NewReader(bytes.ReplaceAll(buf.Bytes(), []byte("\t"), []byte(" ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func goodAttachmentsSlackConf() io.Reader {
|
||||||
|
const conf = `
|
||||||
|
announce:
|
||||||
|
slack:
|
||||||
|
enabled: true
|
||||||
|
username: my_user
|
||||||
|
message_template: fallback
|
||||||
|
channel: my_channel
|
||||||
|
attachments:
|
||||||
|
- mrkdwn_in: ["text"]
|
||||||
|
color: '#46a64f'
|
||||||
|
pretext: optional
|
||||||
|
title: my_title
|
||||||
|
text: another
|
||||||
|
fields:
|
||||||
|
- title: field 1
|
||||||
|
value: value 1
|
||||||
|
short: false
|
||||||
|
footer: a footer
|
||||||
|
`
|
||||||
|
|
||||||
|
buf := bytes.NewBufferString(conf)
|
||||||
|
|
||||||
|
return bytes.NewReader(bytes.ReplaceAll(buf.Bytes(), []byte("\t"), []byte(" ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func badAttachmentsSlackConf() io.Reader {
|
||||||
|
const conf = `
|
||||||
|
announce:
|
||||||
|
slack:
|
||||||
|
enabled: true
|
||||||
|
username: my_user
|
||||||
|
message_template: fallback
|
||||||
|
channel: my_channel
|
||||||
|
attachments:
|
||||||
|
key:
|
||||||
|
mrkdwn_in: ["text"]
|
||||||
|
color: #46a64f
|
||||||
|
pretext: optional
|
||||||
|
title: my_title
|
||||||
|
text: another
|
||||||
|
fields:
|
||||||
|
- title: field 1
|
||||||
|
value: value 1
|
||||||
|
short: false
|
||||||
|
footer: a footer
|
||||||
|
`
|
||||||
|
|
||||||
|
buf := bytes.NewBufferString(conf)
|
||||||
|
|
||||||
|
return bytes.NewReader(bytes.ReplaceAll(buf.Bytes(), []byte("\t"), []byte(" ")))
|
||||||
|
}
|
@ -30,6 +30,23 @@ announce:
|
|||||||
|
|
||||||
# URL to an image to use as the icon for this message.
|
# URL to an image to use as the icon for this message.
|
||||||
icon_url: ''
|
icon_url: ''
|
||||||
|
|
||||||
|
# Blocks for advanced formatting, see: https://api.slack.com/messaging/webhooks#advanced_message_formatting
|
||||||
|
# and https://api.slack.com/messaging/composing/layouts#adding-blocks.
|
||||||
|
#
|
||||||
|
# Templating is possible inside this structure.
|
||||||
|
#
|
||||||
|
# Attention: goreleaser doesn't check the full structure of the Slack API: please make sure that
|
||||||
|
# your configuration for advanced message formatting abides by this API.
|
||||||
|
blocks: []
|
||||||
|
|
||||||
|
# Attachments, see: https://api.slack.com/reference/messaging/attachments
|
||||||
|
#
|
||||||
|
# Templating is possible inside this structure.
|
||||||
|
#
|
||||||
|
# Attention: goreleaser doesn't check the full structure of the Slack API: please make sure that
|
||||||
|
# your configuration for advanced message formatting abides by this API.
|
||||||
|
attachments: []
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
|
Reference in New Issue
Block a user