Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Track status state per build step instead of per pipeline #107

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 40 additions & 13 deletions lib/action.ml
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,30 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
let partition_status (ctx : Context.t) (n : status_notification) =
let cfg = Context.get_config_exn ctx in
let pipeline = n.context in
let current_status = n.state in
let rules = cfg.status_rules.rules in
let action_on_match (branches : branch list) =
let get_build_step_statuses () =
let target_url_path = Uri.of_string (Option.value ~default:"" n.target_url) |> Uri.path in
match%lwt Github_api.get_api_statuses ~ctx ~repo:n.repository ~sha:n.commit.sha with
| Error e -> action_error e
| Ok statuses ->
statuses
(* only include statuses associated with same build as `n` (in case rebuild is triggered for same commit) *)
|> List.filter ~f:(fun s ->
String.equal (Option.value ~default:"" s.target_url |> Uri.of_string |> Uri.path) target_url_path)
(* sort by most recent; string comparisons are sufficient for ISO timestamps *)
|> List.sort ~compare:(fun s1 s2 -> String.compare s2.created_at s1.created_at)
(* map from build step name to status object *)
|> List.map ~f:(fun s -> s.context, s)
|> Map.of_alist_multi (module String)
(* only use the most recent build step status *)
|> Map.map ~f:List.hd_exn
|> Map.to_alist
|> List.map ~f:snd
|> Lwt.return
in
let action_on_match (branches : branch list) statuses =
let default = Option.to_list cfg.prefix_rules.default_channel in
let () = Context.refresh_pipeline_status ~pipeline ~branches ~status:current_status ctx in
Context.refresh_pipeline_status ctx ~pipeline ~branches ~statuses;
match List.is_empty branches with
| true -> Lwt.return []
| false ->
Expand All @@ -114,18 +133,26 @@ module Action (Github_api : Api.Github) (Slack_api : Api.Slack) = struct
if Context.is_pipeline_allowed ctx ~pipeline then begin
match Rule.Status.match_rules ~rules n with
| Some Ignore | None -> Lwt.return []
| Some Allow -> action_on_match n.branches
| Some Allow ->
let%lwt statuses = get_build_step_statuses () in
action_on_match n.branches statuses
| Some Allow_once ->
match Map.find ctx.state.pipeline_statuses pipeline with
| Some branch_statuses ->
let has_same_status_state_as_prev (branch : branch) =
match Map.find branch_statuses branch.name with
| None -> false
| Some state -> Poly.equal state current_status
let%lwt statuses = get_build_step_statuses () in
let status_state_of_branch_changed (branch : branch) status =
match Map.find ctx.state.pipeline_statuses status.context with
| Some branch_statuses ->
begin
match Map.find branch_statuses branch.name with
| None -> true
| Some state -> not @@ Poly.equal state status.state
end
| None -> true
in
let at_least_one_status_state_changed branch =
List.exists statuses ~f:(status_state_of_branch_changed branch)
in
let branches = List.filter n.branches ~f:(Fn.non @@ has_same_status_state_as_prev) in
action_on_match branches
| None -> action_on_match n.branches
let branches = List.filter n.branches ~f:at_least_one_status_state_changed in
action_on_match branches statuses
end
else Lwt.return []

Expand Down
2 changes: 2 additions & 0 deletions lib/api.ml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ module type Github = sig

val get_api_commit : ctx:Context.t -> repo:repository -> sha:string -> (api_commit, string) Result.t Lwt.t

val get_api_statuses : ctx:Context.t -> repo:repository -> sha:string -> (api_statuses, string) Result.t Lwt.t

val get_pull_request : ctx:Context.t -> repo:repository -> number:int -> (pull_request, string) Result.t Lwt.t

val get_issue : ctx:Context.t -> repo:repository -> number:int -> (issue, string) Result.t Lwt.t
Expand Down
7 changes: 7 additions & 0 deletions lib/api_local.ml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ module Github : Api.Github = struct
| Error e -> Lwt.return @@ fmt_error "error while getting local file: %s\nfailed to get api commit %s" e url
| Ok file -> Lwt.return @@ Ok (Github_j.api_commit_of_string file)

