From a60201f39acffab26b804947cc982772c668cce3 Mon Sep 17 00:00:00 2001 From: Simon Prochazka Date: Wed, 20 Nov 2019 23:39:24 +0100 Subject: [PATCH] feat(slack): add basic integration with Slack If SLACK_WEBHOOK is set then Release Notary will send a simple Slack markdown compliant message to it. --- .drone.yml | 2 + cmd/publish.go | 13 ++++++ internal/slack/publish.go | 39 +++++++++++++++++ internal/slack/publish_test.go | 47 +++++++++++++++++++++ internal/slack/release_notes.go | 49 ++++++++++++++++++++++ internal/slack/release_notes_test.go | 19 +++++++++ internal/slack/slack.go | 6 +++ internal/slack/testdata/expected_format.md | 15 +++++++ 8 files changed, 190 insertions(+) create mode 100644 internal/slack/publish.go create mode 100644 internal/slack/publish_test.go create mode 100644 internal/slack/release_notes.go create mode 100644 internal/slack/release_notes_test.go create mode 100644 internal/slack/slack.go create mode 100644 internal/slack/testdata/expected_format.md diff --git a/.drone.yml b/.drone.yml index 35b4888..d7d27a2 100644 --- a/.drone.yml +++ b/.drone.yml @@ -57,6 +57,8 @@ steps: GITHUB_TOKEN: from_secret: github_token GITHUB_REPOSITORY: commitsar-app/release-notary + SLACK_WEBHOOK: + from_secret: slack_webhook commands: - go run main.go publish when: diff --git a/cmd/publish.go b/cmd/publish.go index 2c068f3..fbbbe63 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -7,6 +7,7 @@ import ( history "github.com/commitsar-app/git/pkg" "github.com/commitsar-app/release-notary/internal/releaser" + "github.com/commitsar-app/release-notary/internal/slack" "github.com/commitsar-app/release-notary/internal/text" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -118,6 +119,18 @@ var publishCmd = &cobra.Command{ return err } + if viper.IsSet("SLACK_WEBHOOK") { + slack := &slack.Slack{ + WebHookURL: viper.GetString("SLACK_WEBHOOK"), + } + + err = slack.Publish(sections) + + if err != nil { + return err + } + } + return nil }, } diff --git a/internal/slack/publish.go b/internal/slack/publish.go new file mode 100644 index 0000000..e1c5e4f --- /dev/null +++ b/internal/slack/publish.go @@ -0,0 +1,39 @@ +package slack + +import ( + "bytes" + "net/http" + "time" + + "github.com/commitsar-app/release-notary/internal/text" + jsoniter "github.com/json-iterator/go" +) + +var json = jsoniter.ConfigCompatibleWithStandardLibrary + +type request struct { + Text string `json:"text"` +} + +// Publish pushes the release notes to Slack via provided Webhook. https://api.slack.com/reference/messaging/payload +func (s *Slack) Publish(commits map[string][]text.Commit) error { + releaseNotes := GenerateReleaseNotes(commits) + + client := http.Client{ + Timeout: time.Second * 5, + } + + jsonBody, err := json.Marshal(request{Text: releaseNotes}) + + if err != nil { + return err + } + + _, err = client.Post(s.WebHookURL, "application/json", bytes.NewBuffer(jsonBody)) + + if err != nil { + return err + } + + return nil +} diff --git a/internal/slack/publish_test.go b/internal/slack/publish_test.go new file mode 100644 index 0000000..70c1adf --- /dev/null +++ b/internal/slack/publish_test.go @@ -0,0 +1,47 @@ +package slack + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/commitsar-app/release-notary/internal/text" + "github.com/stretchr/testify/assert" +) + +func TestPublish(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, "/webhook", req.URL.String()) + assert.Equal(t, "application/json", req.Header["Content-Type"][0]) + + body, err := ioutil.ReadAll(req.Body) + + assert.NoError(t, err) + + expectedBody := "{\"text\":\"*Features*\\r\\nci test\\r\\n\\r\\n*Bug fixes*\\r\\nhuge bug\\r\\nbug fix\\r\\n\\r\\n*Chores and Improvements*\\r\\ntesting\\r\\nthis should end up in chores\\r\\n\\r\\n*Other*\\r\\nmerge master in something\\r\\nrandom\\r\\n\\r\\n\"}" + + assert.Equal(t, expectedBody, string(body)) + + _, err = rw.Write([]byte(`ok`)) + + assert.NoError(t, err) + })) + + defer server.Close() + + slack := &Slack{ + WebHookURL: server.URL + "/webhook", + } + + testData := map[string][]text.Commit{ + "features": []text.Commit{text.Commit{Category: "feat", Scope: "ci", Heading: "ci test"}}, + "chores": []text.Commit{text.Commit{Category: "chore", Scope: "", Heading: "testing"}, text.Commit{Category: "improvement", Scope: "", Heading: "this should end up in chores"}}, + "bugs": []text.Commit{text.Commit{Category: "bug", Scope: "", Heading: "huge bug"}, text.Commit{Category: "fix", Scope: "", Heading: "bug fix"}}, + "others": []text.Commit{text.Commit{Category: "other", Scope: "", Heading: "merge master in something"}, text.Commit{Category: "bs", Scope: "", Heading: "random"}}, + } + + err := slack.Publish(testData) + + assert.NoError(t, err) +} diff --git a/internal/slack/release_notes.go b/internal/slack/release_notes.go new file mode 100644 index 0000000..005b8d4 --- /dev/null +++ b/internal/slack/release_notes.go @@ -0,0 +1,49 @@ +package slack + +import ( + "strings" + + "github.com/commitsar-app/release-notary/internal/text" +) + +// GenerateReleaseNotes creates a string from release notes that conforms with the Slack formatting. Expected format can be found in testdata. +func GenerateReleaseNotes(sections map[string][]text.Commit) string { + builder := strings.Builder{} + + if len(sections["features"]) > 0 { + builder.WriteString("*Features*\r\n") + builder.WriteString(buildSection(sections["features"])) + builder.WriteString("\r\n") + } + + if len(sections["bugs"]) > 0 { + builder.WriteString("*Bug fixes*\r\n") + builder.WriteString(buildSection(sections["bugs"])) + builder.WriteString("\r\n") + } + + if len(sections["chores"]) > 0 { + builder.WriteString("*Chores and Improvements*\r\n") + builder.WriteString(buildSection(sections["chores"])) + builder.WriteString("\r\n") + } + + if len(sections["others"]) > 0 { + builder.WriteString("*Other*\r\n") + builder.WriteString(buildSection(sections["others"])) + builder.WriteString("\r\n") + } + + return builder.String() +} + +func buildSection(commits []text.Commit) string { + builder := strings.Builder{} + + for _, commit := range commits { + builder.WriteString(commit.Heading) + builder.WriteString("\r\n") + } + + return builder.String() +} diff --git a/internal/slack/release_notes_test.go b/internal/slack/release_notes_test.go new file mode 100644 index 0000000..c19f0ef --- /dev/null +++ b/internal/slack/release_notes_test.go @@ -0,0 +1,19 @@ +package slack + +import ( + "testing" + + "github.com/commitsar-app/release-notary/internal/text" + "github.com/stretchr/testify/assert" +) + +func TestGenerateReleaseNotes(t *testing.T) { + testData := map[string][]text.Commit{ + "features": []text.Commit{text.Commit{Category: "feat", Scope: "ci", Heading: "ci test"}}, + "chores": []text.Commit{text.Commit{Category: "chore", Scope: "", Heading: "testing"}, text.Commit{Category: "improvement", Scope: "", Heading: "this should end up in chores"}}, + "bugs": []text.Commit{text.Commit{Category: "bug", Scope: "", Heading: "huge bug"}, text.Commit{Category: "fix", Scope: "", Heading: "bug fix"}}, + "others": []text.Commit{text.Commit{Category: "other", Scope: "", Heading: "merge master in something"}, text.Commit{Category: "bs", Scope: "", Heading: "random"}}, + } + + assert.Equal(t, "*Features*\r\nci test\r\n\r\n*Bug fixes*\r\nhuge bug\r\nbug fix\r\n\r\n*Chores and Improvements*\r\ntesting\r\nthis should end up in chores\r\n\r\n*Other*\r\nmerge master in something\r\nrandom\r\n\r\n", GenerateReleaseNotes(testData)) +} diff --git a/internal/slack/slack.go b/internal/slack/slack.go new file mode 100644 index 0000000..ac60264 --- /dev/null +++ b/internal/slack/slack.go @@ -0,0 +1,6 @@ +package slack + +// Slack is the struct holding all the methods to work with the Slack integration. +type Slack struct { + WebHookURL string +} diff --git a/internal/slack/testdata/expected_format.md b/internal/slack/testdata/expected_format.md new file mode 100644 index 0000000..dc6b239 --- /dev/null +++ b/internal/slack/testdata/expected_format.md @@ -0,0 +1,15 @@ +*Features* +ci test + +*Bug fixes* +huge bug +bug fix + +*Chores and Improvements* +testing +this should end up in chores + +*Other* +merge master in something +random +