Skip to content

Commit

Permalink
feat: Slack notifications matched on base branch name (#3644)
Browse files Browse the repository at this point in the history
* Add ability to send slack notification matched on base branch name

* Update BranchRegex usage doc

* Add test with invalid branch and workspace regex
  • Loading branch information
smstone authored Aug 22, 2023
1 parent 2777ad0 commit b8bb6df
Show file tree
Hide file tree
Showing 8 changed files with 67 additions and 10 deletions.
2 changes: 2 additions & 0 deletions runatlantis.io/docs/using-slack-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ In your Atlantis configuration you can now add the following:
webhooks:
- event: apply
workspace-regex: .*
branch-regex: .*
kind: slack
channel: my-channel
```
Expand All @@ -56,6 +57,7 @@ config: |
webhooks:
- event: apply
workspace-regex: .*
branch-regex: .*
kind: slack
channel: my-channel
```
Expand Down
10 changes: 6 additions & 4 deletions server/events/webhooks/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,26 @@ import (
type SlackWebhook struct {
Client SlackClient
WorkspaceRegex *regexp.Regexp
BranchRegex *regexp.Regexp
Channel string
}

func NewSlack(r *regexp.Regexp, channel string, client SlackClient) (*SlackWebhook, error) {
func NewSlack(wr *regexp.Regexp, br *regexp.Regexp, channel string, client SlackClient) (*SlackWebhook, error) {
if err := client.AuthTest(); err != nil {
return nil, fmt.Errorf("testing slack authentication: %s. Verify your slack-token is valid", err)
}

return &SlackWebhook{
Client: client,
WorkspaceRegex: r,
WorkspaceRegex: wr,
BranchRegex: br,
Channel: channel,
}, nil
}

// Send sends the webhook to Slack if the workspace matches the regex.
// Send sends the webhook to Slack if workspace and branch matches their respective regex.
func (s *SlackWebhook) Send(log logging.SimpleLogging, applyResult ApplyResult) error {
if !s.WorkspaceRegex.MatchString(applyResult.Workspace) {
if !s.WorkspaceRegex.MatchString(applyResult.Workspace) || !s.BranchRegex.MatchString(applyResult.Pull.BaseBranch) {
return nil
}
return s.Client.PostMessage(s.Channel, applyResult)
Expand Down
5 changes: 5 additions & 0 deletions server/events/webhooks/slack_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ func (d *DefaultSlackClient) createAttachments(applyResult ApplyResult) []slack.
Value: applyResult.Workspace,
Short: true,
},
{
Title: "Branch",
Value: applyResult.Pull.BaseBranch,
Short: true,
},
{
Title: "User",
Value: applyResult.User.Username,
Expand Down
5 changes: 3 additions & 2 deletions server/events/webhooks/slack_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,9 @@ func setup(t *testing.T) {
FullName: "runatlantis/atlantis",
},
Pull: models.PullRequest{
Num: 1,
URL: "url",
Num: 1,
URL: "url",
BaseBranch: "main",
},
User: models.User{
Username: "lkysow",
Expand Down
9 changes: 9 additions & 0 deletions server/events/webhooks/slack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"testing"

. "github.com/petergtz/pegomock/v4"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/webhooks"
"github.com/runatlantis/atlantis/server/events/webhooks/mocks"
"github.com/runatlantis/atlantis/server/logging"
Expand All @@ -35,10 +36,14 @@ func TestSend_PostMessage(t *testing.T) {
hook := webhooks.SlackWebhook{
Client: client,
WorkspaceRegex: regex,
BranchRegex: regex,
Channel: channel,
}
result := webhooks.ApplyResult{
Workspace: "production",
Pull: models.PullRequest{
BaseBranch: "main",
},
}

t.Log("PostMessage should be called, doesn't matter if it errors or not")
Expand All @@ -57,10 +62,14 @@ func TestSend_NoopSuccess(t *testing.T) {
hook := webhooks.SlackWebhook{
Client: client,
WorkspaceRegex: regex,
BranchRegex: regex,
Channel: channel,
}
result := webhooks.ApplyResult{
Workspace: "production",
Pull: models.PullRequest{
BaseBranch: "main",
},
}
err = hook.Send(logging.NewNoopLogger(t), result)
Ok(t, err)
Expand Down
9 changes: 7 additions & 2 deletions server/events/webhooks/webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,19 @@ type MultiWebhookSender struct {
type Config struct {
Event string
WorkspaceRegex string
BranchRegex string
Kind string
Channel string
}

func NewMultiWebhookSender(configs []Config, client SlackClient) (*MultiWebhookSender, error) {
var webhooks []Sender
for _, c := range configs {
r, err := regexp.Compile(c.WorkspaceRegex)
wr, err := regexp.Compile(c.WorkspaceRegex)
if err != nil {
return nil, err
}
br, err := regexp.Compile(c.BranchRegex)
if err != nil {
return nil, err
}
Expand All @@ -77,7 +82,7 @@ func NewMultiWebhookSender(configs []Config, client SlackClient) (*MultiWebhookS
if c.Channel == "" {
return nil, errors.New("must specify \"channel\" if using a webhook of \"kind: slack\"")
}
slack, err := NewSlack(r, c.Channel, client)
slack, err := NewSlack(wr, br, c.Channel, client)
if err != nil {
return nil, err
}
Expand Down
32 changes: 30 additions & 2 deletions server/events/webhooks/webhooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
var validConfig = webhooks.Config{
Event: validEvent,
WorkspaceRegex: validRegex,
BranchRegex: validRegex,
Kind: validKind,
Channel: validChannel,
}
Expand All @@ -42,8 +43,8 @@ func validConfigs() []webhooks.Config {
return []webhooks.Config{validConfig}
}

func TestNewWebhooksManager_InvalidRegex(t *testing.T) {
t.Log("When given an invalid regex in a config, an error is returned")
func TestNewWebhooksManager_InvalidWorkspaceRegex(t *testing.T) {
t.Log("When given an invalid workspace regex in a config, an error is returned")
RegisterMockTestingT(t)
client := mocks.NewMockSlackClient()

Expand All @@ -55,6 +56,33 @@ func TestNewWebhooksManager_InvalidRegex(t *testing.T) {
Assert(t, strings.Contains(err.Error(), "error parsing regexp"), "expected regex error")
}

func TestNewWebhooksManager_InvalidBranchRegex(t *testing.T) {
t.Log("When given an invalid branch regex in a config, an error is returned")
RegisterMockTestingT(t)
client := mocks.NewMockSlackClient()

invalidRegex := "("
configs := validConfigs()
configs[0].BranchRegex = invalidRegex
_, err := webhooks.NewMultiWebhookSender(configs, client)
Assert(t, err != nil, "expected error")
Assert(t, strings.Contains(err.Error(), "error parsing regexp"), "expected regex error")
}

func TestNewWebhooksManager_InvalidBranchAndWorkspaceRegex(t *testing.T) {
t.Log("When given an invalid branch and invalid workspace regex in a config, an error is returned")
RegisterMockTestingT(t)
client := mocks.NewMockSlackClient()

invalidRegex := "("
configs := validConfigs()
configs[0].WorkspaceRegex = invalidRegex
configs[0].BranchRegex = invalidRegex
_, err := webhooks.NewMultiWebhookSender(configs, client)
Assert(t, err != nil, "expected error")
Assert(t, strings.Contains(err.Error(), "error parsing regexp"), "expected regex error")
}

func TestNewWebhooksManager_NoEvent(t *testing.T) {
t.Log("When the event key is not specified in a config, an error is returned")
RegisterMockTestingT(t)
Expand Down
5 changes: 5 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ type WebhookConfig struct {
// that is being modified for this event. If the regex matches, we'll
// send the webhook, ex. "production.*".
WorkspaceRegex string `mapstructure:"workspace-regex"`
// BranchRegex is a regex that is used to match against the base branch
// that is being modified for this event. If the regex matches, we'll
// send the webhook, ex. "main.*".
BranchRegex string `mapstructure:"branch-regex"`
// Kind is the type of webhook we should send, ex. slack.
Kind string `mapstructure:"kind"`
// Channel is the channel to send this webhook to. It only applies to
Expand Down Expand Up @@ -344,6 +348,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
for _, c := range userConfig.Webhooks {
config := webhooks.Config{
Channel: c.Channel,
BranchRegex: c.BranchRegex,
Event: c.Event,
Kind: c.Kind,
WorkspaceRegex: c.WorkspaceRegex,
Expand Down

0 comments on commit b8bb6df

Please sign in to comment.