Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add slack integration usecase [ER-1669] #90

Draft
wants to merge 24 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 23 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
golang 1.22
11 changes: 9 additions & 2 deletions bitrise.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@ default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
app:
envs:
- BITRISE_STEP_GIT_CLONE_URL: https://github.com/bitrise-io/steps-slack-message.git
- GOLANGCI_INSTALL_URL: https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh
- GOLANGCI_LINT_VERSION: "v1.62.0"

workflows:
test:
before_run:
- audit-this-step
steps:
- [email protected]:
inputs:
- content: |-
#!/bin/bash
curl -sSfL ${GOLANGCI_INSTALL_URL} | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_LINT_VERSION}
asdf reshim golang
golangci-lint run ./...
- go-list:
- golint:
- errcheck:
- go-test:
- path::./:
title: On Success
Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ module github.com/bitrise-steplib/steps-slack-message
go 1.17

require (
github.com/bitrise-io/go-utils v0.0.0-20171214135957-b33f6bcef9b5
github.com/bitrise-io/go-utils v1.0.13
github.com/bitrise-tools/go-steputils v0.0.0-20180209154519-b0e1079aa921
)

require (
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/stretchr/testify v1.8.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
26 changes: 24 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,20 +1,42 @@
github.com/bitrise-io/go-utils v0.0.0-20171214135957-b33f6bcef9b5 h1:kQ5jR1WQy1I8caVbnRZshej4H03crTuqHCPsaYdNRlI=
github.com/bitrise-io/go-utils v0.0.0-20171214135957-b33f6bcef9b5/go.mod h1:Pp48eQd8BSNEzWWTBxdFUd/ufS5b5Bgkmt9D5NedjWg=
github.com/bitrise-io/go-utils v1.0.13 h1:1QENhTS/JlKH9F7+/nB+TtbTcor6jGrE6cQ4CJWfp5U=
github.com/bitrise-io/go-utils v1.0.13/go.mod h1:ZY1DI+fEpZuFpO9szgDeICM4QbqoWVt0RSY3tRI1heY=
github.com/bitrise-tools/go-steputils v0.0.0-20180209154519-b0e1079aa921 h1:vBwboEybHM/I+BNN+TM9z5oQRZOwzeIPqCt1xM2/+LI=
github.com/bitrise-tools/go-steputils v0.0.0-20180209154519-b0e1079aa921/go.mod h1:4eEx1Bd2AAVAN/YGK4V066jO7ekrsLymkyQZzx2UEwQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
86 changes: 79 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,23 @@ import (
"time"

"github.com/bitrise-io/go-utils/log"
"github.com/bitrise-io/go-utils/retry"
"github.com/bitrise-tools/go-steputils/stepconf"
"github.com/hashicorp/go-retryablehttp"
)

// Input ...
type Input struct {
Debug bool `env:"is_debug_mode,opt[yes,no]"`
Debug bool `env:"is_debug_mode,opt[yes,no]"`
BuildAPIToken stepconf.Secret `env:"BITRISE_BUILD_API_TOKEN,required"`
BuildURL string `env:"BITRISE_BUILD_URL,required"`

// Message
WebhookURL stepconf.Secret `env:"webhook_url"`
WebhookURLOnError stepconf.Secret `env:"webhook_url_on_error"`
APIToken stepconf.Secret `env:"api_token"`
IntegrationID string `env:"workspace_integration_id"`
IntegrationIDOnError string `env:"workspace_integration_id_on_error"`
Channel string `env:"channel"`
ChannelOnError string `env:"channel_on_error"`
Text string `env:"text"`
Expand Down Expand Up @@ -144,6 +150,44 @@ func newMessage(c config) Message {
return msg
}

func getWebhookURL(buildURL string, id string, token string) (string, error) {
var webookData struct {
WebhookURL string `json:"webhook_url"`
}
buildURL = strings.Replace(buildURL, "build", "builds", -1)
siURL := fmt.Sprintf("%s/slack_integrations/%s", buildURL, id)

req, err := retryablehttp.NewRequest("GET", siURL, http.NoBody)
if err != nil {
return "", err
}
req.Header.Add("Build-Api-Token", token)
client := retry.NewHTTPClient()

resp, err := client.Do(req)

if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
body, err := io.ReadAll(resp.Body)
ofalvai marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return "", err
}
if err = json.Unmarshal(body, &webookData); err != nil {
return "", err
}
} else {
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return "", fmt.Errorf("server error, status: %s\nresponse: %s", resp.Status, string(body))
}
return webookData.WebhookURL, nil
}

