diff --git a/runatlantis.io/docs/using-slack-hooks.md b/runatlantis.io/docs/using-slack-hooks.md index c10706ecc1..c75c243fca 100644 --- a/runatlantis.io/docs/using-slack-hooks.md +++ b/runatlantis.io/docs/using-slack-hooks.md @@ -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 ``` @@ -56,6 +57,7 @@ config: | webhooks: - event: apply workspace-regex: .* + branch-regex: .* kind: slack channel: my-channel ``` diff --git a/server/events/webhooks/slack.go b/server/events/webhooks/slack.go index 54116de486..091297c74c 100644 --- a/server/events/webhooks/slack.go +++ b/server/events/webhooks/slack.go @@ -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) diff --git a/server/events/webhooks/slack_client.go b/server/events/webhooks/slack_client.go index 3a744b9241..a548f9ea31 100644 --- a/server/events/webhooks/slack_client.go +++ b/server/events/webhooks/slack_client.go @@ -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, diff --git a/server/events/webhooks/slack_client_test.go b/server/events/webhooks/slack_client_test.go index 83db303247..3edd5ba0ef 100644 --- a/server/events/webhooks/slack_client_test.go +++ b/server/events/webhooks/slack_client_test.go @@ -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", diff --git a/server/events/webhooks/slack_test.go b/server/events/webhooks/slack_test.go index dd80d93e4d..0a88a2f310 100644 --- a/server/events/webhooks/slack_test.go +++ b/server/events/webhooks/slack_test.go @@ -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" @@ -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") @@ -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) diff --git a/server/events/webhooks/webhooks.go b/server/events/webhooks/webhooks.go index 080400f9ab..c4b43239a7 100644 --- a/server/events/webhooks/webhooks.go +++ b/server/events/webhooks/webhooks.go @@ -52,6 +52,7 @@ type MultiWebhookSender struct { type Config struct { Event string WorkspaceRegex string + BranchRegex string Kind string Channel string } @@ -59,7 +60,11 @@ type Config struct { 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 } @@ -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 } diff --git a/server/events/webhooks/webhooks_test.go b/server/events/webhooks/webhooks_test.go index 03437f528c..5ee00bf599 100644 --- a/server/events/webhooks/webhooks_test.go +++ b/server/events/webhooks/webhooks_test.go @@ -34,6 +34,7 @@ const ( var validConfig = webhooks.Config{ Event: validEvent, WorkspaceRegex: validRegex, + BranchRegex: validRegex, Kind: validKind, Channel: validChannel, } @@ -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() @@ -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) diff --git a/server/server.go b/server/server.go index 91aca3be87..f3f952b71d 100644 --- a/server/server.go +++ b/server/server.go @@ -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 @@ -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,