Skip to content

Commit 49c775c

Browse files
committed
rename/move helper functions out of github.ml, action.ml, and request_handler.ml
all rule checking logic moved to rule.ml decode_string_pad moved to common.ml mock.ml contents moved to common.ml process_github_notification moved to request_handler.ml
1 parent 1365d9a commit 49c775c

File tree

10 files changed

+428
-413
lines changed

10 files changed

+428
-413
lines changed

lib/action.ml

Lines changed: 207 additions & 313 deletions
Large diffs are not rendered by default.

lib/common.ml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,14 @@ module Tristate : Atdgen_runtime.Json_adapter.S = struct
1616
| `String "false" -> `Bool false
1717
| x -> x
1818
end
19+
20+
let decode_string_pad s =
21+
let rec strip_padding i =
22+
if i < 0 then ""
23+
else (
24+
match s.[i] with
25+
| '=' | '\n' | '\r' | '\t' | ' ' -> strip_padding (i - 1)
26+
| _ -> String.sub s ~pos:0 ~len:(i + 1)
27+
)
28+
in
29+
Base64.decode_string @@ strip_padding (String.length s - 1)

lib/config.atd

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@ type label_rule <ocaml from="Rule"> = abstract
44

55
(* This type of rule is used for CI build notifications. *)
66
type status_rules = {
7-
?allowed_pipelines: string list option; (* keep only status events with a title matching this list *)
7+
?allowed_pipelines : string list option; (* keep only status events with a title matching this list *)
88
rules: status_state;
99
}
1010

1111
(* This type of rule is used for CI build notifications. *)
1212
type prefix_rules = {
13-
?default_channel: string nullable; (* if none of the rules is matching *)
13+
?default_channel : string nullable; (* if none of the rules is matching *)
1414
rules: prefix_rule list;
1515
}
1616

1717
(* This type of rule is used for events that must be routed based on the
1818
files they are related to. *)
1919
type label_rules = {
20-
?default_channel: string nullable; (* if none of the rules is matching *)
20+
?default_channel : string nullable; (* if none of the rules is matching *)
2121
rules: label_rule list;
2222
}
2323

