Skip to content

Commit 271cc44

Browse files
committed
extract minimal interface for communicating w/ slack and GH
define local + remote implementations of each more concise decode_string_pad implementation
1 parent 254320e commit 271cc44

File tree

5 files changed

+169
-9
lines changed

5 files changed

+169
-9
lines changed

lib/api.ml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
open Base
2+
open Github_t
3+
open Slack_t
4+
5+
module type Github = sig
6+
val get_config : ctx:Context.t -> repo:repository -> (Config_t.config, string) Result.t Lwt.t
7+
8+
val get_api_commit : ctx:Context.t -> repo:repository -> sha:string -> (api_commit, string) Result.t Lwt.t
9+
end
10+
11+
module type Slack = sig
12+
val send_notification : chan:string -> msg:webhook_notification -> url:string -> (unit, string) Result.t Lwt.t
13+
end

lib/api_local.ml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
open Base
2+
open Common
3+
open Devkit
4+
5+
let cwd = Caml.Sys.getcwd ()
6+
7+
let cache_dir = Caml.Filename.concat cwd "github-api-cache"
8+
9+
module Github : Api.Github = struct
10+
let log = Log.from "github"
11+
12+
let get_config ~(ctx : Context.t) ~repo:_ =
13+
let url = Caml.Filename.concat cwd ctx.data.cfg_filename in
14+
match%lwt get_local_file url with
15+
| Error e ->
16+
log#error "error while getting local file %s: %s" url e;
17+
Lwt.return @@ fmt_error "failed to get config from file %s" url
18+
| Ok res -> Lwt.return @@ Ok (Config_j.config_of_string res)
19+
20+
let get_api_commit ~ctx:_ ~repo:_ ~sha =
21+
let url = Caml.Filename.concat cache_dir sha in
22+
match%lwt get_local_file url with
23+
| Error e ->
24+
log#error "error while getting local file %s: %s" url e;
25+
Lwt.return @@ fmt_error "failed to get api commit from file %s" url
26+
| Ok res -> Lwt.return @@ Ok (Github_j.api_commit_of_string res)
27+
end
28+
29+
module Slack : Api.Slack = struct
30+
let send_notification ~chan ~msg ~url:_ =
31+
let json =
32+
msg |> Slack_j.string_of_webhook_notification |> Yojson.Basic.from_string |> Yojson.Basic.pretty_to_string
33+
in
34+
Stdio.printf "will notify #%s\n" chan;
35+
Stdio.printf "%s\n" json;
36+
Lwt.return @@ Ok ()
37+
end
38+
39+
module Slack_simple : Api.Slack = struct
40+
let log = Log.from "slack"
41+
42+
let send_notification ~chan ~msg ~url:_ =
43+
log#info "will notify %s%s" chan
44+
( match msg.Slack_t.text with
45+
| None -> ""
46+
| Some s -> Printf.sprintf " with %S" s
47+
);
48+
Lwt.return @@ Ok ()
49+
end
50+
51+
module Slack_json : Api.Slack = struct
52+
let log = Log.from "slack"
53+
54+
let send_notification ~chan ~msg ~url:_ =
55+
let json = Slack_j.string_of_webhook_notification msg in
56+
log#info "will notify %s" chan;
57+
let url = Uri.of_string "https://api.slack.com/docs/messages/builder" in
58+
let url = Uri.add_query_param url ("msg", [ json ]) in
59+
log#info "%s" (Uri.to_string url);
60+
log#info "%s" json;
61+
Lwt.return @@ Ok ()
62+
end

