Skip to content

Commit f3c0f35

Browse files
committed
Merge branch 'master' into feature/#43
* master: (39 commits) Add devkit.core to test Add devkit.core to src re-add devkit.core dependency removed in 4137e70 tests: update expected with change from 2edb227 tests: make tests compile again with explicit transitive deps implicit_transitive_deps false non-main branch build notifications go to default channel only (ref #81) minor tests: promote highlight author name slack: fix unescaping of parentheses slack: tweak escaping slack: properly transform img links issues: show body only for "new issue" notification better branch list in CI notification (fix #73) minor improvement for CI notification message filter out merges of main branch into feature after feature branch is merged into main better logs use standard json specify success state as tristate switch ...
2 parents 7a5c952 + 53b6ac3 commit f3c0f35

17 files changed

+545
-208
lines changed

dune-project

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
(lang dune 2.5)
2+
(implicit_transitive_deps false)
23

34
(formatting
45
(enabled_for ocaml reason))

lib/action.ml

+116-63
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,52 @@ open Github_j
99

1010
let log = Log.from "action"
1111

12-
let touching_prefix rule name =
13-
let has_prefix s = List.exists ~f:(fun prefix -> String.is_prefix s ~prefix) in
14-
(List.is_empty rule.prefix || has_prefix name rule.prefix) && not (has_prefix name rule.ignore)
12+
type prefix_match =
13+
| Match of int
14+
| NoMatch
15+
16+
let chan_of_prefix_rule (r : prefix_rule) = r.chan
17+
18+
let touching_prefix (rule : Notabot_t.prefix_rule) name =
19+
let match_lengths filename prefixes =
20+
List.filter_map
21+
~f:(fun prefix -> if String.is_prefix filename ~prefix then Some (String.length prefix) else None)
22+
prefixes
23+
in
24+
match match_lengths name rule.ignore with
25+
| _ :: _ -> NoMatch
26+
| [] ->
27+
match rule.prefix with
28+
| [] -> Match 0
29+
| _ ->
30+
match List.max_elt (match_lengths name rule.prefix) ~compare:Int.compare with
31+
| Some x -> Match x
32+
| None -> NoMatch
33+
34+
let longest_touching_prefix_rule rules name =
35+
let get_m rule = touching_prefix rule name in
36+
let reduce_to_longest_match longest_rule_match_pair current_rule =
37+
let _, longest_match = longest_rule_match_pair in
38+
let current_match = get_m current_rule in
39+
let current_rule_match_pair = current_rule, current_match in
40+
match longest_match with
41+
| NoMatch -> current_rule_match_pair
42+
| Match x ->
43+
match current_match with
44+
| NoMatch -> longest_rule_match_pair
45+
| Match y -> if y > x then current_rule_match_pair else longest_rule_match_pair
46+
in
47+
match rules with
48+
| [] -> None
49+
| (x : prefix_rule) :: xs ->
50+
match List.fold_left xs ~init:(x, get_m x) ~f:reduce_to_longest_match with
51+
| _, NoMatch -> None
52+
| r, Match _ -> Some r
53+
54+
let chan_of_file rules file = Option.map ~f:chan_of_prefix_rule @@ longest_touching_prefix_rule rules file
55+
56+
let unique_chans_of_files rules files =
57+
List.dedup_and_sort ~compare:String.compare @@ List.filter_map files ~f:(chan_of_file rules)
1558

1659
let touching_label rule name =
1760
let name_lc = String.lowercase name in
@@ -23,6 +66,15 @@ let touching_label rule name =
2366

2467
let is_main_merge_message ~msg:message ~branch cfg =
2568
match cfg.main_branch_name with
69+
| Some main_branch when String.equal branch main_branch ->
70+
(*
71+
handle "Merge <main branch> into <feature branch>" commits when they are merged into main branch
72+
we should have already seen these commits on the feature branch but for some reason they are distinct:true
73+
*)
74+
let prefix = sprintf "Merge branch '%s' into " main_branch in
75+
let prefix2 = sprintf "Merge remote-tracking branch 'origin/%s' into " main_branch in
76+
let title = first_line message in
77+
String.is_prefix title ~prefix || String.is_prefix title ~prefix:prefix2
2678
| Some main_branch ->
2779
let expect = sprintf "Merge branch '%s' into %s" main_branch branch in
2880
let expect2 = sprintf "Merge remote-tracking branch 'origin/%s' into %s" main_branch branch in
@@ -31,18 +83,12 @@ let is_main_merge_message ~msg:message ~branch cfg =
3183
| _ -> false
3284

3385
let filter_push rules commit =
34-
let matching_push rule files = List.exists files ~f:(fun file -> touching_prefix rule file) in
35-
List.filter_map rules ~f:(fun rule ->
36-
let filter =
37-
matching_push rule commit.added || matching_push rule commit.removed || matching_push rule commit.modified
38-
in
39-
match filter with
40-
| false -> None
41-
| true -> Some (rule.chan, commit))
86+
let files = List.concat [ commit.added; commit.removed; commit.modified ] in
87+
List.map ~f:(fun chan -> chan, commit) @@ unique_chans_of_files rules files
4288

43-
let group_commit webhook l =
44-
List.filter_map l ~f:(fun (chan, commit) ->
45-
match String.equal webhook chan with
89+
let group_commit chan l =
90+
List.filter_map l ~f:(fun (chan', commit) ->
91+
match String.equal chan chan' with
4692
| false -> None
4793
| true -> Some commit)
4894

@@ -61,15 +107,14 @@ let partition_push cfg n =
61107
match filter_push rules commit with
62108
| [] -> default commit
63109
| l -> l)
110+
|> List.concat
64111
in
65-
let concat_chan = List.concat channels in
66112
let prefix_chans =
67-
let chans = List.map rules ~f:(fun (rule : prefix_rule) -> rule.chan) in
68-
let chans = Option.value_map cfg.prefix_rules.default ~default:chans ~f:(fun default -> default :: chans) in
113+
let chans = Option.to_list cfg.prefix_rules.default @ List.map rules ~f:(fun (rule : prefix_rule) -> rule.chan) in
69114
List.dedup_and_sort chans ~compare:String.compare
70115
in
71116
List.filter_map prefix_chans ~f:(fun chan ->
72-
match group_commit chan concat_chan with
117+
match group_commit chan channels with
73118
| [] -> None
74119
| l -> Some (chan, { n with commits = l }))
75120

@@ -81,7 +126,7 @@ let filter_label rules (label : Github_j.label) =
81126
| true -> Some rule.chan)
82127

83128
let partition_label cfg (labels : Github_j.label list) =
84-
let default = Option.value_map cfg.label_rules.default ~default:[] ~f:(fun webhook -> [ webhook ]) in
129+
let default = Option.to_list cfg.label_rules.default in
85130
match labels with
86131
| [] -> default
87132
| labels ->
@@ -131,41 +176,33 @@ let partition_pr_review cfg (n : pr_review_notification) =
131176
| Submitted, _, _ -> partition_label cfg n.pull_request.labels
132177
| _ -> []
133178

134-
let filter_commit rules filename =
135-
rules
136-
|> List.filter_map ~f:(fun rule ->
137-
match touching_prefix rule filename with
138-
| false -> None
139-
| true -> Some rule.chan)
140-
141179
let partition_commit cfg files =
142-
let default = Option.value_map cfg.prefix_rules.default ~default:[] ~f:(fun webhook -> [ webhook ]) in
143-
match files with
144-
| [] ->
145-
log#error "this commit contains no files";
146-
[]
147-
| files ->
148-
let rules = cfg.prefix_rules.rules in
149-
let channels =
150-
files
151-
|> List.map ~f:(fun file ->
152-
match filter_commit rules file.filename with
153-
| [] -> default
154-
| l -> l)
155-
in
156-
List.dedup_and_sort ~compare:String.compare (List.concat channels)
180+
let names = List.map ~f:(fun f -> f.filename) files in
181+
match unique_chans_of_files cfg.prefix_rules.rules names with
182+
| _ :: _ as xs -> xs
183+
| [] -> Option.to_list cfg.prefix_rules.default
157184

158185
let hide_cancelled (notification : status_notification) cfg =
159-
let is_cancelled_status =
186+
let find_cancelled status_state =
187+
match status_state with
188+
| Config.Cancelled r -> Some r
189+
| _ -> None
190+
in
191+
let regexp_opt = List.find_map cfg.status_rules.status ~f:find_cancelled in
192+
match regexp_opt with
193+
| None -> false
194+
| Some regexp ->
160195
let { state; description; _ } = notification in
161-
let r = Re.Str.regexp_case_fold "^\\(Build #[0-9]+ canceled by .+\\|Failed (exit status 255)\\)$" in
162-
match description, state with
196+
let r = Re.Str.regexp_case_fold regexp in
197+
( match description, state with
163198
| Some s, Failure when Re.Str.string_match r s 0 -> true
164199
| _ -> false
165-
in
166-
is_cancelled_status && cfg.suppress_cancelled_events
200+
)
167201

168202
let hide_success (n : status_notification) (ctx : Context.t) =
203+
match List.exists ctx.cfg.status_rules.status ~f:(Poly.equal Config.HideConsecutiveSuccess) with
204+
| false -> false
205+
| true ->
169206
match n.state with
170207
| Success ->
171208
List.exists
@@ -179,21 +216,37 @@ let hide_success (n : status_notification) (ctx : Context.t) =
179216
let partition_status (ctx : Context.t) (n : status_notification) =
180217
let cfg = ctx.cfg in
181218
let get_commit_info () =
182-
match%lwt Github.generate_query_commmit cfg ~url:n.commit.url ~sha:n.commit.sha with
183-
| None ->
184-
let default = Option.value_map cfg.prefix_rules.default ~default:[] ~f:(fun webhook -> [ webhook ]) in
185-
Lwt.return default
186-
| Some commit ->
187-
match
188-
List.exists n.branches ~f:(fun { name } -> is_main_merge_message ~msg:commit.commit.message ~branch:name cfg)
189-
with
219+
let default () = Lwt.return @@ Option.to_list cfg.prefix_rules.default in
220+
match cfg.main_branch_name with
221+
| None -> default ()
222+
| Some main_branch_name ->
223+
(* non-main branch build notifications go to default channel to reduce spam in topic channels *)
224+
match List.exists n.branches ~f:(fun { name } -> String.equal name main_branch_name) with
225+
| false -> default ()
190226
| true ->
191-
log#info "main branch merge, ignoring status event %s: %s" n.context (first_line commit.commit.message);
192-
Lwt.return []
193-
| false -> Lwt.return (partition_commit cfg commit.files)
227+
( match%lwt Github.generate_query_commmit cfg ~url:n.commit.url ~sha:n.commit.sha with
228+
| None -> default ()
229+
| Some commit ->
230+
(*
231+
match
232+
List.exists n.branches ~f:(fun { name } -> is_main_merge_message ~msg:commit.commit.message ~branch:name cfg)
233+
with
234+
| true ->
235+
log#info "main branch merge, ignoring status event %s: %s" n.context (first_line commit.commit.message);
236+
Lwt.return []
237+
| false ->
238+
*)
239+
Lwt.return (partition_commit cfg commit.files)
240+
)
194241
in
195242
let res =
196-
match List.exists cfg.status_rules.status ~f:(Poly.equal n.state) with
243+
match
244+
List.exists cfg.status_rules.status ~f:(fun x ->
245+
match x with
246+
| State s -> Poly.equal s n.state
247+
| HideConsecutiveSuccess -> Poly.equal Success n.state
248+
| _ -> false)
249+
with
197250
| false -> Lwt.return []
198251
| true ->
199252
match List.exists ~f:id [ hide_cancelled n cfg; hide_success n ctx ] with
@@ -210,17 +263,17 @@ let partition_status (ctx : Context.t) (n : status_notification) =
210263
res
211264

212265
let partition_commit_comment cfg n =
213-
let default = Option.value_map cfg.prefix_rules.default ~default:[] ~f:(fun webhook -> [ webhook ]) in
266+
let default = Option.to_list cfg.prefix_rules.default in
214267
match n.comment.path with
215268
| None ->
216269
( match%lwt Github.generate_commit_from_commit_comment cfg n with
217270
| None -> Lwt.return default
218271
| Some commit -> Lwt.return (partition_commit cfg commit.files)
219272
)
220273
| Some p ->
221-
match filter_commit cfg.prefix_rules.rules p with
222-
| [] -> Lwt.return default
223-
| l -> Lwt.return l
274+
match chan_of_file cfg.prefix_rules.rules p with
275+
| None -> Lwt.return default
276+
| Some chan -> Lwt.return [ chan ]
224277

225278
let generate_notifications (ctx : Context.t) req =
226279
let cfg = ctx.cfg in
@@ -248,7 +301,7 @@ let generate_notifications (ctx : Context.t) req =
248301
Lwt.return notifs
249302
| Status n ->
250303
let%lwt webhooks = partition_status ctx n in
251-
let notifs = List.map ~f:(fun webhook -> webhook, generate_status_notification n) webhooks in
304+
let notifs = List.map ~f:(fun webhook -> webhook, generate_status_notification cfg n) webhooks in
252305
Lwt.return notifs
253306
| _ -> Lwt.return []
254307

lib/common.ml

+12
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,15 @@ let first_line s =
44
match String.split ~on:'\n' s with
55
| x :: _ -> x
66
| [] -> s
7+
8+
module Tristate : Atdgen_runtime.Json_adapter.S = struct
9+
let normalize = function
10+
| `Bool true -> `String "true"
11+
| `Bool false -> `String "false"
12+
| x -> x
13+
14+
let restore = function
15+
| `String "true" -> `Bool true
16+
| `String "false" -> `Bool false
17+
| x -> x
18+
end

lib/config.ml

+18-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
open Devkit
22
module Chan_map = Map.Make (String)
33

4+
type config_status_state =
5+
| State of Github_t.status_state
6+
| Cancelled of string
7+
| HideConsecutiveSuccess
8+
49
type status_rules = {
510
title : string list option;
6-
status : Github_t.status_state list;
11+
status : config_status_state list;
712
}
813

914
type t = {
@@ -15,7 +20,6 @@ type t = {
1520
gh_token : string option;
1621
offline : string option;
1722
status_rules : status_rules;
18-
suppress_cancelled_events : bool;
1923
}
2024

2125
let make (json_config : Notabot_t.config) (secrets : Notabot_t.secrets) =
@@ -65,15 +69,22 @@ let make (json_config : Notabot_t.config) (secrets : Notabot_t.secrets) =
6569
let j = json_config.status_rules.status in
6670
List.filter_map id
6771
[
68-
(if j.success then Some Success else None);
69-
(if j.failure then Some Failure else None);
70-
(if j.pending then Some Pending else None);
71-
(if j.error then Some Error else None);
72+
( match j.success with
73+
| False -> None
74+
| True -> Some (State Success)
75+
| Once -> Some HideConsecutiveSuccess
76+
);
77+
(if j.failure then Some (State Failure) else None);
78+
(if j.pending then Some (State Pending) else None);
79+
(if j.error then Some (State Error) else None);
80+
( match j.cancelled with
81+
| Some r -> Some (Cancelled r)
82+
| None -> None
83+
);
7284
]
7385
in
7486
{ title = json_config.status_rules.title; status }
7587
in
76-
let suppress_cancelled_events = Option.default true json_config.suppress_cancelled_events in
7788
{
7889
chans;
7990
prefix_rules = json_config.prefix_rules;
@@ -83,7 +94,6 @@ let make (json_config : Notabot_t.config) (secrets : Notabot_t.secrets) =
8394
gh_token = secrets.gh_token;
8495
offline = json_config.offline;
8596
status_rules;
86-
suppress_cancelled_events;
8797
}
8898

8999
let load_config_file ~config_path = Notabot_j.config_of_string @@ Stdio.In_channel.read_all config_path

lib/dune

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
(library
22
(name lib)
3-
(libraries curl curl.lwt nocrypto hex atdgen base stdio lwt lwt.unix uri
4-
devkit devkit.core omd base64)
3+
(libraries atdgen atdgen-runtime base base.caml base64 biniou cstruct curl
4+
curl.lwt devkit devkit.core hex lwt lwt.unix nocrypto omd re stdio uri
5+
yojson)
56
(preprocess
67
(pps lwt_ppx)))
78

@@ -15,7 +16,7 @@
1516
(targets github_j.ml github_j.mli)
1617
(deps github.atd)
1718
(action
18-
(run atdgen -j %{deps})))
19+
(run atdgen -j -j-std %{deps})))
1920

2021
(rule
2122
(targets slack_t.ml slack_t.mli)
@@ -27,7 +28,7 @@
2728
(targets slack_j.ml slack_j.mli)
2829
(deps slack.atd)
2930
(action
30-
(run atdgen -j %{deps})))
31+
(run atdgen -j -j-std %{deps})))
3132

3233
(rule
3334
(targets notabot_t.ml notabot_t.mli)
@@ -39,4 +40,4 @@
3940
(targets notabot_j.ml notabot_j.mli)
4041
(deps notabot.atd)
4142
(action
42-
(run atdgen -j %{deps})))
43+
(run atdgen -j -j-std %{deps})))

0 commit comments

Comments
 (0)