Skip to content

Commit 9e17e7d

Browse files
committed
map repo urls to repo-specific state with a hash table
Repo state is namespaced by the full url of the repo, so that identifiers are unique across platforms (github.com and GH enterprise).
1 parent 66a3930 commit 9e17e7d

File tree

6 files changed

+81
-32
lines changed

6 files changed

+81
-32
lines changed

lib/action.ml

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,14 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
8787
if List.is_empty matched_channel_names then default else matched_channel_names
8888

8989
let partition_status (ctx : Context.t) (n : status_notification) =
90-
let cfg = State.find_repo_config_exn ctx.state in
90+
let repo = n.repository in
91+
let cfg = State.find_repo_config_exn ctx.state repo.url in
9192
let pipeline = n.context in
9293
let current_status = n.state in
9394
let rules = cfg.status_rules.rules in
9495
let action_on_match (branches : branch list) =
9596
let default = Option.to_list cfg.prefix_rules.default_channel in
96-
let () = State.set_repo_pipeline_status ~pipeline ~branches ~status:current_status ctx.state in
97+
State.set_repo_pipeline_status ctx.state repo.url ~pipeline ~branches ~status:current_status;
9798
match List.is_empty branches with
9899
| true -> Lwt.return []
99100
| false ->
@@ -105,18 +106,18 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
105106
| false -> Lwt.return default
106107
| true ->
107108
let sha = n.commit.sha in
108-
let repo = n.repository in
109109
( match%lwt Github_api.get_api_commit ~ctx ~repo ~sha with
110110
| Error e -> action_error e
111111
| Ok commit -> Lwt.return @@ partition_commit cfg commit.files
112112
)
113113
in
114-
if State.is_pipeline_allowed ctx.state ~pipeline then begin
114+
if State.is_pipeline_allowed ctx.state repo.url ~pipeline then begin
115+
let repo_state = State.find_repo_exn ctx.state repo.url in
115116
match Rule.Status.match_rules ~rules n with
116117
| Some Ignore | None -> Lwt.return []
117118
| Some Allow -> action_on_match n.branches
118119
| Some Allow_once ->
119-
match Map.find ctx.state.pipeline_statuses pipeline with
120+
match Map.find repo_state.pipeline_statuses pipeline with
120121
| Some branch_statuses ->
121122
let has_same_status_state_as_prev (branch : branch) =
122123
match Map.find branch_statuses branch.name with
@@ -130,7 +131,7 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
130131
else Lwt.return []
131132

132133
let partition_commit_comment (ctx : Context.t) n =
133-
let cfg = State.find_repo_config_exn ctx.state in
134+
let cfg = State.find_repo_config_exn ctx.state n.repository.url in
134135
match n.comment.commit_id with
135136
| None -> action_error "unable to find commit id for this commit comment event"
136137
| Some sha ->
@@ -149,7 +150,8 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
149150
)
150151

151152
let generate_notifications (ctx : Context.t) req =
152-
let cfg = State.find_repo_config_exn ctx.state in
153+
let repo = Github.repo_of_notification req in
154+
let cfg = State.find_repo_config_exn ctx.state repo.url in
153155
match req with
154156
| Github.Push n ->
155157
partition_push cfg n |> List.map ~f:(fun (channel, n) -> generate_push_notification n channel) |> Lwt.return
@@ -178,20 +180,21 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
178180
in
179181
Lwt_list.iter_s notify notifications
180182

