Skip to content

Commit

Permalink
Add dev notifications config (#170)
Browse files Browse the repository at this point in the history
* add dev notifications config for failed and failing builds

* update config docs
  • Loading branch information
thatportugueseguy authored Dec 30, 2024
1 parent 847588d commit 31bd0d7
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 16 deletions.
14 changes: 13 additions & 1 deletion documentation/config_docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ Refer [here](https://docs.github.com/en/free-pro-team@latest/developers/webhooks
"[email protected]": "[email protected]",
"gh-handle": "[email protected]"
},
"notifications_configs": {
"dm_for_failing_build": {
"[email protected]": false,
"[email protected]": false
},
"dm_after_failed_build": {
"[email protected]": true
}
},
"prefix_rules": {
...
},
Expand All @@ -46,8 +55,11 @@ Refer [here](https://docs.github.com/en/free-pro-team@latest/developers/webhooks
| `project_owners` | project owners config object | no project owners are defined |
| `ignored_users` | list of users to be ignored on all notifications | no user is ignored |
| `user_mappings` | list of mappings from git email and/or GitHub handle to Slack email | no mapping defined
| `notifications_configs` | list of configs for certain types of notifications | dm_for_failing_build defaults to true and dm_after_failed_build to false

Note that `notifications_configs` expects to match against the email used in slack. It will respect the `user_mappings` rules and then use the resulting slack email to find the correct user id and @mention. If you are not using the `user_mappings` feature, you can directly use the slack email in the `notifications_configs` field.

Note that in `user_mappings`, git email to Slack email mappings are used for status DMs, while GitHub handle to Slack email mappings are used to get Slack mentions in comment notifications.
Also note that in `user_mappings`, git email to Slack email mappings are used for status DMs, while GitHub handle to Slack email mappings are used to get Slack mentions in comment notifications.

The reason for these two separate Slack email matching schemes is that in the case of commits, the git email is available in the GitHub payload and can be used to directly match with Slack emails for status DMs (`user_mappings` can be used to manually override if there is a known mismatch between git email and Slack email). However, for actions done on GitHub itself (e.g. opening PRs, commenting, etc.), usually the only thing available is the GitHub username. To get the email in these cases, a user will have to go to settings and set their email to public, which is (1) hard to enforce if there are many working on the monorepo, and (2) might be undesirable for privacy reasons.

Expand Down
39 changes: 31 additions & 8 deletions lib/action.ml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
let repo = n.repository in
let cfg = Context.find_repo_config_exn ctx repo.url in
let context = n.context in
let email = n.commit.commit.author.email in
let rules = cfg.status_rules.rules in
let repo_state = State.find_or_add_repo ctx.state repo.url in
let action_on_match (branches : branch list) ~notify_channels ~notify_dm =
Expand All @@ -159,11 +160,31 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
in
let%lwt direct_message =
if notify_dm then begin
match%lwt Slack_api.lookup_user ~ctx ~cfg ~email:n.commit.commit.author.email () with
match%lwt Slack_api.lookup_user ~ctx ~cfg ~email () with
| Ok res ->
State.set_repo_pipeline_commit ctx.state n;
(* To send a DM, channel parameter is set to the user id of the recipient *)
Lwt.return [ Slack_user_id.to_channel_id res.user.id ]
let is_failing_build = Util.Build.is_failing_build n in
let is_failed_build = Util.Build.is_failed_build n in
(* Check if config holds Github to Slack email mapping for the commit author. The user id we get from slack
is not an email, so we need to see if we can map the commit author email to a slack user's email. *)
let author = List.assoc_opt email cfg.user_mappings |> Option.default email in
let dm_after_failed_build =
List.assoc_opt author cfg.notifications_configs.dm_after_failed_build
|> (* dm_after_failed_build is opt in *)
Option.default false
in
let dm_for_failing_build =
List.assoc_opt author cfg.notifications_configs.dm_for_failing_build
|> (* dm_for_failing_build is opt out *)
Option.default true
in
(match (dm_for_failing_build && is_failing_build) || (dm_after_failed_build && is_failed_build) with
| true ->
(* if we send a dm for a failing build and we want another dm after the build is finished, we don't
set the pipeline commit immediately. Otherwise, we wouldn't be able to notify later *)
if (is_failing_build && not dm_after_failed_build) || is_failed_build then
State.set_repo_pipeline_commit ctx.state n;
Lwt.return [ Status_notification.User res.user.id ]
| false -> Lwt.return [])
| Error e ->
log#warn "couldn't match commit email %s to slack profile: %s" n.commit.commit.author.email e;
Lwt.return []
Expand All @@ -177,13 +198,14 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
(* non-main branch build notifications go to default channel to reduce spam in topic channels *)
match is_main_branch with
| false ->
Lwt.return (Option.map_default (fun c -> [ Slack_channel.to_any c ]) [] cfg.prefix_rules.default_channel)
Lwt.return
(Option.map_default (fun c -> [ Status_notification.inject_channel c ]) [] cfg.prefix_rules.default_channel)
| true ->
(match%lwt Github_api.get_api_commit ~ctx ~repo ~sha:n.commit.sha with
| Error e -> action_error e
| Ok commit ->
let chans = partition_commit cfg commit.files in
Lwt.return (List.map Slack_channel.to_any chans))
Lwt.return (List.map Status_notification.inject_channel chans))
in
(* only notify the failed builds channels for full failed builds with new failed steps on the main branch *)
let notify_failed_builds_channel =
Expand All @@ -206,11 +228,12 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
List.find_map
(fun ({ name; failed_builds_channel } : Config_t.pipeline) ->
match String.equal name context, failed_builds_channel with
| true, Some failed_builds_channel -> Some (Slack_channel.to_any failed_builds_channel :: chans)
| true, Some failed_builds_channel ->
Some (Status_notification.inject_channel failed_builds_channel :: chans)
| _ -> None)
allowed_pipelines
|> Option.default chans
|> List.sort_uniq Slack_channel.compare
|> List.sort_uniq Status_notification.compare
in
Lwt.return (direct_message @ chans)
in
Expand Down
19 changes: 19 additions & 0 deletions lib/common.ml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,25 @@ module StringSet = struct
let unwrap = to_list
end

module Status_notification = struct
type t =
| Channel of Slack_channel.Any.t
| User of Slack_user_id.t

let inject_channel c = Channel (Slack_channel.to_any c)

let to_slack_channel = function
| Channel c -> c
| User u -> Slack_user_id.to_channel_id u

let is_user = function
| User _ -> true
| Channel _ -> false

let compare a b = Slack_channel.compare (to_slack_channel a) (to_slack_channel b)
let equal a b = compare a b = 0
end

module Map (S : Map.OrderedType) = struct
include Map.Make (S)

Expand Down
8 changes: 7 additions & 1 deletion lib/config.atd
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ type project_owners = {
rules: project_owners_rule list;
}

type notifications_configs = {
~dm_for_failing_build <ocaml default="[]">: (string * bool) list <json repr="object">;
~dm_after_failed_build <ocaml default="[]">: (string * bool) list <json repr="object">
}

(* This is the structure of the repository configuration file. It should be at the
root of the monorepo, on the main branch. *)
type config = {
Expand All @@ -44,7 +49,8 @@ type config = {
~project_owners <ocaml default="{rules = []}"> : project_owners;
~ignored_users <ocaml default="[]">: string list; (* list of ignored users *)
?main_branch_name : string nullable; (* the name of the main branch; used to filter out notifications about merges of main branch into other branches *)
~user_mappings <ocaml default="[]">: (string * string) list <json repr="object"> (* list of github to slack profile mappings *)
~user_mappings <ocaml default="[]">: (string * string) list <json repr="object">; (* list of github to slack profile mappings *)
~notifications_configs <ocaml default="{dm_after_failed_build = []; dm_for_failing_build = []}"> : notifications_configs;
}

(* This specifies the Slack webhook to query to post to the channel with the given name *)
Expand Down
12 changes: 6 additions & 6 deletions lib/slack.ml
Original file line number Diff line number Diff line change
Expand Up @@ -335,9 +335,9 @@ let generate_status_notification ~(ctx : Context.t) ?slack_user_id (cfg : Config
let commit_info =
[
(let mention =
match slack_user_id with
| None -> ""
| Some id -> sprintf "<@%s>" (Slack_user_id.project id)
match slack_user_id, channel with
| None, _ | _, Status_notification.User _ -> ""
| Some id, Channel _ -> sprintf "<@%s>" (Slack_user_id.project id)
in
sprintf "*Commit*: `<%s|%s>` %s" html_url (git_short_sha_hash sha) mention);
]
Expand Down Expand Up @@ -395,10 +395,10 @@ let generate_status_notification ~(ctx : Context.t) ?slack_user_id (cfg : Config
List.exists
(fun ({ name; failed_builds_channel } : Config_t.pipeline) ->
String.equal name context
&& Option.map_default Slack_channel.(equal channel $ to_any) false failed_builds_channel)
&& Option.map_default Status_notification.(equal channel $ inject_channel) false failed_builds_channel)
pipelines
in
match Build.is_failed_build notification && is_failed_builds_channel with
match Build.is_failed_build notification && (is_failed_builds_channel || Status_notification.is_user channel) with
| false -> []
| true ->
let repo_state = State.find_or_add_repo ctx.state repository.url in
Expand All @@ -415,7 +415,7 @@ let generate_status_notification ~(ctx : Context.t) ?slack_user_id (cfg : Config
let attachment =
{ empty_attachments with mrkdwn_in = Some [ "fields"; "text" ]; color = Some color_info; text = Some msg }
in
make_message ~text:summary ~attachments:[ attachment ] ~channel:(Slack_channel.to_any channel) ()
make_message ~text:summary ~attachments:[ attachment ] ~channel:(Status_notification.to_slack_channel channel) ()

let generate_commit_comment_notification ~slack_match_func api_commit notification channel =
let { commit; _ } = api_commit in
Expand Down
4 changes: 4 additions & 0 deletions lib/util.ml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ module Build = struct
}

let buildkite_is_failed_re = Re2.create_exn {|^Build #\d+ failed|}
let buildkite_is_failing_re = Re2.create_exn {|^Build #(\d+) is failing|}

let buildkite_is_step_re =
(* Checks if a pipeline or build step, by looking into the buildkite context
Expand Down Expand Up @@ -74,6 +75,9 @@ module Build = struct
let is_failed_build (n : Github_t.status_notification) =
n.state = Failure && Re2.matches buildkite_is_failed_re (Option.default "" n.description)

let is_failing_build (n : Github_t.status_notification) =
n.state = Failure && Re2.matches buildkite_is_failing_re (Option.default "" n.description)

let new_failed_steps (n : Github_t.status_notification) (repo_state : State_t.repo_state) =
if not (is_failed_build n) then failwith "Error: new_failed_steps fn must be called on a finished build";
match n.target_url with
Expand Down

0 comments on commit 31bd0d7

Please sign in to comment.