let get_api_statuses ~ctx:_ ~repo:_ ~sha =
let url = Caml.Filename.concat cache_dir @@ Printf.sprintf "%s-statuses" sha in
match get_local_file url with
| Error e ->
Lwt.return @@ fmt_error "error while getting local file: %s\nfailed to get api statuses for commit %s" e url
| Ok file -> Lwt.return @@ Ok (Github_j.api_statuses_of_string file)

let get_pull_request ~ctx:_ ~repo:_ ~number:_ = Lwt.return @@ Error "undefined for local setup"

let get_issue ~ctx:_ ~repo:_ ~number:_ = Lwt.return @@ Error "undefined for local setup"
Expand Down
4 changes: 4 additions & 0 deletions lib/api_remote.ml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ module Github : Api.Github = struct
let%lwt res = commits_url ~repo ~sha |> get_resource ctx in
Lwt.return @@ Result.map res ~f:Github_j.api_commit_of_string

let get_api_statuses ~(ctx : Context.t) ~repo ~sha =
let%lwt res = commits_url ~repo ~sha |> sprintf "%s/statuses" |> get_resource ctx in
Lwt.return @@ Result.map res ~f:Github_j.api_statuses_of_string

let get_pull_request ~(ctx : Context.t) ~repo ~number =
let%lwt res = pulls_url ~repo ~number |> get_resource ctx in
Lwt.return @@ Result.map res ~f:Github_j.pull_request_of_string
Expand Down
4 changes: 2 additions & 2 deletions lib/context.ml
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ let is_pipeline_allowed ctx ~pipeline =
| Some allowed_pipelines when not @@ List.exists allowed_pipelines ~f:(String.equal pipeline) -> false
| _ -> true

let refresh_pipeline_status ctx ~pipeline ~(branches : Github_t.branch list) ~status =
if is_pipeline_allowed ctx ~pipeline then State.refresh_pipeline_status ctx.state ~pipeline ~branches ~status else ()
let refresh_pipeline_status ctx ~pipeline ~(branches : Github_t.branch list) ~statuses =
if is_pipeline_allowed ctx ~pipeline then State.refresh_pipeline_status ctx.state ~branches ~statuses else ()

let log = Log.from "context"

Expand Down
16 changes: 16 additions & 0 deletions lib/github.atd
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,22 @@ type status_notification = {
updated_at: string;
}

type api_status = {
url: string;
avatar_url: string;
id: int;
node_id: string;
state: status_state;
?description: string nullable;
?target_url: string nullable;
context: string;
created_at: string;
updated_at: string;
creator: github_user;
}

type api_statuses = api_status list

