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 17 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
79 changes: 72 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@ import (

// Input ...
type Input struct {
Debug bool `env:"is_debug_mode,opt[yes,no]"`
Debug bool `env:"is_debug_mode,opt[yes,no]"`
BuildAPIToken string `env:"bitrise_build_api_token,required"`
nyikos-zoltan marked this conversation as resolved.
Show resolved Hide resolved
BuildURL string `env:"bitrise_build_url,required"`
ofalvai marked this conversation as resolved.
Show resolved Hide resolved

// 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 +148,39 @@ 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 := http.NewRequest("GET", siURL, http.NoBody)
ofalvai marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return "", err
}
req.Header.Add("Build-Api-Token", token)
client := &http.Client{}

resp, err := client.Do(req)

if err != nil {
return "", err
}
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 {
return "", fmt.Errorf("server error: %s", resp.Status)
ofalvai marked this conversation as resolved.
Show resolved Hide resolved
}
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 +201,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 +238,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 +262,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 +275,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, 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 +313,7 @@ func parseInputIntoConfig(inp *Input) config {
ThreadTsOutputVariableName: inp.ThreadTsOutputVariableName,
Ts: selectValue(inp.Ts, inp.TsOnError),
}
return config
return config, nil

}

Expand All @@ -270,7 +331,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
46 changes: 40 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 @@ -62,30 +65,61 @@ inputs:
value_options:
- "yes"
- "no"
- bitrise_build_api_token: $BITRISE_BUILD_API_TOKEN
ofalvai marked this conversation as resolved.
Show resolved Hide resolved
opts:
title: Bitrise Build API Token
summary: API Token for the build on Bitrise.io.
is_required: true
is_sensitive: true
is_dont_change_value: true
- bitrise_build_url: $BITRISE_BUILD_URL
opts:
title: Bitrise Build URL
summary: Bitrise Build URL
is_required: true
is_sensitive: false
is_dont_change_value: true

# Message inputs
- webhook_url:
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