181-
(** `refresh_config_of_context ctx n` updates the current context if the configuration
182-
hasn't been loaded yet, or if the incoming request `n` is a push
183+
(** `refresh_repo_config ctx n` fetches the latest repo config if it's
184+
uninitialized in state, or if the incoming request `n` is a push
183185
notification containing commits that touched the config file. *)
184-
let refresh_config_of_context (ctx : Context.t) notification =
186+
let refresh_repo_config (ctx : Context.t) notification =
187+
let repo = Github.repo_of_notification notification in
185188
let fetch_config () =
186-
let repo = Github.repo_of_notification notification in
187189
match%lwt Github_api.get_config ~ctx ~repo with
188190
| Ok config ->
189-
ctx.state.config <- Some config;
190-
Context.print_config ctx;
191+
State.set_repo_config ctx.state ~repo_url:repo.url ~config;
192+
Context.print_config ctx repo.url;
191193
Lwt.return @@ Ok ()
192194
| Error e -> action_error e
193195
in
194-
match ctx.state.config with
196+
let repo_state = State.find_or_add_repo ctx.state repo.url in
197+
match repo_state.config with
195198
| None -> fetch_config ()
196199
| Some _ ->
197200
match notification with
@@ -208,7 +211,7 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
208211
match Github.parse_exn ~secret:secrets.gh_hook_token headers body with
209212
| exception exn -> Exn_lwt.fail ~exn "failed to parse payload"
210213
| payload ->
211-
( match%lwt refresh_config_of_context ctx payload with
214+
( match%lwt refresh_repo_config ctx payload with
212215
| Error e -> action_error e
213216
| Ok () ->
214217
let%lwt notifications = generate_notifications ctx payload in
@@ -232,6 +235,9 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
232235
| Context.Context_error msg ->
233236
log#error "%s" msg;
234237
Lwt.return_unit
238+
| State.State_error msg ->
239+
log#error "%s" msg;
240+
Lwt.return_unit
235241

236242
let process_link_shared_event (ctx : Context.t) (event : Slack_t.link_shared_event) =
237243
let process link =

lib/common.atd

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
type 'v map_as_object =
22
(string * 'v) list <json repr="object">
33
wrap <ocaml module="Common.StringMap" t="'v Common.StringMap.t">
4+
5+
type 'v table_as_object =
6+
(string * 'v) list <json repr="object">
7+
wrap <ocaml module="Common.Table" t="'v Common.Table.t">

lib/common.ml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@ module StringMap = struct
1515
let unwrap = to_list
1616
end
1717

18+
module Table = struct
19+
type 'a t = (string, 'a) Hashtbl.t
20+
21+
let empty () = Hashtbl.create (module String)
22+
23+
let to_list (l : 'a t) : (string * 'a) list = Hashtbl.to_alist l
24+
25+
let of_list (m : (string * 'a) list) : 'a t = Hashtbl.of_alist_exn (module String) m
26+
27+
let wrap = of_list
28+
29+
let unwrap = to_list
30+
end
31+
1832
let fmt_error fmt = Printf.ksprintf (fun s -> Error s) fmt
1933

2034
let first_line s =

lib/context.ml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ let refresh_state ctx =
7070
end
7171
else Ok ctx
7272

73-
let print_config ctx =
74-
let cfg = State.find_repo_config_exn ctx.state in
73+
let print_config ctx repo_url =
74+
let cfg = State.find_repo_config_exn ctx.state repo_url in
7575
let secrets = get_secrets_exn ctx in
7676
log#info "using prefix routing:";
7777
Rule.Prefix.print_prefix_routing cfg.prefix_rules.rules;

lib/state.atd

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
type status_state <ocaml from="Github"> = abstract
22
type config <ocaml from="Config"> = abstract
33
type 'v map_as_object <ocaml from="Common"> = abstract
4+
type 'v table_as_object <ocaml from="Common"> = abstract
45

56
(* A map from branch names to build statuses *)
67
type branch_statuses = status_state map_as_object
@@ -10,8 +11,13 @@ type branch_statuses = status_state map_as_object
1011
branch *)
1112
type pipeline_statuses = branch_statuses map_as_object
1213

13-
(* The serializable runtime state of the bot *)
14-
type state = {
14+
(* The runtime state of a given GitHub repository *)
15+
type repo_state = {
1516
?config <ocaml mutable>: config option;
1617
pipeline_statuses <ocaml mutable>: pipeline_statuses
17-
}
18+
}
19+
20+
(* The serializable runtime state of the bot *)
21+
type state = {
22+
~repos <ocaml default="Common.Table.empty ()"> : repo_state table_as_object
23+
}

lib/state.ml

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,45 @@ exception State_error of string
66

77
let state_error fmt = Printf.ksprintf (fun msg -> raise (State_error msg)) fmt
88

9-
let empty () : State_t.state = { pipeline_statuses = StringMap.empty; config = None }
9+
let empty_repo_state () : State_t.repo_state = { pipeline_statuses = StringMap.empty; config = None }
1010

11-
let find_repo_config_exn (state : State_t.state) =
12-
match state.config with
13-
| None -> state_error "config is uninitialized"
11+
let empty () : State_t.state = { repos = Table.empty () }
12+
13+
let find_or_add_repo (state : State_t.state) repo_url =
14+
Hashtbl.find_or_add state.repos repo_url ~default:empty_repo_state
15+
16+
let find_repo_exn (state : State_t.state) repo_url =
17+
match Hashtbl.find state.repos repo_url with
18+
| None -> state_error "state uninitialized for repo %s" repo_url
19+
| Some repo_state -> repo_state
20+
21+
let find_repo_config_exn state repo_url =
22+
match (find_repo_exn state repo_url).config with
23+
| None -> state_error "config uninitialized for repo %s" repo_url
1424
| Some config -> config
1525

16-
let set_repo_pipeline_status (state : State_t.state) ~pipeline ~(branches : Github_t.branch list) ~status =
26+
let set_repo_config (state : State_t.state) ~repo_url ~config =
27+
match Hashtbl.find state.repos repo_url with
28+
| None -> state_error "state uninitialized for repo %s" repo_url
29+
| Some repo_state -> repo_state.config <- Some config
30+
31+
let set_repo_pipeline_status (state : State_t.state) repo_url ~pipeline ~(branches : Github_t.branch list) ~status =
1732
let set_branch_status branch_statuses =
1833
let new_statuses = List.map branches ~f:(fun b -> b.name, status) in
1934
let init = Option.value branch_statuses ~default:(Map.empty (module String)) in
2035
List.fold_left new_statuses ~init ~f:(fun m (key, data) -> Map.set m ~key ~data)
2136
in
22-
state.pipeline_statuses <- Map.update state.pipeline_statuses pipeline ~f:set_branch_status
23-
24-
(** `is_pipeline_allowed s p` returns `true` if status_rules of state
25-
`s`'s config doesn't define a whitelist of allowed pipelines, or
26-
if the list contains pipeline `p`; returns `false` otherwise. *)
27-
let is_pipeline_allowed (state : State_t.state) ~pipeline =
28-
match state.config with
37+
match Hashtbl.find state.repos repo_url with
38+
| None -> state_error "state uninitialized for repo %s" repo_url
39+
| Some repo_state ->
40+
repo_state.pipeline_statuses <- Map.update repo_state.pipeline_statuses pipeline ~f:set_branch_status
41+
42+
(** `is_pipeline_allowed s r p` returns `true` if
43+
`status_rules` doesn't define a whitelist of allowed
44+
pipelines in the config of repo `r`, or if the list
45+
contains pipeline `p`; returns `false` otherwise. *)
46+
let is_pipeline_allowed (state : State_t.state) repo_url ~pipeline =
47+
match (find_repo_exn state repo_url).config with
2948
| None -> false
3049
| Some config ->
3150
match config.status_rules.allowed_pipelines with

0 commit comments

Comments
 (0)