type file = {
sha: commit_hash;
filename: string;
Expand Down
23 changes: 17 additions & 6 deletions lib/state.ml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,24 @@ open Devkit

let empty : State_t.state = { pipeline_statuses = StringMap.empty }

let refresh_pipeline_status (state : State_t.state) ~pipeline ~(branches : Github_t.branch list) ~status =
let update_pipeline_status branch_statuses =
let new_statuses = List.map branches ~f:(fun b -> b.name, status) in
let init = Option.value branch_statuses ~default:(Map.empty (module String)) in
List.fold_left new_statuses ~init ~f:(fun m (key, data) -> Map.set m ~key ~data)
(** take the cross product of build steps and branches, and set each entry
to that build step's status state *)
let refresh_pipeline_status (state : State_t.state) ~branches ~statuses =
(* give a branch the specified status *)
let set_status_of_branch status_state branch_statuses (branch : Github_t.branch) =
Map.set branch_statuses ~key:branch.name ~data:status_state
in
state.pipeline_statuses <- Map.update state.pipeline_statuses pipeline ~f:update_pipeline_status
(* give all branches in a branch list the specified status state *)
let set_status_of_branches status_state ?(branch_statuses = StringMap.empty) branches =
List.fold_left branches ~init:branch_statuses ~f:(set_status_of_branch status_state)
in
(* give all branches in a branch list the specified status, for a given build step *)
let set_status_of_build_step branches pipeline_statuses (build_step_status : Github_t.api_status) =
Map.update pipeline_statuses build_step_status.context ~f:(fun branch_statuses ->
set_status_of_branches build_step_status.state ?branch_statuses branches)
in
state.pipeline_statuses <-
List.fold_left statuses ~init:state.pipeline_statuses ~f:(set_status_of_build_step branches)

let log = Log.from "state"

Expand Down
222 changes: 222 additions & 0 deletions mock_payloads/status.success_test_different_steps_from_prev.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
{
"id": 0,
"sha": "71a5b16f3e880a58a4b63201174254c9663faca4",
"name": "ahrefs/monorepo",
"target_url": "https://buildkite.com/org/pipeline2/builds/4",
"avatar_url": "https://github.com/avatars/oa/0",
"context": "buildkite/pipeline2",
"description": "Build #4 passed (5 minutes, 19 seconds)",
"state": "success",
"commit": {
"sha": "71a5b16f3e880a58a4b63201174254c9663faca4",
"node_id": "00000000000000000000",
"commit": {
"author": {
"name": "Louis",
"email": "[email protected]",
"date": "2020-06-02T03:14:51Z"
},
"committer": {
"name": "GitHub Enterprise",
"email": "[email protected]",
"date": "2020-06-02T03:14:51Z"
},
"message": "Update README.md",
"tree": {
"sha": "ee5c539cad37c77348ce7a55756acc542b41cfc7",
"url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/trees/ee5c539cad37c77348ce7a55756acc542b41cfc7"
},
"url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/commits/71a5b16f3e880a58a4b63201174254c9663faca4",
"comment_count": 0,
"verification": {
"verified": false,
"reason": "unsigned",
"signature": null,
"payload": null
}
},
"url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/71a5b16f3e880a58a4b63201174254c9663faca4",
"html_url": "https://github.com/ahrefs/monorepo/commit/71a5b16f3e880a58a4b63201174254c9663faca4",
"comments_url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/71a5b16f3e880a58a4b63201174254c9663faca4/comments",
"author": {
"login": "Khady",
"id": 0,
"node_id": "00000000000000000000",
"avatar_url": "https://github.com/avatars/u/0",
"gravatar_id": "",
"url": "https://github.com/api/v3/users/Khady",
"html_url": "https://github.com/Khady",
"followers_url": "https://github.com/api/v3/users/Khady/followers",
"following_url": "https://github.com/api/v3/users/Khady/following{/other_user}",
"gists_url": "https://github.com/api/v3/users/Khady/gists{/gist_id}",
"starred_url": "https://github.com/api/v3/users/Khady/starred{/owner}{/repo}",
"subscriptions_url": "https://github.com/api/v3/users/Khady/subscriptions",
"organizations_url": "https://github.com/api/v3/users/Khady/orgs",
"repos_url": "https://github.com/api/v3/users/Khady/repos",
"events_url": "https://github.com/api/v3/users/Khady/events{/privacy}",
"received_events_url": "https://github.com/api/v3/users/Khady/received_events",
"type": "User",
"site_admin": false
},
"committer": null,
"parents": [
{
"sha": "04cb72d6dc8d92131282a7eff57f6caf632f0a39",
"url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/04cb72d6dc8d92131282a7eff57f6caf632f0a39",
"html_url": "https://github.com/ahrefs/monorepo/commit/04cb72d6dc8d92131282a7eff57f6caf632f0a39"
}
]
},
"branches": [
{
"name": "develop",
"commit": {
"sha": "71a5b16f3e880a58a4b63201174254c9663faca4",
"url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits/71a5b16f3e880a58a4b63201174254c9663faca4"
},
"protected": false
}
],
"created_at": "2020-06-02T03:21:39+00:00",
"updated_at": "2020-06-02T03:21:39+00:00",
"repository": {
"id": 0,
"node_id": "00000000000000000000",
"name": "monorepo",
"full_name": "ahrefs/monorepo",
"private": true,
"owner": {
"login": "ahrefs",
"id": 0,
"node_id": "00000000000000000000",
"avatar_url": "https://github.com/avatars/u/0",
"gravatar_id": "",
"url": "https://github.com/api/v3/users/ahrefs",
"html_url": "https://github.com/ahrefs",
"followers_url": "https://github.com/api/v3/users/ahrefs/followers",
"following_url": "https://github.com/api/v3/users/ahrefs/following{/other_user}",
"gists_url": "https://github.com/api/v3/users/ahrefs/gists{/gist_id}",
"starred_url": "https://github.com/api/v3/users/ahrefs/starred{/owner}{/repo}",
"subscriptions_url": "https://github.com/api/v3/users/ahrefs/subscriptions",
"organizations_url": "https://github.com/api/v3/users/ahrefs/orgs",
"repos_url": "https://github.com/api/v3/users/ahrefs/repos",
"events_url": "https://github.com/api/v3/users/ahrefs/events{/privacy}",
"received_events_url": "https://github.com/api/v3/users/ahrefs/received_events",
"type": "Organization",
"site_admin": false
},
"html_url": "https://github.com/ahrefs/monorepo",
"description": null,
"fork": false,
"url": "https://github.com/api/v3/repos/ahrefs/monorepo",
"forks_url": "https://github.com/api/v3/repos/ahrefs/monorepo/forks",
"keys_url": "https://github.com/api/v3/repos/ahrefs/monorepo/keys{/key_id}",
"collaborators_url": "https://github.com/api/v3/repos/ahrefs/monorepo/collaborators{/collaborator}",
"teams_url": "https://github.com/api/v3/repos/ahrefs/monorepo/teams",
"hooks_url": "https://github.com/api/v3/repos/ahrefs/monorepo/hooks",
"issue_events_url": "https://github.com/api/v3/repos/ahrefs/monorepo/issues/events{/number}",
"events_url": "https://github.com/api/v3/repos/ahrefs/monorepo/events",
"assignees_url": "https://github.com/api/v3/repos/ahrefs/monorepo/assignees{/user}",
"branches_url": "https://github.com/api/v3/repos/ahrefs/monorepo/branches{/branch}",
"tags_url": "https://github.com/api/v3/repos/ahrefs/monorepo/tags",
"blobs_url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/blobs{/sha}",
"git_tags_url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/tags{/sha}",
"git_refs_url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/refs{/sha}",
"trees_url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/trees{/sha}",
"statuses_url": "https://github.com/api/v3/repos/ahrefs/monorepo/statuses/{sha}",
"languages_url": "https://github.com/api/v3/repos/ahrefs/monorepo/languages",
"stargazers_url": "https://github.com/api/v3/repos/ahrefs/monorepo/stargazers",
"contributors_url": "https://github.com/api/v3/repos/ahrefs/monorepo/contributors",
"subscribers_url": "https://github.com/api/v3/repos/ahrefs/monorepo/subscribers",
"subscription_url": "https://github.com/api/v3/repos/ahrefs/monorepo/subscription",
"commits_url": "https://github.com/api/v3/repos/ahrefs/monorepo/commits{/sha}",
"git_commits_url": "https://github.com/api/v3/repos/ahrefs/monorepo/git/commits{/sha}",
"comments_url": "https://github.com/api/v3/repos/ahrefs/monorepo/comments{/number}",
"issue_comment_url": "https://github.com/api/v3/repos/ahrefs/monorepo/issues/comments{/number}",
"contents_url": "https://github.com/api/v3/repos/ahrefs/monorepo/contents/{+path}",
"compare_url": "https://github.com/api/v3/repos/ahrefs/monorepo/compare/{base}...{head}",
"merges_url": "https://github.com/api/v3/repos/ahrefs/monorepo/merges",
"archive_url": "https://github.com/api/v3/repos/ahrefs/monorepo/{archive_format}{/ref}",
"downloads_url": "https://github.com/api/v3/repos/ahrefs/monorepo/downloads",
"issues_url": "https://github.com/api/v3/repos/ahrefs/monorepo/issues{/number}",
"pulls_url": "https://github.com/api/v3/repos/ahrefs/monorepo/pulls{/number}",
"milestones_url": "https://github.com/api/v3/repos/ahrefs/monorepo/milestones{/number}",
"notifications_url": "https://github.com/api/v3/repos/ahrefs/monorepo/notifications{?since,all,participating}",
"labels_url": "https://github.com/api/v3/repos/ahrefs/monorepo/labels{/name}",
"releases_url": "https://github.com/api/v3/repos/ahrefs/monorepo/releases{/id}",
"deployments_url": "https://github.com/api/v3/repos/ahrefs/monorepo/deployments",
"created_at": "2020-06-01T18:44:17Z",
"updated_at": "2020-06-02T03:14:53Z",
"pushed_at": "2020-06-02T03:14:51Z",
"git_url": "git://github.com/ahrefs/monorepo.git",
"ssh_url": "[email protected]:ahrefs/monorepo.git",
"clone_url": "https://github.com/ahrefs/monorepo.git",
"svn_url": "https://github.com/ahrefs/monorepo",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": "Shell",
"has_issues": true,
"has_projects": false,
"has_downloads": true,
"has_wiki": true,
"has_pages": false,
"forks_count": 0,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 0,
"license": null,
"forks": 0,
"open_issues": 0,
"watchers": 0,
"default_branch": "master"
},
"organization": {
"login": "ahrefs",
"id": 0,
"node_id": "00000000000000000000",
"url": "https://github.com/api/v3/orgs/ahrefs",
"repos_url": "https://github.com/api/v3/orgs/ahrefs/repos",
"events_url": "https://github.com/api/v3/orgs/ahrefs/events",
"hooks_url": "https://github.com/api/v3/orgs/ahrefs/hooks",
"issues_url": "https://github.com/api/v3/orgs/ahrefs/issues",
"members_url": "https://github.com/api/v3/orgs/ahrefs/members{/member}",
"public_members_url": "https://github.com/api/v3/orgs/ahrefs/public_members{/member}",
"avatar_url": "https://github.com/avatars/u/0",
"description": null
},
"enterprise": {
"id": 0,
"slug": "ahrefs-pte-ltd",
"name": "Ahrefs Pte Ltd",
"node_id": "00000000000000000000",
"avatar_url": "https://github.com/avatars/b/0",
"description": null,
"website_url": null,
"html_url": "https://github.com/enterprises/ahrefs-pte-ltd",
"created_at": "2019-01-09T18:50:55Z",
"updated_at": "2019-03-01T16:07:28Z"
},
"sender": {
"login": "ygrek",
"id": 0,
"node_id": "00000000000000000000",
"avatar_url": "https://github.com/avatars/u/0",
"gravatar_id": "",
"url": "https://github.com/api/v3/users/ygrek",
"html_url": "https://github.com/ip",
"followers_url": "https://github.com/api/v3/users/ygrek/followers",
"following_url": "https://github.com/api/v3/users/ygrek/following{/other_user}",
"gists_url": "https://github.com/api/v3/users/ygrek/gists{/gist_id}",
"starred_url": "https://github.com/api/v3/users/ygrek/starred{/owner}{/repo}",
"subscriptions_url": "https://github.com/api/v3/users/ygrek/subscriptions",
"organizations_url": "https://github.com/api/v3/users/ygrek/orgs",
"repos_url": "https://github.com/api/v3/users/ygrek/repos",
"events_url": "https://github.com/api/v3/users/ygrek/events{/privacy}",
"received_events_url": "https://github.com/api/v3/users/ygrek/received_events",
"type": "User",
"site_admin": true
}
}
Loading