Skip to content

Commit 2d90fb7

Browse files
committed
restore option to send slack messages via webhooks
Treat webhooks as a fallback option in case access token isn't defined
1 parent 0380eb3 commit 2d90fb7

File tree

5 files changed

+67
-16
lines changed

5 files changed

+67
-16
lines changed

documentation/secret_docs.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ A secrets file stores sensitive information. Unlike the repository configuration
1717
|-|-|-|-|
1818
| `gh_token` | specify to grant the bot access to private repositories; omit for public repositories | Yes | - |
1919
| `gh_hook_token` | specify to ensure the bot only receives GitHub notifications from pre-approved repositories | Yes | - |
20-
| `slack_access_token` | slack bot token obtained via [oauth](https://api.slack.com/authentication/oauth-v2), enabling message posting to the workspace | Yes | - |
20+
| `slack_access_token` | slack bot access token to enable message posting to the workspace | Yes | try to use webhooks defined in `slack_hooks` instead |
21+
| `slack_hooks` | list of channel names and their corresponding webhook endpoint | Yes | try to use token defined in `slack_access_token` instead |
22+
23+
Note that either `slack_access_token` or `slack_hooks` must be defined.
2124

2225
## `gh_token`
2326

@@ -26,3 +29,22 @@ Some operations, such as fetching a config file from a private repository, or th
2629
## `gh_hook_token`
2730

2831
Refer [here](https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/securing-your-webhooks) for more information on securing webhooks with a token.
32+
33+
## `slack_access_token`
34+
35+
Refer [here](https://api.slack.com/authentication/oauth-v2) for obtaining an access token via OAuth.
36+
37+
## `slack_hooks`
38+
39+
*Note: If `slack_access_token` is also defined, the bot will authenticate over Slack's Web API and this option will not be used.*
40+
41+
Expected format:
42+
43+
```json
44+
{
45+
"channel": "channel name",
46+
"url": "webhook url"
47+
}
48+
```
49+
50+
Refer [here](https://api.slack.com/messaging/webhooks) for obtaining a webhook for a channel.

lib/api_remote.ml

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,24 +50,34 @@ end
5050
module Slack : Api.Slack = struct
5151
let log = Log.from "slack"
5252

53-
let send_notification ~(ctx : Context.t) ~msg =
53+
let bearer_token_header access_token = sprintf "Authorization: Bearer %s" access_token
54+
55+
(** `send_notification ctx msg` notifies `msg.channel` with the payload `msg`;
56+
uses web API with access token if available, or with webhook otherwise *)
57+
let send_notification ~(ctx : Context.t) ~(msg : Slack_t.post_message_req) =
58+
let build_error e = fmt_error "%s\nfailed to send Slack notification" e in
59+
let build_query_error url e = build_error @@ sprintf "error while querying %s: %s" url e in
5460
let secrets = Context.get_secrets_exn ctx in
55-
match secrets.slack_access_token with
56-
| None -> Lwt.return @@ fmt_error "failed to retrieve Slack access token"
57-
| Some access_token ->
58-
let url = "https://slack.com/api/chat.postMessage" in
61+
let headers, url, webhook_mode =
62+
match secrets.slack_access_token with
63+
| Some access_token -> [ bearer_token_header access_token ], Some "https://slack.com/api/chat.postMessage", false
64+
| None -> [], Context.hook_of_channel ctx msg.channel, true
65+
in
66+
match url with
67+
| None -> Lwt.return @@ build_error @@ sprintf "no token or webhook configured to notify channel %s" msg.channel
68+
| Some url ->
5969
let data = Slack_j.string_of_post_message_req msg in
60-
let headers = [ sprintf "Authorization: Bearer %s" access_token ] in
6170
let body = `Raw ("application/json", data) in
6271
log#info "sending to %s : %s" msg.channel data;
6372
( match%lwt http_request ~body ~headers `POST url with
73+
(* error detection in response: slack uses status codes for webhooks versus a 200 code w/ `error` field for web api *)
6474
| Ok s ->
65-
let res = Slack_j.post_message_res_of_string s in
66-
if res.ok then Lwt.return @@ Ok ()
75+
if webhook_mode then Lwt.return @@ Ok ()
6776
else (
68-
let msg = Option.value ~default:"an unknown error occurred" res.error in
69-
Lwt.return @@ fmt_error "%s\nfailed to send Slack notification" msg
77+
let res = Slack_j.post_message_res_of_string s in
78+
if res.ok then Lwt.return @@ Ok ()
79+
else Lwt.return @@ build_query_error url (Option.value ~default:"an unknown error occurred" res.error)
7080
)
71-
| Error e -> Lwt.return @@ fmt_error "error while querying %s: %s\nfailed to send Slack notification" url e
81+
| Error e -> Lwt.return @@ build_query_error url e
7282
)
7383
end

lib/config.atd

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,21 @@ type config = {
3030
?main_branch_name : string nullable; (* the name of the main branch; used to filter out notifications about merges of main branch into other branches *)
3131
}
3232

33+
(* This specifies the Slack webhook to query to post to the channel with the given name *)
34+
type webhook = {
35+
url : string; (* webhook URL to post the Slack message *)
36+
channel : string; (* name of the Slack channel to post the message *)
37+
}
38+
3339
(* This is the structure of the secrets file which stores sensitive information, and
3440
shouldn't be checked into version control. *)
3541
type secrets = {
3642
(* GitHub personal access token, if repo access requires it *)
3743
?gh_token : string nullable;
3844
(* GitHub webhook token to secure the webhook *)
3945
?gh_hook_token : string nullable;
46+
(* list of Slack webhook & channel name pairs *)
47+
~slack_hooks <ocaml default="[]"> : webhook list;
4048
(* Slack bot token obtained via OAuth, enabling message posting to the workspace *)
4149
?slack_access_token : string nullable;
4250
}

lib/context.ml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ let get_config_exn ctx =
4040
| None -> context_error "config is uninitialized"
4141
| Some config -> config
4242

43+
let hook_of_channel ctx channel_name =
44+
let secrets = get_secrets_exn ctx in
45+
match List.find secrets.slack_hooks ~f:(fun webhook -> String.equal webhook.channel channel_name) with
46+
| Some hook -> Some hook.url
47+
| None -> None
48+
4349
(** `is_pipeline_allowed ctx p` returns `true` if ctx.config.status_rules
4450
doesn't define a whitelist of allowed pipelines, or if the list
4551
contains pipeline `p`; returns `false` otherwise. *)
@@ -61,8 +67,14 @@ let refresh_secrets ctx =
6167
match get_local_file path with
6268
| Error e -> fmt_error "error while getting local file: %s\nfailed to get secrets from file %s" e path
6369
| Ok file ->
64-
ctx.secrets <- Some (Config_j.secrets_of_string file);
65-
Ok ctx
70+
let secrets = Config_j.secrets_of_string file in
71+
begin
72+
match secrets.slack_access_token, secrets.slack_hooks with
73+
| None, [] -> fmt_error "either slack_access_token or slack_hooks must be defined in file '%s'" path
74+
| _ ->
75+
ctx.secrets <- Some secrets;
76+
Ok ctx
77+
end
6678

6779
let refresh_state ctx =
6880
match ctx.state_filepath with

test/secrets.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
{
2-
"slack_client_id": "",
3-
"slack_client_secret": ""
2+
"slack_access_token": ""
43
}

0 commit comments

Comments
 (0)