Skip to content

Commit 0b3854c

Browse files
committed
introduce new status_rules syntax and update state tracking logic (#91)
This implementation properly tracks different build states per pipeline (`pipeline_statuses` is now a two-level map indexed by pipeline name and branch name). The “last build state” for a given pipeline + branch permutation is only updated if the current build notification passes the defined status_rules. This change also removes the temporary wrapper around config that was needed for the old status_rules syntax.
1 parent fa53571 commit 0b3854c

File tree

14 files changed

+198
-234
lines changed

14 files changed

+198
-234
lines changed

lib/action.ml

Lines changed: 34 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ open Devkit
22
open Base
33
open Slack
44
open Config_t
5-
open Config
65
open Common
76
open Github_j
87

@@ -13,24 +12,14 @@ let action_error msg = raise (Action_error msg)
1312
let log = Log.from "action"
1413

1514
module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
16-
(* this should move to context.ml once rule.ml is refactored to not depend on it *)
17-
let print_config ctx =
18-
let cfg = Context.get_config_exn ctx in
19-
let secrets = Context.get_secrets_exn ctx in
20-
log#info "using prefix routing:";
21-
Rule.Prefix.print_prefix_routing cfg.prefix_rules.rules;
22-
log#info "using label routing:";
23-
Rule.Label.print_label_routing cfg.label_rules.rules;
24-
log#info "signature checking %s" (if Option.is_some secrets.gh_hook_token then "enabled" else "disabled")
25-
26-
let partition_push cfg n =
15+
let partition_push (cfg : Config_t.config) n =
2716
let default = Option.to_list cfg.prefix_rules.default_channel in
2817
let rules = cfg.prefix_rules.rules in
2918
n.commits
3019
|> List.filter ~f:(fun c -> c.distinct)
3120
|> List.filter ~f:(fun c ->
3221
let branch = Github.commits_branch_of_ref n.ref in
33-
let skip = Github.is_main_merge_message ~msg:c.message ~branch cfg in
22+
let skip = Github.is_main_merge_message ~msg:c.message ?main_branch:cfg.main_branch_name ~branch in
3423
if skip then log#info "main branch merge, ignoring %s: %s" c.id (first_line c.message);
3524
not skip)
3625
|> List.concat_map ~f:(fun commit ->
@@ -45,7 +34,7 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
4534
|> Map.map ~f:(fun commits -> { n with commits })
4635
|> Map.to_alist
4736

48-
let partition_label (cfg : Config.t) (labels : label list) =
37+
let partition_label (cfg : Config_t.config) (labels : label list) =
4938
let default = cfg.label_rules.default_channel in
5039
let rules = cfg.label_rules.rules in
5140
labels |> List.concat_map ~f:(Rule.Label.match_rules ~rules) |> List.dedup_and_sort ~compare:String.compare
@@ -87,7 +76,7 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
8776
| Submitted, _, _ -> partition_label cfg n.pull_request.labels
8877
| _ -> []
8978

90-
let partition_commit (cfg : Config.t) files =
79+
let partition_commit (cfg : Config_t.config) files =
9180
let default = Option.to_list cfg.prefix_rules.default_channel in
9281
let rules = cfg.prefix_rules.rules in
9382
let matched_channel_names =
@@ -101,18 +90,18 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
10190
let cfg = Context.get_config_exn ctx in
10291
let pipeline = n.context in
10392
let current_status = n.state in
104-
let updated_at = n.updated_at in
105-
let get_commit_info () =
93+
let rules = cfg.status_rules.rules in
94+
let action_on_match (branches : branch list) =
10695
let default = Option.to_list cfg.prefix_rules.default_channel in
107-
let () = Context.refresh_pipeline_status ~pipeline ~branches:n.branches ~status:current_status ~updated_at ctx in
108-
match List.is_empty n.branches with
96+
let () = Context.refresh_pipeline_status ~pipeline ~branches ~status:current_status ctx in
97+
match List.is_empty branches with
10998
| true -> Lwt.return []
11099
| false ->
111100
match cfg.main_branch_name with
112101
| None -> Lwt.return default
113102
| Some main_branch_name ->
114103
(* non-main branch build notifications go to default channel to reduce spam in topic channels *)
115-
match List.exists n.branches ~f:(fun { name } -> String.equal name main_branch_name) with
104+
match List.exists branches ~f:(fun { name } -> String.equal name main_branch_name) with
116105
| false -> Lwt.return default
117106
| true ->
118107
let sha = n.commit.sha in
@@ -122,27 +111,23 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
122111
| Ok commit -> Lwt.return @@ partition_commit cfg commit.files
123112
)
124113
in
125-
let res =
126-
match
127-
List.exists cfg.status_rules.status ~f:(fun x ->
128-
match x with
129-
| State s -> Poly.equal s n.state
130-
| HideConsecutiveSuccess -> Poly.equal Success n.state
131-
| _ -> false)
132-
with
133-
| false -> Lwt.return []
134-
| true ->
135-
match List.exists ~f:id [ Rule.Status.hide_cancelled n cfg; Rule.Status.hide_success n ctx ] with
136-
| true -> Lwt.return []
137-
| false ->
138-
match cfg.status_rules.title with
139-
| None -> get_commit_info ()
140-
| Some status_filter ->
141-
match List.exists status_filter ~f:(String.equal n.context) with
142-
| false -> Lwt.return []
143-
| true -> get_commit_info ()
144-
in
145-
res
114+
if Context.is_pipeline_allowed ctx ~pipeline then begin
115+
match Rule.Status.match_rules ~rules n with
116+
| Some Ignore | None -> Lwt.return []
117+
| Some Allow -> action_on_match n.branches
118+
| Some Allow_once ->
119+
match Map.find ctx.state.pipeline_statuses pipeline with
120+
| Some branch_statuses ->
121+
let has_same_status_state_as_prev (branch : branch) =
122+
match Map.find branch_statuses branch.name with
123+
| None -> false
124+
| Some state -> Poly.equal state current_status
125+
in
126+
let branches = List.filter n.branches ~f:(Fn.non @@ has_same_status_state_as_prev) in
127+
action_on_match branches
128+
| None -> action_on_match n.branches
129+
end
130+
else Lwt.return []
146131

147132
let partition_commit_comment (ctx : Context.t) n =
148133
let cfg = Context.get_config_exn ctx in
@@ -213,9 +198,8 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
213198
let repo = Github.repo_of_notification notification in
214199
match%lwt Github_api.get_config ~ctx ~repo with
215200
| Ok config ->
216-
print_config ctx;
217-
(* can remove this wrapper once status_rules doesn't depend on Config.t *)
218-
ctx.config <- Some (Config.make config);
201+
Context.print_config ctx;
202+
ctx.config <- Some config;
219203
Lwt.return @@ Ok ()
220204
| Error e -> action_error e
221205
in
@@ -233,7 +217,7 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
233217
let process_github_notification (ctx : Context.t) headers body =
234218
try%lwt
235219
let secrets = Context.get_secrets_exn ctx in
236-
match Github.parse_exn ~secret:secrets.gh_hook_token headers body with
220+
match Github.parse_exn ?hook_token:secrets.gh_hook_token headers body with
237221
| exception exn -> Exn_lwt.fail ~exn "failed to parse payload"
238222
| payload ->
239223
( match%lwt refresh_config_of_context ctx payload with
@@ -243,7 +227,11 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
243227
let%lwt () = send_notifications ctx notifications in
244228
( match ctx.state_filepath with
245229
| None -> Lwt.return_unit
246-
| Some path -> Lwt.return @@ State.save path ctx.state
230+
| Some path ->
231+
( match%lwt State.save ctx.state path with
232+
| Ok () -> Lwt.return_unit
233+
| Error e -> action_error e
234+
)
247235
)
248236
)
249237
with

lib/common.ml

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,6 @@ let first_line s =
2222
| x :: _ -> x
2323
| [] -> s
2424

25-
module Tristate : Atdgen_runtime.Json_adapter.S = struct
26-
let normalize = function
27-
| `Bool true -> `String "true"
28-
| `Bool false -> `String "false"
29-
| x -> x
30-
31-
let restore = function
32-
| `String "true" -> `Bool true
33-
| `String "false" -> `Bool false
34-
| x -> x
35-
end
36-
3725
let decode_string_pad s =
3826
String.rstrip ~drop:(List.mem [ '='; ' '; '\n'; '\r'; '\t' ] ~equal:Char.equal) s |> Base64.decode_string
3927

lib/config.atd

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
type status_state <ocaml from="Rule"> = abstract
1+
type status_rule <ocaml from="Rule"> = abstract
22
type prefix_rule <ocaml from="Rule"> = abstract
33
type label_rule <ocaml from="Rule"> = abstract
44

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

1111
(* This type of rule is used for events that must be routed based on the

lib/config.ml

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

lib/context.ml

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ type t = {
1111
secrets_filepath : string;
1212
state_filepath : string option;
1313
mutable secrets : Config_t.secrets option;
14-
mutable config : Config.t option;
15-
mutable state : State_t.state;
14+
mutable config : Config_t.config option;
15+
state : State_t.state;
1616
}
1717

1818
let default : t =
@@ -46,8 +46,19 @@ let hook_of_channel ctx channel_name =
4646
| Some hook -> Some hook.url
4747
| None -> None
4848

49-
let refresh_pipeline_status ctx ~pipeline ~(branches : Github_t.branch list) ~status ~updated_at =
50-
ctx.state <- State.refresh_pipeline_status ctx.state ~pipeline ~branches ~status ~updated_at
49+
(** `is_pipeline_allowed ctx p` returns `true` if ctx.config.status_rules
50+
doesn't define a whitelist of allowed pipelines, or if the list
51+
contains pipeline `p`; returns `false` otherwise. *)
52+
let is_pipeline_allowed ctx ~pipeline =
53+
match ctx.config with
54+
| None -> false
55+
| Some config ->
56+
match config.status_rules.allowed_pipelines with
57+
| Some allowed_pipelines when not @@ List.exists allowed_pipelines ~f:(String.equal pipeline) -> false
58+
| _ -> true
59+
60+
let refresh_pipeline_status ctx ~pipeline ~(branches : Github_t.branch list) ~status =
61+
if is_pipeline_allowed ctx ~pipeline then State.refresh_pipeline_status ctx.state ~pipeline ~branches ~status else ()
5162

5263
let log = Log.from "context"
5364

@@ -70,3 +81,12 @@ let refresh_state ctx =
7081
let state = State_j.state_of_string file in
7182
Ok { ctx with state }
7283
)
84+
85+
let print_config ctx =
86+
let cfg = get_config_exn ctx in
87+
let secrets = get_secrets_exn ctx in
88+
log#info "using prefix routing:";
89+
Rule.Prefix.print_prefix_routing cfg.prefix_rules.rules;
90+
log#info "using label routing:";
91+
Rule.Label.print_label_routing cfg.label_rules.rules;
92+
log#info "signature checking %s" (if Option.is_some secrets.gh_hook_token then "enabled" else "disabled")

lib/github.ml

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
open Base
2-
open Devkit
32
open Printf
3+
open Common
4+
open Devkit
45
open Github_j
56

67
type t =
@@ -36,44 +37,42 @@ let event_of_filename filename =
3637
| [ kind; _; "json" ] -> Some kind
3738
| _ -> None
3839

39-
let is_main_merge_message ~msg:message ~branch (cfg : Config.t) =
40-
match cfg.main_branch_name with
40+
let is_main_merge_message ~msg:message ?main_branch ~branch =
41+
match main_branch with
4142
| Some main_branch when String.equal branch main_branch ->
4243
(*
4344
handle "Merge <main branch> into <feature branch>" commits when they are merged into main branch
4445
we should have already seen these commits on the feature branch but for some reason they are distinct:true
4546
*)
4647
let prefix = sprintf "Merge branch '%s' into " main_branch in
4748
let prefix2 = sprintf "Merge remote-tracking branch 'origin/%s' into " main_branch in
48-
let title = Common.first_line message in
49+
let title = first_line message in
4950
String.is_prefix title ~prefix || String.is_prefix title ~prefix:prefix2
5051
| Some main_branch ->
5152
let expect = sprintf "Merge branch '%s' into %s" main_branch branch in
5253
let expect2 = sprintf "Merge remote-tracking branch 'origin/%s' into %s" main_branch branch in
53-
let title = Common.first_line message in
54+
let title = first_line message in
5455
String.equal title expect || String.equal title expect2
5556
| _ -> false
5657

5758
let modified_files_of_commit commit = List.concat [ commit.added; commit.removed; commit.modified ]
5859

59-
let is_valid_signature ~secret headers_sig body =
60-
let request_hash =
61-
let key = Cstruct.of_string secret in
62-
Cstruct.to_string @@ Nocrypto.Hash.SHA1.hmac ~key (Cstruct.of_string body)
63-
in
64-
let (`Hex request_hash) = Hex.of_string request_hash in
65-
String.equal headers_sig (sprintf "sha1=%s" request_hash)
60+
let has_valid_signature ~hook_token ~headers ~body =
61+
match List.Assoc.find headers "x-hub-signature" ~equal:String.equal with
62+
| None -> Exn.fail "unable to find header x-hub-signature"
63+
| Some signature ->
64+
let key = Cstruct.of_string hook_token in
65+
let request_hash = Cstruct.to_string @@ Nocrypto.Hash.SHA1.hmac ~key (Cstruct.of_string body) in
66+
let (`Hex request_hash) = Hex.of_string request_hash in
67+
String.equal signature (sprintf "sha1=%s" request_hash)
6668

6769
(* Parse a payload. The type of the payload is detected from the headers. *)
68-
let parse_exn ~secret headers body =
69-
begin
70-
match secret with
71-
| None -> ()
72-
| Some secret ->
73-
match List.Assoc.find headers "x-hub-signature" ~equal:String.equal with
74-
| None -> Exn.fail "unable to find header x-hub-signature"
75-
| Some req_sig -> if not @@ is_valid_signature ~secret req_sig body then failwith "request signature invalid"
76-
end;
70+
let parse_exn ?hook_token headers body =
71+
match
72+
Option.value_map hook_token ~default:true ~f:(fun hook_token -> has_valid_signature ~hook_token ~headers ~body)
73+
with
74+
| false -> failwith "request signature invalid"
75+
| true ->
7776
match List.Assoc.find_exn headers "x-github-event" ~equal:String.equal with
7877
| exception exn -> Exn.fail ~exn "unable to read x-github-event"
7978
| "push" -> Push (commit_pushed_notification_of_string body)

0 commit comments

Comments
 (0)