@@ -41,6 +41,6 @@ type webhook = {
4141
shouldn't be checked into version control. *)
4242
type secrets = {
4343
slack_hooks : webhook list;
44-
?gh_token: string option; (* must not be specified for public repositories *)
45-
?gh_webhook_secret: string option; (* if not specified - signatures will not be checked *)
44+
?gh_token : string option; (* must not be specified for public repositories *)
45+
?gh_webhook_secret : string option; (* if not specified - signatures will not be checked *)
4646
}

lib/github.ml

Lines changed: 61 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ open Devkit
33
open Printf
44
open Github_j
55

6+
exception Remote_config_error of string
7+
8+
let remote_config_error fmt = ksprintf (fun s -> raise (Remote_config_error s)) fmt
9+
610
let log = Log.from "github"
711

812
type t =
@@ -14,14 +18,9 @@ type t =
1418
| Issue_comment of issue_comment_notification
1519
| Commit_comment of commit_comment_notification
1620
| Status of status_notification
21+
(* all other events *)
1722
| Event of string
1823

19-
(* all other events *)
20-
21-
exception Remote_config_error of string
22-
23-
let remote_config_error fmt = ksprintf (fun s -> raise (Remote_config_error s)) fmt
24-
2524
let to_repo = function
2625
| Push n -> Some n.repository
2726
| Pull_request n -> Some n.repository
@@ -33,6 +32,16 @@ let to_repo = function
3332
| Status n -> Some n.repository
3433
| Event _ -> None
3534

35+
let commits_branch_of_ref ref =
36+
match String.split ~on:'/' ref with
37+
| "refs" :: "heads" :: l -> String.concat ~sep:"/" l
38+
| _ -> ref
39+
40+
let event_of_filename filename =
41+
match String.split_on_chars filename ~on:[ '.' ] with
42+
| [ kind; _name; ext ] when String.equal ext "json" -> Some kind
43+
| _ -> None
44+
3645
let api_url_of_repo (repo : repository) =
3746
Option.map ~f:(fun host ->
3847
match host with
@@ -74,23 +83,12 @@ let get_remote_config_json_url filename ?token req =
7483
end
7584

7685
let config_of_content_api_response response =
77-
let decode_string_pad s =
78-
let rec strip_padding i =
79-
if i < 0 then ""
80-
else (
81-
match s.[i] with
82-
| '=' | '\n' | '\r' | '\t' | ' ' -> strip_padding (i - 1)
83-
| _ -> String.sub s ~pos:0 ~len:(i + 1)
84-
)
85-
in
86-
Base64.decode_string @@ strip_padding (String.length s - 1)
87-
in
8886
try%lwt
8987
match response.encoding with
9088
| "base64" ->
9189
Lwt.return
9290
@@ Config_j.config_of_string
93-
@@ decode_string_pad
91+
@@ Common.decode_string_pad
9492
@@ String.concat
9593
@@ String.split_lines
9694
@@ response.content
@@ -99,11 +97,23 @@ let config_of_content_api_response response =
9997
| Base64.Invalid_char -> remote_config_error "unable to decode configuration file from base64"
10098
| Yojson.Json_error msg -> remote_config_error "unable to parse configuration file as valid JSON (%s)" msg
10199

102-
let load_config_json url =
103-
let headers = [ "Accept: application/vnd.github.v3+json" ] in
104-
match%lwt Web.http_request_lwt ~headers `GET url with
105-
| `Error e -> remote_config_error "error while querying github api %s: %s" url e
106-
| `Ok s -> config_of_content_api_response @@ Github_j.content_api_response_of_string s
100+
let is_main_merge_message ~msg:message ~branch (cfg : Config.t) =
101+
match cfg.main_branch_name with
102+
| Some main_branch when String.equal branch main_branch ->
103+
(*
104+
handle "Merge <main branch> into <feature branch>" commits when they are merged into main branch
105+
we should have already seen these commits on the feature branch but for some reason they are distinct:true
106+
*)
107+
let prefix = sprintf "Merge branch '%s' into " main_branch in
108+
let prefix2 = sprintf "Merge remote-tracking branch 'origin/%s' into " main_branch in
109+
let title = Common.first_line message in
110+
String.is_prefix title ~prefix || String.is_prefix title ~prefix:prefix2
111+
| Some main_branch ->
112+
let expect = sprintf "Merge branch '%s' into %s" main_branch branch in
113+
let expect2 = sprintf "Merge remote-tracking branch 'origin/%s' into %s" main_branch branch in
114+
let title = Common.first_line message in
115+
String.equal title expect || String.equal title expect2
116+
| _ -> false
107117

108118
let is_valid_signature ~secret headers_sig body =
109119
let request_hash =
@@ -113,33 +123,11 @@ let is_valid_signature ~secret headers_sig body =
113123
let (`Hex request_hash) = Hex.of_string request_hash in
114124
String.equal headers_sig (sprintf "sha1=%s" request_hash)
115125

116-
(* Parse a payload. The type of the payload is detected from the headers. *)
117-
let parse_exn ~secret headers body =
118-
begin
119-
match secret with
120-
| None -> ()
121-
| Some secret ->
122-
match List.Assoc.find headers "x-hub-signature" ~equal:String.equal with
123-
| None -> Exn.fail "unable to find header x-hub-signature"
124-
| Some req_sig -> if not @@ is_valid_signature ~secret req_sig body then failwith "request signature invalid"
125-
end;
126-
match List.Assoc.find_exn headers "x-github-event" ~equal:String.equal with
127-
| exception exn -> Exn.fail ~exn "unable to read x-github-event"
128-
| "push" -> Push (commit_pushed_notification_of_string body)
129-
| "pull_request" -> Pull_request (pr_notification_of_string body)
130-
| "pull_request_review" -> PR_review (pr_review_notification_of_string body)
131-
| "pull_request_review_comment" -> PR_review_comment (pr_review_comment_notification_of_string body)
132-
| "issues" -> Issue (issue_notification_of_string body)
133-
| "issue_comment" -> Issue_comment (issue_comment_notification_of_string body)
134-
| "status" -> Status (status_notification_of_string body)
135-
| "commit_comment" -> Commit_comment (commit_comment_notification_of_string body)
136-
| ("member" | "create" | "delete" | "release") as event -> Event event
137-
| event -> failwith @@ sprintf "unsupported event : %s" event
138-
139-
let get_commits_branch ref =
140-
match String.split ~on:'/' ref with
141-
| "refs" :: "heads" :: l -> String.concat ~sep:"/" l
142-
| _ -> ref
126+
let load_config_json url =
127+
let headers = [ "Accept: application/vnd.github.v3+json" ] in
128+
match%lwt Web.http_request_lwt ~headers `GET url with
129+
| `Error e -> remote_config_error "error while querying github api %s: %s" url e
130+
| `Ok s -> config_of_content_api_response @@ Github_j.content_api_response_of_string s
143131

144132
let query_api ?token ~url parse =
145133
let headers = Option.map token ~f:(fun t -> [ sprintf "Authorization: token %s" t ]) in
@@ -187,3 +175,26 @@ let generate_commit_from_commit_comment cfg n =
187175
let commit_url = String.sub ~pos:0 ~len:url_length url ^ "/" ^ sha in
188176
(* add sha hash to get the full api link *)
189177
generate_query_commit cfg ~url:commit_url ~sha
178+
179+
(* Parse a payload. The type of the payload is detected from the headers. *)
180+
let parse_exn ~secret headers body =
181+
begin
182+
match secret with
183+
| None -> ()
184+
| Some secret ->
185+
match List.Assoc.find headers "x-hub-signature" ~equal:String.equal with
186+
| None -> Exn.fail "unable to find header x-hub-signature"
187+
| Some req_sig -> if not @@ is_valid_signature ~secret req_sig body then failwith "request signature invalid"
188+
end;
189+
match List.Assoc.find_exn headers "x-github-event" ~equal:String.equal with
190+
| exception exn -> Exn.fail ~exn "unable to read x-github-event"
191+
| "push" -> Push (commit_pushed_notification_of_string body)
192+
| "pull_request" -> Pull_request (pr_notification_of_string body)
193+
| "pull_request_review" -> PR_review (pr_review_notification_of_string body)
194+
| "pull_request_review_comment" -> PR_review_comment (pr_review_comment_notification_of_string body)
195+
| "issues" -> Issue (issue_notification_of_string body)
196+
| "issue_comment" -> Issue_comment (issue_comment_notification_of_string body)
197+
| "status" -> Status (status_notification_of_string body)
198+
| "commit_comment" -> Commit_comment (commit_comment_notification_of_string body)
199+
| ("member" | "create" | "delete" | "release") as event -> Event event
200+
| event -> failwith @@ sprintf "unsupported event : %s" event

lib/mock.ml

Lines changed: 0 additions & 7 deletions
This file was deleted.

lib/rule.ml

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
open Base
2+
open Rule_t
3+
open Github_t
4+
5+
module Status = struct
6+
let hide_cancelled (notification : status_notification) (cfg : Config.t) =
7+
let find_cancelled status_state =
8+
match status_state with
9+
| Config.Cancelled r -> Some r
10+
| _ -> None
11+
in
12+
let regexp_opt = List.find_map cfg.status_rules.status ~f:find_cancelled in
13+
match regexp_opt with
14+
| None -> false
15+
| Some regexp ->
16+
let { state; description; _ } = notification in
17+
let r = Re.Str.regexp_case_fold regexp in
18+
( match description, state with
19+
| Some s, Failure when Re.Str.string_match r s 0 -> true
20+
| _ -> false
21+
)
22+
23+
let hide_success (n : status_notification) (ctx : Context.t) =
24+
match List.exists ctx.cfg.status_rules.status ~f:(Poly.equal Config.HideConsecutiveSuccess) with
25+
| false -> false
26+
| true ->
27+
match n.state with
28+
| Success ->
29+
List.exists
30+
~f:(fun b ->
31+
match State.get_branch_state b.name ctx.state with
32+
| None | Some { last_build_state = Failure; _ } -> false
33+
| Some { last_build_state = Success; _ } -> true)
34+
n.branches
35+
| _ -> false
36+
end
37+
38+
module Prefix = struct
39+
type prefix_match =
40+
| Match of int
41+
| NoMatch
42+
43+
let chan_of_prefix_rule (r : prefix_rule) = r.channel_name
44+
45+
let touching_prefix (rule : prefix_rule) name =
46+
let match_lengths filename prefixes =
47+
List.filter_map
48+
~f:(fun prefix -> if String.is_prefix filename ~prefix then Some (String.length prefix) else None)
49+
prefixes
50+
in
51+
match match_lengths name rule.ignore with
52+
| _ :: _ -> NoMatch
53+
| [] ->
54+
match rule.allow with
55+
| [] -> Match 0
56+
| _ ->
57+
match List.max_elt (match_lengths name rule.allow) ~compare:Int.compare with
58+
| Some x -> Match x
59+
| None -> NoMatch
60+
61+
let longest_touching_prefix_rule rules name =
62+
let get_m rule = touching_prefix rule name in
63+
let reduce_to_longest_match longest_rule_match_pair current_rule =
64+
let _, longest_match = longest_rule_match_pair in
65+
let current_match = get_m current_rule in
66+
let current_rule_match_pair = current_rule, current_match in
67+
match longest_match with
68+
| NoMatch -> current_rule_match_pair
69+
| Match x ->
70+
match current_match with
71+
| NoMatch -> longest_rule_match_pair
72+
| Match y -> if y > x then current_rule_match_pair else longest_rule_match_pair
73+
in
74+
match rules with
75+
| [] -> None
76+
| (x : prefix_rule) :: xs ->
77+
match List.fold_left xs ~init:(x, get_m x) ~f:reduce_to_longest_match with
78+
| _, NoMatch -> None
79+
| r, Match _ -> Some r
80+
81+
let chan_of_file rules file = Option.map ~f:chan_of_prefix_rule @@ longest_touching_prefix_rule rules file
82+
83+
let unique_chans_of_files rules files =
84+
List.dedup_and_sort ~compare:String.compare @@ List.filter_map files ~f:(chan_of_file rules)
85+
86+
let filter_push rules (commit : Github_t.commit) =
87+
let files = List.concat [ commit.added; commit.removed; commit.modified ] in
88+
List.map ~f:(fun chan -> chan, commit) @@ unique_chans_of_files rules files
89+
90+
let print_prefix_routing rules =
91+
let show_match l = String.concat ~sep:" or " @@ List.map ~f:(fun s -> s ^ "*") l in
92+
rules
93+
|> List.iter ~f:(fun (rule : prefix_rule) ->
94+
begin
95+
match rule.allow, rule.ignore with
96+
| [], [] -> Stdio.printf " any"
97+
| l, [] -> Stdio.printf " %s" (show_match l)
98+
| [], l -> Stdio.printf " not %s" (show_match l)
99+
| l, i -> Stdio.printf " %s and not %s" (show_match l) (show_match i)
100+
end;
101+
Stdio.printf " -> #%s\n%!" rule.channel_name)
102+
end
103+
104+
module Label = struct
105+
let touching_label rule name =
106+
let name_lc = String.lowercase name in
107+
let label_lc = List.map rule.allow ~f:(fun l -> String.lowercase l) in
108+
let ignore_lc = List.map rule.ignore ~f:(fun l -> String.lowercase l) in
109+
(* convert both labels and config into lowe-case to make label matching case-insensitive *)
110+
(List.is_empty label_lc || List.mem ~equal:String.equal label_lc name_lc)
111+
&& not (List.mem ~equal:String.equal ignore_lc name_lc)
112+
113+
let filter_label rules (label : Github_j.label) =
114+
rules
115+
|> List.filter_map ~f:(fun rule ->
116+
match touching_label rule label.name with
117+
| false -> None
118+
| true -> Some rule.channel_name)
119+
120+
let print_label_routing rules =
121+
let show_match l = String.concat ~sep:" or " l in
122+
rules
123+
|> List.iter ~f:(fun (rule : label_rule) ->
124+
begin
125+
match rule.allow, rule.ignore with
126+
| [], [] -> Stdio.printf " any"
127+
| l, [] -> Stdio.printf " %s" (show_match l)
128+
| [], l -> Stdio.printf " not %s" (show_match l)
129+
| l, i -> Stdio.printf " %s and not %s" (show_match l) (show_match i)
130+
end;
131+
Stdio.printf " -> #%s\n%!" rule.channel_name)
132+
end

lib/slack.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ let git_short_sha_hash hash = String.sub ~pos:0 ~len:8 hash
223223

224224
let generate_push_notification notification =
225225
let { sender; created; deleted; forced; compare; commits; repository; _ } = notification in
226-
let commits_branch = Github.get_commits_branch notification.ref in
226+
let commits_branch = Github.commits_branch_of_ref notification.ref in
227227
let tree_url = String.concat ~sep:"/" [ repository.url; "tree"; Uri.pct_encode commits_branch ] in
228228
let title =
229229
if deleted then

src/notabot.ml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
open Devkit
22
open Base
33
open Lib
4+
open Action
45
module Arg = Caml.Arg
56
open Cmdliner
67

@@ -10,9 +11,9 @@ let log = Log.from "monorobot"
1011

1112
let cfg_action_after_refresh (cfg : Config.t) =
1213
log#info "using prefix routing:";
13-
Action.print_prefix_routing cfg.prefix_rules.rules;
14+
Rule.Prefix.print_prefix_routing cfg.prefix_rules.rules;
1415
log#info "using label routing:";
15-
Action.print_label_routing cfg.label_rules.rules;
16+
Rule.Label.print_label_routing cfg.label_rules.rules;
1617
log#info "signature checking %s" (if Option.is_some cfg.gh_webhook_secret then "enabled" else "disabled")
1718

1819
let update_state_at_path state_path state event = State.save state_path @@ State.update_state state event
@@ -36,7 +37,8 @@ let check_common file print config secrets state_path =
3637
Context.make_thunk ~state_path ~cfg_path_or_remote_filename:config ~secrets_path:secrets ~cfg_action_after_refresh
3738
()
3839
in
39-
match Mock.kind file with
40+
let filename = Caml.Filename.basename file in
41+
match Github.event_of_filename filename with
4042
| None ->
4143
log#error "aborting because payload %s is not named properly, named should be KIND.NAME_OF_PAYLOAD.json" file;
4244
Lwt.return_unit

0 commit comments

Comments
 (0)