// postMessage sends a message to a channel.
func postMessage(conf config, msg Message) error {
b, err := json.Marshal(msg)
Expand All @@ -164,6 +208,9 @@ func postMessage(conf config, msg Message) error {
}

req, err := http.NewRequest("POST", url, bytes.NewReader(b))
if err != nil {
return err
}
req.Header.Add("Content-Type", "application/json; charset=utf-8")

if string(conf.APIToken) != "" {
Expand Down Expand Up @@ -198,8 +245,20 @@ func postMessage(conf config, msg Message) error {
}

func validate(inp *Input) error {
if inp.APIToken == "" && inp.WebhookURL == "" {
return fmt.Errorf("Both API Token and WebhookURL are empty. You need to provide one of them. If you want to use incoming webhooks provide the webhook url. If you want to use a bot to send a message provide the bot API token")
if inp.APIToken == "" && inp.WebhookURL == "" && inp.IntegrationID == "" {
return fmt.Errorf("All of Integration ID, API Token and WebhookURL are empty. You need to provide one of them. If you want to use incoming webhooks provide the webhook url. If you want to use a bot to send a message provide the bot API token. If you want to use a configured workspace integration use its ID.")
}

if inp.IntegrationID != "" {
if inp.APIToken != "" {
log.Warnf("Both API Token and Integration ID are provided. Ignoring API Token.")
inp.APIToken = ""
}
if inp.WebhookURL != "" {
log.Warnf("Both WebhookURL and Integration ID are provided. Ignoring WebhookURL.")
inp.WebhookURL = ""
}
return nil
}

if inp.APIToken != "" && inp.WebhookURL != "" {
Expand All @@ -210,7 +269,7 @@ func validate(inp *Input) error {
return nil
}

func parseInputIntoConfig(inp *Input) config {
func parseInputIntoConfig(inp *Input) (config, error) {
pipelineSuccess := inp.PipelineBuildStatus == "" ||
inp.PipelineBuildStatus == "succeeded" ||
inp.PipelineBuildStatus == "succeeded_with_abort"
Expand All @@ -223,11 +282,20 @@ func parseInputIntoConfig(inp *Input) config {
}
return ifFailed
}
var integrationID = selectValue(inp.IntegrationID, inp.IntegrationIDOnError)
var webhookURL = selectValue(string(inp.WebhookURL), string(inp.WebhookURLOnError))
if integrationID != "" {
var err error
webhookURL, err = getWebhookURL(inp.BuildURL, integrationID, string(inp.BuildAPIToken))
if err != nil {
return config{}, err
}
}

var config = config{
Debug: inp.Debug,
APIToken: inp.APIToken,
WebhookURL: selectValue(string(inp.WebhookURL), string(inp.WebhookURLOnError)),
WebhookURL: webhookURL,
Channel: selectValue(inp.Channel, inp.ChannelOnError),
Text: selectValue(inp.Text, inp.TextOnError),
IconEmoji: selectValue(inp.IconEmoji, inp.IconEmojiOnError),
Expand All @@ -252,7 +320,7 @@ func parseInputIntoConfig(inp *Input) config {
ThreadTsOutputVariableName: inp.ThreadTsOutputVariableName,
Ts: selectValue(inp.Ts, inp.TsOnError),
}
return config
return config, nil

}

Expand All @@ -270,7 +338,11 @@ func main() {
os.Exit(1)
}

config := parseInputIntoConfig(&input)
config, err := parseInputIntoConfig(&input)
if err != nil {
log.Errorf("Error: %s\n", err)
os.Exit(1)
}

msg := newMessage(config)
if err := postMessage(config, msg); err != nil {
Expand Down
32 changes: 26 additions & 6 deletions step.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,21 @@ description: |-

### Configuring the Step

To use this Step, you need either an incoming Slack webhook or a Slack bot user with an API token. You can set up both on your Slack account:
To use this Step, you need either a configured Slack Integration in your workspace, an incoming Slack webhook or a Slack bot user with an API token. For the former see your Workspace settings, for the latter two, you can set them up in Slack:

- [Incoming webhooks](https://api.slack.com/incoming-webhooks).
- [Bot user with an API token](https://api.slack.com/bot-users).

Once you're ready with those, come back to Bitrise and configure the Step itself.
Once you're ready with those, come back to Bitrise and configure the Step itself:

1. Create a [Secret Env Var](https://devcenter.bitrise.io/builds/env-vars-secret-env-vars/) for either your Slack webhook URL or your Slack API token.
1. Add the Secret to either the **Slack Webhook URL** or the **Slack API token** input.
1. Toggle the **Run if previous Step failed** option on - you should see a white checkmark on green background next to it. This allows Slack messages to be sent for failed builds, too.
1. In the **Target Slack channel, group or username**, set where the Slack message should be sent.
1. Customize your messages as you'd like. For the details, see the respective inputs.


In case of the Slack Integration usecase you can copy the ID in your Workspace settings, on the Integrations page. This ID is not senstive, you can use it as a step input as-is, or put it into a regular environment variable.

Note that this step always sends a message (either to `channel` or `channel_on_error`). If your use case is to send a message only on success or on failure, then you can [run the entire step conditionally](https://devcenter.bitrise.io/en/steps-and-workflows/introduction-to-steps/enabling-or-disabling-a-step-conditionally.html).

Expand Down Expand Up @@ -68,24 +71,41 @@ inputs:
opts:
title: "Slack Webhook URL (Webhook or API token is required)"
description: |
**Either webhook\_url or api\_token input is required.**
**One of workspace\_integration\_id, webhook\_url or api\_token input is required.**
To register an **Incoming WebHook integration** visit: https://api.slack.com/incoming-webhooks
is_required: false
is_sensitive: true
- webhook_url_on_error:
opts:
title: "Slack Webhook URL (Webhook or API token is required) if the build failed"
description: |
**Either webhook\_url or api\_token input is required.**
**One of workspace\_integration\_id, webhook\_url or api\_token input is required.**
To register an **Incoming WebHook integration** visit: https://api.slack.com/incoming-webhooks
is_required: false
is_sensitive: true
category: If Build Failed
- workspace_integration_id:
opts:
title: "Workspace Slack Integration ID (Integration ID, Webhook or API token is required)"
description: |
**One of workspace\_integration\_id, webhook\_url or api\_token input is required.**
To register a **Workspace Slack Integration** see the Integration page in your Workspace settings
is_required: false
is_sensitive: false
- workspace_integration_id_on_error:
opts:
title: "Workspace Slack Integration ID (Integration ID, Webhook or API token is required) if the build failed"
description: |
**One of workspace\_integration\_id, webhook\_url or api\_token input is required.**
To register a **Workspace Slack Integration** see the Integration page in your Workspace settings
ofalvai marked this conversation as resolved.
Show resolved Hide resolved
is_required: false
is_sensitive: false
category: If Build Failed
- api_token:
opts:
title: "Slack API token (Webhook or API token is required)"
title: "Slack API token (One of webhook URL, API token or workspace integration ID is required)"
description: |
**Either webhook\_url or api\_token input is required.**
**One of workspace\_integration\_id, webhook\_url or api\_token input is required.**

To setup a **bot with an API token** visit: https://api.slack.com/bot-users
is_required: false
Expand Down
57 changes: 57 additions & 0 deletions vendor/github.com/bitrise-io/go-utils/log/defaultlogger.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading