Skip to content

Commit 914406b

Browse files
committed
make state a hash table mapping repo urls to repo-specific state
Repo state is namespaced by the full url of the repo, so that so that identifiers are unique across platforms (github.com and GH Enterprise).
1 parent b9a0cd9 commit 914406b

File tree

5 files changed

+76
-33
lines changed

5 files changed

+76
-33
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.get_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.get_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.get_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 (webhook, n) -> webhook, generate_push_notification n) |> Lwt.return
@@ -190,20 +192,21 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
190192
in
191193
Lwt_list.iter_s notify notifications
192194

193-
(** `refresh_config_of_context ctx n` updates the current context if the configuration
194-
hasn't been loaded yet, or if the incoming request `n` is a push
195+
(** `refresh_repo_config ctx n` fetches the latest repo config if it's
196+
uninitialized in state, or if the incoming request `n` is a push
195197
notification containing commits that touched the config file. *)
196-
let refresh_config_of_context (ctx : Context.t) notification =
198+
let refresh_repo_config (ctx : Context.t) notification =
199+
let repo = Github.repo_of_notification notification in
197200
let fetch_config () =
198-
let repo = Github.repo_of_notification notification in
199201
match%lwt Github_api.get_config ~ctx ~repo with
200202
| Ok config ->
201-
ctx.state.config <- Some config;
202-
Context.print_config ctx;
203+
State.set_repo_config ctx.state ~repo_url:repo.url ~config;
204+
Context.print_config ctx repo.url;
203205
Lwt.return @@ Ok ()
204206
| Error e -> action_error e
205207
in
206-
match ctx.state.config with
208+
let repo_state = State.find_or_add_repo ctx.state repo.url in
209+
match repo_state.config with
207210
| None -> fetch_config ()
208211
| Some _ ->
209212
match notification with
@@ -220,7 +223,7 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
220223
match Github.parse_exn ~secret:secrets.gh_hook_token headers body with
221224
| exception exn -> Exn_lwt.fail ~exn "failed to parse payload"
222225
| payload ->
223-
( match%lwt refresh_config_of_context ctx payload with
226+
( match%lwt refresh_repo_config ctx payload with
224227
| Error e -> action_error e
225228
| Ok () ->
226229
let%lwt notifications = generate_notifications ctx payload in
@@ -244,4 +247,7 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
244247
| Context.Context_error msg ->
245248
log#error "%s" msg;
246249
Lwt.return_unit
250+
| State.State_error msg ->
251+
log#error "%s" msg;
252+
Lwt.return_unit
247253
end

lib/common.ml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@ 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 to_list (l : 'a t) : (string * 'a) list = Hashtbl.to_alist l
22+
23+
let of_list (m : (string * 'a) list) : 'a t = Hashtbl.of_alist_exn (module String) m
24+
25+
let wrap = of_list
26+
27+
let unwrap = to_list
28+
end
29+
1830
let fmt_error fmt = Printf.ksprintf (fun s -> Error s) fmt
1931

2032
let first_line s =

lib/context.ml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ let default : t =
2020
secrets_filepath = "secrets.json";
2121
state_filepath = None;
2222
secrets = None;
23-
state = State.empty;
23+
state = State.empty ();
2424
}
2525

2626
let make ?config_filename ?secrets_filepath ?state_filepath () =
@@ -63,8 +63,8 @@ let refresh_state ctx =
6363
end
6464
else Ok ctx
6565

66-
let print_config ctx =
67-
let cfg = State.get_repo_config_exn ctx.state in
66+
let print_config ctx repo_url =
67+
let cfg = State.find_repo_config_exn ctx.state repo_url in
6868
let secrets = get_secrets_exn ctx in
6969
log#info "using prefix routing:";
7070
Rule.Prefix.print_prefix_routing cfg.prefix_rules.rules;

lib/state.atd

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ type 'v map_as_object =
55
(string * 'v) list <json repr="object">
66
wrap <ocaml module="Common.StringMap" t="'v Common.StringMap.t">
77

8+
type 'v table_as_object =
9+
(string * 'v) list <json repr="object">
10+
wrap <ocaml module="Common.Table" t="'v Common.Table.t">
11+
812
(* A map from branch names to build statuses *)
913
type branch_statuses = status_state map_as_object
1014

@@ -13,8 +17,11 @@ type branch_statuses = status_state map_as_object
1317
branch *)
1418
type pipeline_statuses = branch_statuses map_as_object
1519

16-
(* The serializable runtime state of the bot *)
17-
type state = {
20+
(* The runtime state of a given GitHub repository *)
21+
type repo_state = {
1822
?config <ocaml mutable>: config option;
1923
pipeline_statuses <ocaml mutable>: pipeline_statuses
20-
}
24+
}
25+
26+
(* The serializable runtime state of the bot *)
27+
type state = repo_state table_as_object

lib/state.ml

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,44 @@ 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 get_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 = Hashtbl.create (module String)
12+
13+
let find_or_add_repo (state : State_t.state) repo_url = Hashtbl.find_or_add state repo_url ~default:empty_repo_state
14+
15+
let find_repo_exn (state : State_t.state) repo_url =
16+
match Hashtbl.find state repo_url with
17+
| None -> state_error "state uninitialized for repo %s" repo_url
18+
| Some repo_state -> repo_state
19+
20+
let find_repo_config_exn state repo_url =
21+
match (find_repo_exn state repo_url).config with
22+
| None -> state_error "config uninitialized for repo %s" repo_url
1423
| Some config -> config
1524

16-
let set_repo_pipeline_status (state : State_t.state) ~pipeline ~(branches : Github_t.branch list) ~status =
25+
let set_repo_config (state : State_t.state) ~repo_url ~config =
26+
match Hashtbl.find state repo_url with
27+
| None -> state_error "state uninitialized for repo %s" repo_url
28+
| Some repo_state -> repo_state.config <- Some config
29+
30+
let set_repo_pipeline_status (state : State_t.state) repo_url ~pipeline ~(branches : Github_t.branch list) ~status =
1731
let set_branch_status branch_statuses =
1832
let new_statuses = List.map branches ~f:(fun b -> b.name, status) in
1933
let init = Option.value branch_statuses ~default:(Map.empty (module String)) in
2034
List.fold_left new_statuses ~init ~f:(fun m (key, data) -> Map.set m ~key ~data)
2135
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
36+
match Hashtbl.find state repo_url with
37+
| None -> state_error "state uninitialized for repo %s" repo_url
38+
| Some repo_state ->
39+
repo_state.pipeline_statuses <- Map.update repo_state.pipeline_statuses pipeline ~f:set_branch_status
40+
41+
(** `is_pipeline_allowed s r p` returns `true` if
42+
`status_rules` doesn't define a whitelist of allowed
43+
pipelines in the config of repo `r`, or if the list
44+
contains pipeline `p`; returns `false` otherwise. *)
45+
let is_pipeline_allowed (state : State_t.state) repo_url ~pipeline =
46+
match (find_repo_exn state repo_url).config with
2947
| None -> false
3048
| Some config ->
3149
match config.status_rules.allowed_pipelines with

0 commit comments

Comments
 (0)