lib/api_remote.ml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
open Base
2+
open Printf
3+
open Devkit
4+
open Common
5+
6+
module Github : Api.Github = struct
7+
let log = Log.from "github"
8+
9+
let commits_url ~(repo : Github_t.repository) ~sha =
10+
String.substr_replace_first ~pattern:"{/sha}" ~with_:sha repo.commits_url
11+
12+
let contents_url ~(repo : Github_t.repository) ~path =
13+
String.substr_replace_first ~pattern:"{+path}" ~with_:path repo.contents_url
14+
15+
let build_headers ?token () =
16+
let headers = [ "Accept: application/vnd.github.v3+json" ] in
17+
Option.value_map token ~default:headers ~f:(fun v -> sprintf "Authorization: token %s" v :: headers)
18+
19+
let get_config ~(ctx : Context.t) ~repo =
20+
let url = contents_url ~repo ~path:ctx.data.cfg_filename in
21+
let headers = build_headers ?token:ctx.secrets.gh_token () in
22+
match%lwt http_get ~headers url with
23+
| Error e ->
24+
log#error "error while querying %s: %s" url e;
25+
Lwt.return @@ fmt_error "failed to get config from file %s" url
26+
| Ok res ->
27+
let response = Github_j.content_api_response_of_string res in
28+
( match response.encoding with
29+
| "base64" ->
30+
begin
31+
try
32+
response.content |> String.split_lines |> String.concat |> decode_string_pad |> Config_j.config_of_string
33+
|> fun res -> Lwt.return @@ Ok res
34+
with Base64.Invalid_char as exn ->
35+
log#error ~exn "failed to decode base64 in Github response";
36+
Lwt.return @@ fmt_error "failed to get config from file %s" url
37+
end
38+
| encoding ->
39+
log#error "unexpected encoding '%s' in Github response" encoding;
40+
Lwt.return @@ fmt_error "failed to get config from file %s" url
41+
)
42+
43+
let get_api_commit ~(ctx : Context.t) ~repo ~sha =
44+
let url = commits_url ~repo ~sha in
45+
let headers = build_headers ?token:ctx.secrets.gh_token () in
46+
match%lwt http_get ~headers url with
47+
| Ok res -> Lwt.return @@ Ok (Github_j.api_commit_of_string res)
48+
| Error e ->
49+
log#error "error while querying %s: %s" url e;
50+
Lwt.return @@ fmt_error "failed to get api commit %s" sha
51+
end
52+
53+
module Slack : Api.Slack = struct
54+
let log = Log.from "slack"
55+
56+
let send_notification ~chan ~msg ~url =
57+
let data = Slack_j.string_of_webhook_notification msg in
58+
log#info "sending to %s : %s" chan data;
59+
match%lwt http_post ~path:url ~data with
60+
| Ok _ -> Lwt.return @@ Ok ()
61+
| Error e ->
62+
log#error "error while querying %s: %s" url e;
63+
Lwt.return @@ fmt_error "failed to send Slack notification"
64+
end

lib/common.ml

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
open Base
2+
open Devkit
3+
4+
let fmt_error fmt = Printf.ksprintf (fun s -> Error s) fmt
25

36
let first_line s =
47
match String.split ~on:'\n' s with
@@ -18,12 +21,29 @@ module Tristate : Atdgen_runtime.Json_adapter.S = struct
1821
end
1922

2023
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)
24+
String.rstrip ~drop:(List.mem [ '='; ' '; '\n'; '\r'; '\t' ] ~equal:Char.equal) s |> Base64.decode_string
25+
26+
let http_get ?headers path =
27+
match%lwt Web.http_request_lwt ~ua:"monorobot" ~verbose:true ?headers `GET path with
28+
| `Ok s -> Lwt.return @@ Ok s
29+
| `Error e -> Lwt.return @@ Error e
30+
31+
let http_post ~path ~data =
32+
let body = `Raw ("application/json", data) in
33+
match%lwt Web.http_request_lwt ~verbose:true ~body `POST path with
34+
| `Ok res -> Lwt.return @@ Ok res
35+
| `Error e -> Lwt.return @@ Error e
36+
37+
let get_local_file path =
38+
try%lwt
39+
let%lwt data = Lwt_io.with_file ~mode:Lwt_io.input path (fun ic -> Lwt_io.read ic) in
40+
Lwt.return @@ Ok data
41+
with exn -> Lwt.return @@ Error (Exn.str exn)
42+
43+
let write_to_local_file ~path ~data =
44+
try%lwt
45+
let%lwt () =
46+
Lwt_io.with_file ~flags:[ O_CREAT; O_WRONLY; O_TRUNC ] ~mode:Lwt_io.output path (fun oc -> Lwt_io.write oc data)
47+
in
48+
Lwt.return @@ Ok ()
49+
with exn -> Lwt.return @@ Error (Exn.str exn)

lib/github.atd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type repository = {
3131
full_name: string;
3232
html_url <ocaml name="url"> : string;
3333
commits_url: string;
34+
contents_url: string;
3435
}
3536

3637
type commit_pushed_notification = {

0 commit comments

Comments
 (0)