From f10278d2d3b42e045538842fc00cf2cf4ae95a44 Mon Sep 17 00:00:00 2001 From: Yasunari Watanabe Date: Mon, 4 Jan 2021 09:55:24 +0800 Subject: [PATCH 1/6] rename slack req/res atd types so they're not webhook-specific Slack Webhooks and Web API should be able to share the same payload structure. --- lib/api.ml | 2 +- lib/api_local.ml | 6 ++---- lib/api_remote.ml | 2 +- lib/slack.atd | 30 +++++++++++++++--------------- src/monorobot.ml | 2 +- 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/lib/api.ml b/lib/api.ml index 73d6ce86..468e20fe 100644 --- a/lib/api.ml +++ b/lib/api.ml @@ -9,5 +9,5 @@ module type Github = sig end module type Slack = sig - val send_notification : chan:string -> msg:webhook_notification -> url:string -> (unit, string) Result.t Lwt.t + val send_notification : chan:string -> msg:post_message_req -> url:string -> (unit, string) Result.t Lwt.t end diff --git a/lib/api_local.ml b/lib/api_local.ml index 4d78c883..5317a4b9 100644 --- a/lib/api_local.ml +++ b/lib/api_local.ml @@ -22,9 +22,7 @@ end module Slack : Api.Slack = struct let send_notification ~chan ~msg ~url:_ = - let json = - msg |> Slack_j.string_of_webhook_notification |> Yojson.Basic.from_string |> Yojson.Basic.pretty_to_string - in + let json = msg |> Slack_j.string_of_post_message_req |> Yojson.Basic.from_string |> Yojson.Basic.pretty_to_string in Stdio.printf "will notify #%s\n" chan; Stdio.printf "%s\n" json; Lwt.return @@ Ok () @@ -46,7 +44,7 @@ module Slack_json : Api.Slack = struct let log = Log.from "slack" let send_notification ~chan ~msg ~url:_ = - let json = Slack_j.string_of_webhook_notification msg in + let json = Slack_j.string_of_post_message_req msg in log#info "will notify %s" chan; let url = Uri.of_string "https://api.slack.com/docs/messages/builder" in let url = Uri.add_query_param url ("msg", [ json ]) in diff --git a/lib/api_remote.ml b/lib/api_remote.ml index 5e7b4b94..be01651c 100644 --- a/lib/api_remote.ml +++ b/lib/api_remote.ml @@ -51,7 +51,7 @@ module Slack : Api.Slack = struct let log = Log.from "slack" let send_notification ~chan ~msg ~url = - let data = Slack_j.string_of_webhook_notification msg in + let data = Slack_j.string_of_post_message_req msg in let body = `Raw ("application/json", data) in log#info "sending to %s : %s" chan data; match%lwt http_request ~body `POST url with diff --git a/lib/slack.atd b/lib/slack.atd index 8528ac95..19ffb529 100644 --- a/lib/slack.atd +++ b/lib/slack.atd @@ -1,9 +1,9 @@ -type webhook_notification_field = { +type message_field = { ?title: string nullable; value: string; } -type webhook_notification_attachment = { +type message_attachment = { fallback: string nullable; ?mrkdwn_in: string list nullable; ?color: string nullable; @@ -14,18 +14,18 @@ type webhook_notification_attachment = { ?title: string nullable; ?title_link: string nullable; ?text: string nullable; - ?fields: webhook_notification_field list nullable; + ?fields: message_field list nullable; ?image_url: string nullable; ?thumb_url: string nullable; ?ts: int nullable; ?footer: string nullable; } -type notification_section_block_type = [ +type message_section_block_type = [ Section ] -type notification_divider_block_type = [ +type message_divider_block_type = [ Divider ] @@ -39,22 +39,22 @@ type text_object = { text: string; } -type webhook_notification_text_block = { - notification_type : notification_section_block_type; +type message_text_block = { + message_type : message_section_block_type; text: text_object; } -type webhook_notification_divider_block = { - notification_type : notification_divider_block_type; +type message_divider_block = { + message_type : message_divider_block_type; } -type webhook_notification_block = [ - Text of webhook_notification_text_block - | Divider of webhook_notification_divider_block +type message_block = [ + Text of message_text_block + | Divider of message_divider_block ] -type webhook_notification = { +type post_message_req = { ?text: string nullable; - ?attachments: webhook_notification_attachment list nullable; - ?blocks: webhook_notification_block list nullable; + ?attachments: message_attachment list nullable; + ?blocks: message_block list nullable; } diff --git a/src/monorobot.ml b/src/monorobot.ml index a78a991d..9cd10a1c 100644 --- a/src/monorobot.ml +++ b/src/monorobot.ml @@ -46,7 +46,7 @@ let check_gh_action file json config secrets state = let check_slack_action url file = let data = Stdio.In_channel.read_all file in let chan = Printf.sprintf "webhook %s" url in - match Slack_j.webhook_notification_of_string data with + match Slack_j.post_message_req_of_string data with | exception exn -> log#error ~exn "unable to parse notification" | msg -> Lwt_main.run From f1b0b934ef402c291d61f1c6f1b8f94c1a7dbcc1 Mon Sep 17 00:00:00 2001 From: Yasunari Watanabe Date: Mon, 4 Jan 2021 10:14:35 +0800 Subject: [PATCH 2/6] send slack notifications with web api if token is configured Adds a way to send notifications without per-channel webhooks. The sending mechanism will authenticate with a bearer token that is valid for any channel that the bot has been added to. The channel is specified in a new `channel` field in the notification payload. A note on error handling: for web API requests, Slack always returns a 200 response and communicates the error message in the response body, so checking for status alone isn't enough. --- lib/api.ml | 2 +- lib/api_local.ml | 12 ++++++------ lib/api_remote.ml | 39 +++++++++++++++++++++++++++++++-------- lib/config.atd | 11 ++++++++--- lib/slack.atd | 7 +++++++ 5 files changed, 53 insertions(+), 18 deletions(-) diff --git a/lib/api.ml b/lib/api.ml index 468e20fe..e195af7c 100644 --- a/lib/api.ml +++ b/lib/api.ml @@ -9,5 +9,5 @@ module type Github = sig end module type Slack = sig - val send_notification : chan:string -> msg:post_message_req -> url:string -> (unit, string) Result.t Lwt.t + val send_notification : ctx:Context.t -> msg:post_message_req -> (unit, string) Result.t Lwt.t end diff --git a/lib/api_local.ml b/lib/api_local.ml index 5317a4b9..c2eeab17 100644 --- a/lib/api_local.ml +++ b/lib/api_local.ml @@ -21,9 +21,9 @@ module Github : Api.Github = struct end module Slack : Api.Slack = struct - let send_notification ~chan ~msg ~url:_ = + let send_notification ~ctx:_ ~msg = let json = msg |> Slack_j.string_of_post_message_req |> Yojson.Basic.from_string |> Yojson.Basic.pretty_to_string in - Stdio.printf "will notify #%s\n" chan; + Stdio.printf "will notify #%s\n" msg.channel; Stdio.printf "%s\n" json; Lwt.return @@ Ok () end @@ -31,8 +31,8 @@ end module Slack_simple : Api.Slack = struct let log = Log.from "slack" - let send_notification ~chan ~msg ~url:_ = - log#info "will notify %s%s" chan + let send_notification ~ctx:_ ~(msg : Slack_t.post_message_req) = + log#info "will notify %s%s" msg.channel ( match msg.Slack_t.text with | None -> "" | Some s -> Printf.sprintf " with %S" s @@ -43,9 +43,9 @@ end module Slack_json : Api.Slack = struct let log = Log.from "slack" - let send_notification ~chan ~msg ~url:_ = + let send_notification ~ctx:_ ~(msg : Slack_t.post_message_req) = + log#info "will notify %s" msg.channel; let json = Slack_j.string_of_post_message_req msg in - log#info "will notify %s" chan; let url = Uri.of_string "https://api.slack.com/docs/messages/builder" in let url = Uri.add_query_param url ("msg", [ json ]) in log#info "%s" (Uri.to_string url); diff --git a/lib/api_remote.ml b/lib/api_remote.ml index be01651c..8976f44c 100644 --- a/lib/api_remote.ml +++ b/lib/api_remote.ml @@ -50,12 +50,35 @@ end module Slack : Api.Slack = struct let log = Log.from "slack" - let send_notification ~chan ~msg ~url = - let data = Slack_j.string_of_post_message_req msg in - let body = `Raw ("application/json", data) in - log#info "sending to %s : %s" chan data; - match%lwt http_request ~body `POST url with - | Ok _ -> Lwt.return @@ Ok () - | Error e -> - Lwt.return @@ fmt_error "error while querying remote: %s\nfailed to send Slack notification to %s" e url + let bearer_token_header access_token = sprintf "Authorization: Bearer %s" (Uri.pct_decode access_token) + + (** `send_notification ctx msg` notifies `msg.channel` with the payload `msg`; + uses web API with access token if available, or with webhook otherwise *) + let send_notification ~(ctx : Context.t) ~(msg : Slack_t.post_message_req) = + log#info "sending to %s" msg.channel; + let build_error e = fmt_error "%s\nfailed to send Slack notification" e in + let build_query_error url e = build_error @@ sprintf "error while querying %s: %s" url e in + let secrets = Context.get_secrets_exn ctx in + let headers, url, webhook_mode = + match secrets.slack_access_token with + | Some access_token -> [ bearer_token_header access_token ], Some "https://slack.com/api/chat.postMessage", false + | None -> [], Context.hook_of_channel ctx msg.channel, true + in + match url with + | None -> Lwt.return @@ build_error @@ sprintf "no token or webhook configured to notify channel %s" msg.channel + | Some url -> + let data = Slack_j.string_of_post_message_req msg in + let body = `Raw ("application/json", data) in + log#info "data: %s" data; + ( match%lwt http_request ~body ~headers `POST url with + (* error detection in response: slack uses status codes for webhooks versus a 200 code w/ `error` field for web api *) + | Ok s -> + if webhook_mode then Lwt.return @@ Ok () + else ( + let res = Slack_j.post_message_res_of_string s in + if res.ok then Lwt.return @@ Ok () + else Lwt.return @@ build_query_error url (Option.value ~default:"an unknown error occurred" res.error) + ) + | Error e -> Lwt.return @@ build_query_error url e + ) end diff --git a/lib/config.atd b/lib/config.atd index 08529549..ddabf950 100644 --- a/lib/config.atd +++ b/lib/config.atd @@ -39,7 +39,12 @@ type webhook = { (* This is the structure of the secrets file which stores sensitive information, and shouldn't be checked into version control. *) type secrets = { - slack_hooks : webhook list; - ?gh_token : string option; (* GitHub personal access token, if repo access requires it *) - ?gh_hook_token : string option; (* GitHub webhook token to secure the webhook *) + (* GitHub personal access token, if repo access requires it *) + ?gh_token : string nullable; + (* GitHub webhook token to secure the webhook *) + ?gh_hook_token : string nullable; + (* list of Slack webhook & channel name pairs *) + ~slack_hooks : webhook list; + (* Slack bot token obtained via OAuth, enabling message posting to the workspace *) + ?slack_access_token : string nullable; } diff --git a/lib/slack.atd b/lib/slack.atd index 19ffb529..bcf3ebcf 100644 --- a/lib/slack.atd +++ b/lib/slack.atd @@ -54,7 +54,14 @@ type message_block = [ ] type post_message_req = { + channel: string; ?text: string nullable; ?attachments: message_attachment list nullable; ?blocks: message_block list nullable; } + +type post_message_res = { + ok: bool; + ?channel: string nullable; + ?error: string nullable; +} From de0a28744b414fa7d56492532b94ed7169b1f8d5 Mon Sep 17 00:00:00 2001 From: Yasunari Watanabe Date: Mon, 4 Jan 2021 10:18:24 +0800 Subject: [PATCH 3/6] use the new notification sending interface throughout the codebase --- lib/action.ml | 40 ++++++++++++++-------------------------- lib/slack.ml | 31 ++++++++++++++++--------------- src/monorobot.ml | 19 +++++++++---------- 3 files changed, 39 insertions(+), 51 deletions(-) diff --git a/lib/action.ml b/lib/action.ml index e01991aa..469bafb0 100644 --- a/lib/action.ml +++ b/lib/action.ml @@ -152,41 +152,29 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct let cfg = Context.get_config_exn ctx in match req with | Github.Push n -> - partition_push cfg n |> List.map ~f:(fun (webhook, n) -> webhook, generate_push_notification n) |> Lwt.return - | Pull_request n -> - partition_pr cfg n |> List.map ~f:(fun webhook -> webhook, generate_pull_request_notification n) |> Lwt.return - | PR_review n -> - partition_pr_review cfg n |> List.map ~f:(fun webhook -> webhook, generate_pr_review_notification n) |> Lwt.return + partition_push cfg n |> List.map ~f:(fun (channel, n) -> generate_push_notification n channel) |> Lwt.return + | Pull_request n -> partition_pr cfg n |> List.map ~f:(generate_pull_request_notification n) |> Lwt.return + | PR_review n -> partition_pr_review cfg n |> List.map ~f:(generate_pr_review_notification n) |> Lwt.return | PR_review_comment n -> - partition_pr_review_comment cfg n - |> List.map ~f:(fun webhook -> webhook, generate_pr_review_comment_notification n) - |> Lwt.return - | Issue n -> - partition_issue cfg n |> List.map ~f:(fun webhook -> webhook, generate_issue_notification n) |> Lwt.return + partition_pr_review_comment cfg n |> List.map ~f:(generate_pr_review_comment_notification n) |> Lwt.return + | Issue n -> partition_issue cfg n |> List.map ~f:(generate_issue_notification n) |> Lwt.return | Issue_comment n -> - partition_issue_comment cfg n - |> List.map ~f:(fun webhook -> webhook, generate_issue_comment_notification n) - |> Lwt.return + partition_issue_comment cfg n |> List.map ~f:(generate_issue_comment_notification n) |> Lwt.return | Commit_comment n -> - let%lwt webhooks, api_commit = partition_commit_comment ctx n in - let%lwt notif = generate_commit_comment_notification api_commit n in - let notifs = List.map ~f:(fun webhook -> webhook, notif) webhooks in + let%lwt channels, api_commit = partition_commit_comment ctx n in + let notifs = List.map ~f:(generate_commit_comment_notification api_commit n) channels in Lwt.return notifs | Status n -> - let%lwt webhooks = partition_status ctx n in - let notifs = List.map ~f:(fun webhook -> webhook, generate_status_notification cfg n) webhooks in + let%lwt channels = partition_status ctx n in + let notifs = List.map ~f:(generate_status_notification cfg n) channels in Lwt.return notifs | _ -> Lwt.return [] let send_notifications (ctx : Context.t) notifications = - let notify (chan, msg) = - match Context.hook_of_channel ctx chan with - | None -> Printf.ksprintf action_error "webhook not defined for Slack channel '%s'" chan - | Some url -> - ( match%lwt Slack_api.send_notification ~chan ~msg ~url with - | Ok () -> Lwt.return_unit - | Error e -> action_error e - ) + let notify (msg : Slack_t.post_message_req) = + match%lwt Slack_api.send_notification ~ctx ~msg with + | Ok () -> Lwt.return_unit + | Error e -> action_error e in Lwt_list.iter_s notify notifications diff --git a/lib/slack.ml b/lib/slack.ml index bd825276..9b15a5c5 100644 --- a/lib/slack.ml +++ b/lib/slack.ml @@ -5,10 +5,6 @@ open Common open Github_j open Slack_j -let log = Log.from "slack" - -type channel_hook = string - let empty_attachments = { mrkdwn_in = None; @@ -37,7 +33,7 @@ let show_labels = function | (labels : label list) -> Some (sprintf "Labels: %s" @@ String.concat ~sep:", " (List.map ~f:(fun x -> x.name) labels)) -let generate_pull_request_notification notification = +let generate_pull_request_notification notification channel = let { action; number; sender; pull_request; repository } = notification in let ({ body; title; html_url; labels; _ } : pull_request) = pull_request in let action, body = @@ -57,6 +53,7 @@ let generate_pull_request_notification notification = action sender.login) in { + channel; text = None; attachments = Some @@ -73,7 +70,7 @@ let generate_pull_request_notification notification = blocks = None; } -let generate_pr_review_notification notification = +let generate_pr_review_notification notification channel = let { action; sender; pull_request; review; repository } = notification in let ({ number; title; html_url; _ } : pull_request) = pull_request in let action_str = @@ -96,6 +93,7 @@ let generate_pr_review_notification notification = action_str number html_url title) in { + channel; text = None; attachments = Some @@ -112,7 +110,7 @@ let generate_pr_review_notification notification = blocks = None; } -let generate_pr_review_comment_notification notification = +let generate_pr_review_comment_notification notification channel = let { action; pull_request; sender; comment; repository } = notification in let ({ number; title; html_url; _ } : pull_request) = pull_request in let action_str = @@ -134,6 +132,7 @@ let generate_pr_review_comment_notification notification = | Some a -> Some (sprintf "New comment by %s in <%s|%s>" sender.login comment.html_url a) in { + channel; text = None; attachments = Some @@ -151,7 +150,7 @@ let generate_pr_review_comment_notification notification = blocks = None; } -let generate_issue_notification notification = +let generate_issue_notification notification channel = let ({ action; sender; issue; repository } : issue_notification) = notification in let { number; body; title; html_url; labels; _ } = issue in let action, body = @@ -171,6 +170,7 @@ let generate_issue_notification notification = sender.login) in { + channel; text = None; attachments = Some @@ -187,7 +187,7 @@ let generate_issue_notification notification = blocks = None; } -let generate_issue_comment_notification notification = +let generate_issue_comment_notification notification channel = let { action; issue; sender; comment; repository } = notification in let { number; title; _ } = issue in let action_str = @@ -205,6 +205,7 @@ let generate_issue_comment_notification notification = action_str number issue.html_url title) in { + channel; text = None; attachments = Some @@ -223,7 +224,7 @@ let generate_issue_comment_notification notification = let git_short_sha_hash hash = String.sub ~pos:0 ~len:8 hash -let generate_push_notification notification = +let generate_push_notification notification channel = let { sender; created; deleted; forced; compare; commits; repository; _ } = notification in let commits_branch = Github.commits_branch_of_ref notification.ref in let tree_url = String.concat ~sep:"/" [ repository.url; "tree"; Uri.pct_encode commits_branch ] in @@ -247,6 +248,7 @@ let generate_push_notification notification = sprintf "`<%s|%s>` %s - %s" url (git_short_sha_hash id) title author.name) in { + channel; text = Some title; attachments = Some @@ -262,7 +264,7 @@ let generate_push_notification notification = blocks = None; } -let generate_status_notification (cfg : Config_t.config) (notification : status_notification) = +let generate_status_notification (cfg : Config_t.config) (notification : status_notification) channel = let { commit; state; description; target_url; context; repository; _ } = notification in let ({ commit : inner_commit; sha; author; html_url; _ } : status_commit) = commit in let ({ message; _ } : inner_commit) = commit in @@ -324,9 +326,9 @@ let generate_status_notification (cfg : Config_t.config) (notification : status_ fields = Some [ { title = None; value = String.concat ~sep:"\n" @@ List.concat [ commit_info; branches_info ] } ]; } in - { text = None; attachments = Some [ attachment ]; blocks = None } + { channel; text = None; attachments = Some [ attachment ]; blocks = None } -let generate_commit_comment_notification api_commit notification = +let generate_commit_comment_notification api_commit notification channel = let { commit; _ } = api_commit in let { sender; comment; repository; _ } = notification in let commit_id = @@ -355,5 +357,4 @@ let generate_commit_comment_notification api_commit notification = text = Some (mrkdwn_of_markdown comment.body); } in - let notifs = { text = None; attachments = Some [ attachment ]; blocks = None } in - Lwt.return notifs + { channel; text = None; attachments = Some [ attachment ]; blocks = None } diff --git a/src/monorobot.ml b/src/monorobot.ml index 9cd10a1c..a6e72008 100644 --- a/src/monorobot.ml +++ b/src/monorobot.ml @@ -43,14 +43,17 @@ let check_gh_action file json config secrets state = ) ) -let check_slack_action url file = +let check_slack_action file secrets = let data = Stdio.In_channel.read_all file in - let chan = Printf.sprintf "webhook %s" url in + let ctx = Context.make ~secrets_filepath:secrets () in match Slack_j.post_message_req_of_string data with | exception exn -> log#error ~exn "unable to parse notification" | msg -> + match Context.refresh_secrets ctx with + | Error e -> log#error "%s" e + | Ok ctx -> Lwt_main.run - ( match%lwt Api_remote.Slack.send_notification ~chan ~msg ~url with + ( match%lwt Api_remote.Slack.send_notification ~ctx ~msg with | Error e -> log#error "%s" e; Lwt.return_unit @@ -86,13 +89,9 @@ let gh_payload = let doc = "path to a JSON file containing a github webhook payload" in Arg.(required & pos 0 (some file) None & info [] ~docv:"GH_PAYLOAD" ~doc) -let slack_webhook_url = - let doc = "slack webhook url" in - Arg.(required & pos 0 (some string) None & info [] ~docv:"SLACK_WEBHOOK" ~doc) - let slack_payload = let doc = "path to a JSON file containing a slack notification payload" in - Arg.(required & pos 1 (some file) None & info [] ~docv:"SLACK_PAYLOAD" ~doc) + Arg.(required & pos 0 (some file) None & info [] ~docv:"SLACK_PAYLOAD" ~doc) let json = let doc = "if set, will format output as json" in @@ -113,9 +112,9 @@ let check_gh = term, info let check_slack = - let doc = "read a Slack notification from a file and send it to a webhook; used for testing" in + let doc = "read a Slack notification from a file and send it to a channel; used for testing" in let info = Term.info "check_slack" ~doc in - let term = Term.(const check_slack_action $ slack_webhook_url $ slack_payload) in + let term = Term.(const check_slack_action $ slack_payload $ secrets) in term, info let default_cmd = From b92a2c3f816f362e20f4bc8a4660bf43155b7d09 Mon Sep 17 00:00:00 2001 From: Yasunari Watanabe Date: Mon, 4 Jan 2021 10:23:50 +0800 Subject: [PATCH 4/6] enforce presence of either webhooks or access token in secrets file --- lib/context.ml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/context.ml b/lib/context.ml index 30567e56..0c44f68c 100644 --- a/lib/context.ml +++ b/lib/context.ml @@ -67,8 +67,14 @@ let refresh_secrets ctx = match get_local_file path with | Error e -> fmt_error "error while getting local file: %s\nfailed to get secrets from file %s" e path | Ok file -> - ctx.secrets <- Some (Config_j.secrets_of_string file); - Ok ctx + let secrets = Config_j.secrets_of_string file in + begin + match secrets.slack_access_token, secrets.slack_hooks with + | None, [] -> fmt_error "either slack_access_token or slack_hooks must be defined in file '%s'" path + | _ -> + ctx.secrets <- Some secrets; + Ok ctx + end let refresh_state ctx = match ctx.state_filepath with From 4f3445711ff8c74b0bf0a7d56093e85f27a1ba77 Mon Sep 17 00:00:00 2001 From: Yasunari Watanabe Date: Mon, 4 Jan 2021 10:24:38 +0800 Subject: [PATCH 5/6] tests: promote w/ new `channel` field in notification payload --- test/secrets.json | 35 +---------------------------------- test/slack_payloads.expected | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/test/secrets.json b/test/secrets.json index c0588bf9..f5a7ea7e 100644 --- a/test/secrets.json +++ b/test/secrets.json @@ -1,36 +1,3 @@ { - "slack_hooks": [ - { - "url": "https://slack_webhook_url", - "channel": "default" - }, - { - "url": "https://slack_webhook_url", - "channel": "aa" - }, - { - "url": "https://slack_webhook_url", - "channel": "longest-aa" - }, - { - "url": "https://slack_webhook_url", - "channel": "backend" - }, - { - "url": "https://slack_webhook_url", - "channel": "all-push-events" - }, - { - "url": "https://slack_webhook_url", - "channel": "frontend-bot" - }, - { - "url": "https://slack_webhook_url", - "channel": "aa-git" - }, - { - "url": "https://slack_webhook_url", - "channel": "siren" - } - ] + "slack_access_token": "" } \ No newline at end of file diff --git a/test/slack_payloads.expected b/test/slack_payloads.expected index b641af69..fbcdd809 100644 --- a/test/slack_payloads.expected +++ b/test/slack_payloads.expected @@ -1,6 +1,7 @@ ===== file ../mock_payloads/commit_comment.different_author.json ===== will notify #all-push-events { + "channel": "all-push-events", "attachments": [ { "fallback": @@ -18,6 +19,7 @@ will notify #all-push-events ===== file ../mock_payloads/commit_comment.general_comment.json ===== will notify #all-push-events { + "channel": "all-push-events", "attachments": [ { "fallback": @@ -33,6 +35,7 @@ will notify #all-push-events ===== file ../mock_payloads/commit_comment.general_comment_multiple_path.json ===== will notify #aa { + "channel": "aa", "attachments": [ { "fallback": @@ -47,6 +50,7 @@ will notify #aa } will notify #all-push-events { + "channel": "all-push-events", "attachments": [ { "fallback": @@ -62,6 +66,7 @@ will notify #all-push-events ===== file ../mock_payloads/commit_comment.long_comment.json ===== will notify #all-push-events { + "channel": "all-push-events", "attachments": [ { "fallback": @@ -80,6 +85,7 @@ will notify #all-push-events ===== file ../mock_payloads/commit_comment.mrkdwn_comment.json ===== will notify #all-push-events { + "channel": "all-push-events", "attachments": [ { "fallback": @@ -98,6 +104,7 @@ will notify #all-push-events ===== file ../mock_payloads/commit_comment.multiple_paths.json ===== will notify #all-push-events { + "channel": "all-push-events", "attachments": [ { "fallback": @@ -113,6 +120,7 @@ will notify #all-push-events ===== file ../mock_payloads/commit_comment.short_comment.json ===== will notify #all-push-events { + "channel": "all-push-events", "attachments": [ { "fallback": @@ -130,6 +138,7 @@ will notify #all-push-events ===== file ../mock_payloads/issue_comment.created_in_issue.json ===== will notify #default { + "channel": "default", "attachments": [ { "fallback": @@ -145,6 +154,7 @@ will notify #default ===== file ../mock_payloads/issue_comment.created_in_pr.json ===== will notify #aa-git { + "channel": "aa-git", "attachments": [ { "fallback": @@ -159,6 +169,7 @@ will notify #aa-git } will notify #backend { + "channel": "backend", "attachments": [ { "fallback": @@ -175,6 +186,7 @@ will notify #backend ===== file ../mock_payloads/issue_comment.draft_pr.json ===== will notify #frontend-bot { + "channel": "frontend-bot", "attachments": [ { "fallback": @@ -192,6 +204,7 @@ will notify #frontend-bot ===== file ../mock_payloads/issues.closed.json ===== will notify #backend { + "channel": "backend", "attachments": [ { "fallback": @@ -206,6 +219,7 @@ will notify #backend ===== file ../mock_payloads/issues.labeled.json ===== will notify #backend { + "channel": "backend", "attachments": [ { "fallback": @@ -221,6 +235,7 @@ will notify #backend ===== file ../mock_payloads/issues.opened.json ===== will notify #default { + "channel": "default", "attachments": [ { "fallback": @@ -237,6 +252,7 @@ will notify #default ===== file ../mock_payloads/issues.reopened.json ===== will notify #backend { + "channel": "backend", "attachments": [ { "fallback": @@ -251,6 +267,7 @@ will notify #backend ===== file ../mock_payloads/pull_request.closed_capitalized.json ===== will notify #aa-git { + "channel": "aa-git", "attachments": [ { "fallback": @@ -265,6 +282,7 @@ will notify #aa-git ===== file ../mock_payloads/pull_request.closed_no_labels.json ===== will notify #default { + "channel": "default", "attachments": [ { "fallback": @@ -279,6 +297,7 @@ will notify #default ===== file ../mock_payloads/pull_request.labeled_two_labels.json ===== will notify #aa-git { + "channel": "aa-git", "attachments": [ { "fallback": @@ -293,6 +312,7 @@ will notify #aa-git } will notify #backend { + "channel": "backend", "attachments": [ { "fallback": @@ -308,6 +328,7 @@ will notify #backend ===== file ../mock_payloads/pull_request.opened_one_label.json ===== will notify #frontend-bot { + "channel": "frontend-bot", "attachments": [ { "fallback": @@ -324,6 +345,7 @@ will notify #frontend-bot ===== file ../mock_payloads/pull_request_review.approved.json ===== will notify #default { + "channel": "default", "attachments": [ { "fallback": @@ -339,6 +361,7 @@ will notify #default ===== file ../mock_payloads/pull_request_review.commented.json ===== will notify #frontend-bot { + "channel": "frontend-bot", "attachments": [ { "fallback": @@ -356,6 +379,7 @@ will notify #frontend-bot ===== file ../mock_payloads/pull_request_review.request_changes.json ===== will notify #default { + "channel": "default", "attachments": [ { "fallback": @@ -373,6 +397,7 @@ will notify #default ===== file ../mock_payloads/pull_request_review_comment.created.json ===== will notify #aa-git { + "channel": "aa-git", "attachments": [ { "fallback": @@ -389,6 +414,7 @@ will notify #aa-git } will notify #backend { + "channel": "backend", "attachments": [ { "fallback": @@ -408,6 +434,7 @@ will notify #backend ===== file ../mock_payloads/push.two_commits.json ===== will notify #all-push-events { + "channel": "all-push-events", "text": " pushed by thatportugueseguy", "attachments": [ @@ -427,6 +454,7 @@ will notify #all-push-events ===== file ../mock_payloads/push.two_commits_longest_match.json ===== will notify #all-push-events { + "channel": "all-push-events", "text": " pushed by thatportugueseguy", "attachments": [ @@ -445,6 +473,7 @@ will notify #all-push-events } will notify #longest-aa { + "channel": "longest-aa", "text": " pushed by thatportugueseguy", "attachments": [ @@ -465,6 +494,7 @@ will notify #longest-aa ===== file ../mock_payloads/status.failure_test.json ===== will notify #default { + "channel": "default", "attachments": [ { "fallback": @@ -490,6 +520,7 @@ will notify #default ===== file ../mock_payloads/status.success_public_repo_no_buildkite.json ===== will notify #default { + "channel": "default", "attachments": [ { "fallback": @@ -510,6 +541,7 @@ will notify #default ===== file ../mock_payloads/status.success_test_main_branch.json ===== will notify #all-push-events { + "channel": "all-push-events", "attachments": [ { "fallback": @@ -531,6 +563,7 @@ will notify #all-push-events ===== file ../mock_payloads/status.success_test_non_main_branch.json ===== will notify #default { + "channel": "default", "attachments": [ { "fallback": From 7404d4a91a0e8cdee2b8dd278983baf9629b6e90 Mon Sep 17 00:00:00 2001 From: Yasunari Watanabe Date: Mon, 4 Jan 2021 10:27:32 +0800 Subject: [PATCH 6/6] update docs with slack access token setup and secrets file changes --- README.md | 18 ++++++++++- documentation/config_docs.md | 4 +-- documentation/secret_docs.md | 63 ++++++++++++++++++------------------ 3 files changed, 51 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 39f9f93e..8ae6b5f5 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,23 @@ Run the `_build/default/src/monorobot.exe` binary. The following commands are su - `run`: Launch the HTTP server - `check_gh `: read a Github notification from a file and display the actions that will be taken (used for testing) -- `check_slack `: read a Slack notification from a file and send it to a webhook (used for testing) +- `check_slack `: read a Slack notification from a file and send it to a channel (used for testing) + +## Getting Started + +1. Commit a **repository configuration** file to the root of your target repository. +2. Place a **secrets** file locally on the server. +3. Configure GitHub + 1. If targeting a private repository, set up a [personal access token](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token) with `repo` scope and store it in the `gh_token` field of the secrets file. + 2. [Create a webhook](https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/creating-webhooks#setting-up-a-webhook) for the repository you are targeting. Set the *Payload URL* to be `/github`. + 3. You can optionally [secure the webhook](https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/securing-your-webhooks) with a token, and store it in the `gh_hook_token` field of the secrets file. +4. Configure Slack + 1. [Create a Slack app](https://api.slack.com/apps?new_app=1). + 2. Click "Install to Workspace", and when prompted to grant permissions to your workspace, click "Allow". + 3. Set up notifications with one of the following methods: + - **Web API (recommended):** To use Slack's [Web API](https://api.slack.com/web), click on "OAuth & Permissions" in your app dashboard's sidebar. Give your bot a *Bot Token Scope* of `chat:write`. Copy the generated OAuth access token (`xoxb-XXXX`) to the `slack_access_token` field of your secrets file. This token is used by the bot to authenticate to the workspace, and remains valid until the token is revoked or the app is uninstalled. + - **Incoming Webhooks:** To use [incoming webhooks](https://api.slack.com/messaging/webhooks), enable them in your app dashboard and create one for each channel you want to notify. Store them in the `slack_hooks` field of your secrets file. If you decide to notify additional channels later, you will need to update the secrets file with the new webhooks and restart the server. + ### Documentation diff --git a/documentation/config_docs.md b/documentation/config_docs.md index b5e045f9..2e779237 100644 --- a/documentation/config_docs.md +++ b/documentation/config_docs.md @@ -86,7 +86,7 @@ A **label rule** specifies whether or not a Slack channel should be notified, ba |-|-|-|-| | `match` | if notifications have any label in this list, they should be routed to the channel | Yes | all labels matched if no list provided | | `ignore` | if notifications have any label in this list, they shouldn't be routed to the channel (even if they have any `match` labels) | Yes | - | -| `channel` | channel to use as webhook if the rule is matched | No | - | +| `channel` | channel to notify if the rule is matched | No | - | ## Prefix Options @@ -125,7 +125,7 @@ A **prefix rule** specifies whether or not a Slack channel should be notified, b |-|-|-|-| | `match` | if commit files have any prefix in this list, they should be routed to the channel | Yes | all prefixes matched if no list provided | | `ignore` | if commit files have any prefix in this list, they shouldn't be routed to the channel (even if they have any `match` prefixes) | Yes | - | -| `channel` | channel to use as webhook if the rule is matched | No | - | +| `channel` | channel to notify if the rule is matched | No | - | ## Status Options diff --git a/documentation/secret_docs.md b/documentation/secret_docs.md index 516d894d..d66506b1 100644 --- a/documentation/secret_docs.md +++ b/documentation/secret_docs.md @@ -8,44 +8,19 @@ A secrets file stores sensitive information. Unlike the repository configuration ```json { - "slack_hooks": [ - { - "url": "https://slack_webhook_url", - "channel": "default" - }, - { - "url": "https://slack_webhook_url", - "channel": "aa" - }, - { - "url": "https://slack_webhook_url", - "channel": "backend" - }, - { - "url": "https://slack_webhook_url", - "channel": "all-push-events" - }, - { - "url": "https://slack_webhook_url", - "channel": "frontend-bot" - }, - { - "url": "https://slack_webhook_url", - "channel": "aa-git" - }, - { - "url": "https://slack_webhook_url", - "channel": "siren" - } - ] + "gh_token": "", + "slack_access_token": "" } ``` | value | description | optional | default | |-|-|-|-| -| `slack_hooks` | list of channel names (`channel`) and their corresponding webhook endpoint (`url`) | No | - | | `gh_token` | specify to grant the bot access to private repositories; omit for public repositories | Yes | - | | `gh_hook_token` | specify to ensure the bot only receives GitHub notifications from pre-approved repositories | Yes | - | +| `slack_access_token` | slack bot access token to enable message posting to the workspace | Yes | try to use webhooks defined in `slack_hooks` instead | +| `slack_hooks` | list of channel names and their corresponding webhook endpoint | Yes | try to use token defined in `slack_access_token` instead | + +Note that either `slack_access_token` or `slack_hooks` must be defined. ## `gh_token` @@ -54,3 +29,29 @@ Some operations, such as fetching a config file from a private repository, or th ## `gh_hook_token` 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. + +## `slack_access_token` + +Refer [here](https://api.slack.com/authentication/oauth-v2) for obtaining an access token via OAuth. + +## `slack_hooks` + +*Note: If `slack_access_token` is also defined, the bot will authenticate over Slack's Web API and this option will not be used.* + +Expected format: + +```json +[ + { + "channel": "channel name", + "url": "webhook url" + }, + { + "channel": "channel name", + "url": "webhook url" + }, + ... +] +``` + +Refer [here](https://api.slack.com/messaging/webhooks) for obtaining a webhook for a channel.