Skip to content

Commit

Permalink
update notification logic
Browse files Browse the repository at this point in the history
  • Loading branch information
thatportugueseguy committed Sep 27, 2024
1 parent dc96df5 commit 7ddbc93
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 66 deletions.
87 changes: 52 additions & 35 deletions lib/action.ml
Original file line number Diff line number Diff line change
Expand Up @@ -128,56 +128,73 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
in
if matched_channel_names = [] then default else matched_channel_names

let buildkite_is_failed_re = Re2.create_exn {|^Build #\d+ failed|}

let partition_status (ctx : Context.t) (n : status_notification) =
let repo = n.repository in
let cfg = Context.find_repo_config_exn ctx repo.url in
let pipeline = n.context in
let current_status = n.state in
let rules = cfg.status_rules.rules in
let repo_state = State.find_or_add_repo ctx.state n.repository.url in
let broken_steps = Util.Build.new_failed_steps n repo_state pipeline in
let no_notify = n.state = Failure && broken_steps = [] in

let action_on_match (branches : branch list) ~notify_channels ~notify_dm =
let%lwt direct_message =
if notify_dm then begin
match%lwt Slack_api.lookup_user ~ctx ~cfg ~email:n.commit.commit.author.email () with
| Ok res ->
State.set_repo_pipeline_commit ctx.state repo.url ~pipeline ~commit:n.sha;
(* To send a DM, channel parameter is set to the user id of the recipient *)
Lwt.return [ res.user.id ]
| Error e ->
log#warn "couldn't match commit email %s to slack profile: %s" n.commit.commit.author.email e;
Lwt.return []
end
else Lwt.return []
in
let%lwt chans =
let default = Stdlib.Option.to_list cfg.prefix_rules.default_channel in
match notify_channels with
| false -> Lwt.return []
| true ->
match branches with
| [] -> Lwt.return []
| _ ->
match cfg.main_branch_name with
| None -> Lwt.return default
| Some main_branch_name ->
match List.exists (fun ({ name } : branch) -> String.equal name main_branch_name) branches with
| false ->
(* non-main branch build notifications go to default channel to reduce spam in topic channels *)
Lwt.return default
| true ->
let sha = n.commit.sha in
(match%lwt Github_api.get_api_commit ~ctx ~repo ~sha with
| Error e -> action_error e
| Ok commit -> Lwt.return @@ partition_commit cfg commit.files)
in
Lwt.return (direct_message @ chans)
match no_notify with
| true -> Lwt.return []
| false ->
let%lwt direct_message =
if notify_dm then begin
match%lwt Slack_api.lookup_user ~ctx ~cfg ~email:n.commit.commit.author.email () with
| Ok res ->
State.set_repo_pipeline_commit ctx.state repo.url ~pipeline ~commit:n.sha;
(* To send a DM, channel parameter is set to the user id of the recipient *)
Lwt.return [ res.user.id ]
| Error e ->
log#warn "couldn't match commit email %s to slack profile: %s" n.commit.commit.author.email e;
Lwt.return []
end
else Lwt.return []
in
let%lwt chans =
let default = Stdlib.Option.to_list cfg.prefix_rules.default_channel in
match notify_channels with
| false -> Lwt.return []
| true ->
match branches with
| [] -> Lwt.return []
| _ ->
match cfg.main_branch_name with
| None -> Lwt.return default
| Some main_branch_name ->
match List.exists (fun ({ name } : branch) -> String.equal name main_branch_name) branches with
| false ->
(* non-main branch build notifications go to default channel to reduce spam in topic channels *)
Lwt.return default
| true ->
let sha = n.commit.sha in
(match%lwt Github_api.get_api_commit ~ctx ~repo ~sha with
| Error e -> action_error e
| Ok commit -> Lwt.return @@ partition_commit cfg commit.files)
in
Lwt.return (direct_message @ chans)
in

let%lwt recipients =
if Context.is_pipeline_allowed ctx repo.url ~pipeline then begin
let repo_state = State.find_or_add_repo ctx.state repo.url in
match Rule.Status.match_rules ~rules n with
| Some (Ignore, _, _) | None -> Lwt.return []
| Some (Allow, notify_channels, notify_dm) -> action_on_match n.branches ~notify_channels ~notify_dm
| Some (Allow_once, notify_channels, notify_dm) ->
(* for failing states, if we only allow one notification, it must be the final one, not an intermediate one *)
match
n.state <> Failure
|| (n.state = Failure && Re2.matches buildkite_is_failed_re (Option.default "" n.description))
with
| false -> Lwt.return []
| true ->
let branches =
match StringMap.find_opt pipeline repo_state.pipeline_statuses with
| Some branch_statuses ->
Expand Down
41 changes: 10 additions & 31 deletions lib/slack.ml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
open Printf
open Common
open Util
open Mrkdwn
open Github_j
open Slack_j
Expand Down Expand Up @@ -296,39 +297,17 @@ let generate_status_notification (ctx : Context.t) (cfg : Config_t.config) (noti
in
let failed_steps_info =
let repo_state = State.find_or_add_repo ctx.state repository.url in
let new_failing_steps =
match notification.state, notification.branches with
| (Success | Pending | Error), _ | _, [] -> []
| Failure, [ current_branch ] ->
StringMap.fold
(fun step branches_statuses acc ->
(* check if step of an allowed pipeline *)
match step with
| s when s = pipeline -> acc
| s when not @@ Devkit.Stre.starts_with s pipeline -> acc
| _ ->
(* check if this step failed *)
match StringMap.find_opt current_branch.name branches_statuses with
| Some (build_status : State_t.build_status) when build_status.status = Failure ->
let failing_commit_link =
match build_status.current_failed_commit, build_status.original_failed_commit with
| Some { build_link = Some build_link; _ }, _ -> build_link
| None, Some { build_link = Some build_link; _ } -> build_link
| _ ->
(* if we can't get the correct build link, use the pipeline one.
if we can't get either, don't use any *)
Option.default "" notification.target_url
in
(step, failing_commit_link) :: acc
| _ -> acc)
repo_state.pipeline_statuses []
| Failure, _ -> []
in
match new_failing_steps with
match Build.new_failed_steps notification repo_state pipeline with
| [] -> []
| steps_and_links ->
let steps_with_links = String.concat ", " @@ List.map (fun (s, l) -> sprintf "<%s|%s>" l s) steps_and_links in
[ sprintf "*Steps broken*: %s" steps_with_links ]
let steps_with_links =
List.map
(fun (s, l) ->
let step = Devkit.Stre.drop_prefix s (notification.context ^ "/") in
sprintf "<%s|%s>" l step)
steps_and_links
in
[ sprintf "*Steps broken*: %s" (String.concat ", " steps_with_links) ]
in
let summary =
let state_info =
Expand Down
30 changes: 30 additions & 0 deletions lib/util.ml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
open Devkit

module StringMap = Common.StringMap

let fmt_error ?exn fmt =
Printf.ksprintf
(fun s ->
Expand All @@ -26,3 +28,31 @@ let http_request ?headers ?body meth path =

let sign_string_sha256 ~key ~basestring =
Cstruct.of_string basestring |> Nocrypto.Hash.SHA256.hmac ~key:(Cstruct.of_string key) |> Hex.of_cstruct |> Hex.show

module Build = struct
let new_failed_steps (n : Github_t.status_notification) (repo_state : State_t.repo_state) pipeline =
match n.state = Failure, n.branches with
| false, _ | true, [] -> []
| true, [ branch ] ->
StringMap.fold
(fun step statuses acc ->
(* check if step of an allowed pipeline *)
match step with
| step when step = pipeline -> acc
| step when not @@ Devkit.Stre.starts_with step pipeline -> acc
| _ ->
(* check if this step failed *)
match StringMap.find_opt branch.name statuses with
| Some (s : State_t.build_status) when s.status = Failure ->
(match s.current_failed_commit, s.original_failed_commit with
| Some _, _ ->
(* if we have a value for current_failed_commit, this step was already failed and notified *)
acc
| None, Some { build_link = Some build_link; sha; _ } when sha = n.commit.sha ->
(* we need to check the value of the commit sha to avoid false positives *)
(step, build_link) :: acc
| _ -> acc)
| _ -> acc)
repo_state.pipeline_statuses []
| true, _ -> []
end

0 comments on commit 7ddbc93

Please sign in to comment.