From b32fd458bdc81791f7dc120b6842c48736e9bff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Zimmermann?= Date: Mon, 1 Jul 2024 17:38:01 +0200 Subject: [PATCH] Automatically create project v2 field when it does not exist. --- bot-components/GitHub_GraphQL.ml | 4 +- bot-components/GitHub_mutations.ml | 24 ++++- bot-components/GitHub_mutations.mli | 5 +- bot-components/GitHub_queries.ml | 9 +- bot-components/GitHub_queries.mli | 2 +- src/actions.ml | 162 +++++++++++++++++++++++----- 6 files changed, 167 insertions(+), 39 deletions(-) diff --git a/bot-components/GitHub_GraphQL.ml b/bot-components/GitHub_GraphQL.ml index 35a6d6b3..d0a228cb 100644 --- a/bot-components/GitHub_GraphQL.ml +++ b/bot-components/GitHub_GraphQL.ml @@ -442,8 +442,8 @@ module UpdateFieldValue = module CreateNewReleaseManagementField = [%graphql {| - mutation createNewField($project_id: ID!, $name: String!) { - createProjectV2Field(input: {projectId: $project_id, dataType: SINGLE_SELECT, name: $name, singleSelectOptions: [{name: "Request inclusion", color: GREEN, description: "This merged pull request is proposed for inclusion."}, {name: "Shipped", color: PURPLE, description: "This pull request has been backported (or merged directly in the release branch)."}, {name: "Rejected", color: RED, description: "This merged pull request will not be included in this release."}]}) { + mutation createNewField($project_id: ID!, $field: String!) { + createProjectV2Field(input: {projectId: $project_id, dataType: SINGLE_SELECT, name: $field, singleSelectOptions: [{name: "Request inclusion", color: GREEN, description: "This merged pull request is proposed for inclusion."}, {name: "Shipped", color: PURPLE, description: "This pull request has been backported (or merged directly in the release branch)."}, {name: "Rejected", color: RED, description: "This merged pull request will not be included in this release."}]}) { projectV2Field { ... on ProjectV2SingleSelectField { id diff --git a/bot-components/GitHub_mutations.ml b/bot-components/GitHub_mutations.ml index a83668a5..b76e42e9 100644 --- a/bot-components/GitHub_mutations.ml +++ b/bot-components/GitHub_mutations.ml @@ -46,17 +46,31 @@ let update_field_value ~bot_info ~card_id ~project_id ~field_id ~field_value_id | Error err -> Lwt_io.printlf "Error while updating field value: %s" err -let create_new_release_management_field ~bot_info ~project_id ~name = +let create_new_release_management_field ~bot_info ~project_id ~field = let open GitHub_GraphQL.CreateNewReleaseManagementField in - makeVariables ~project_id:(GitHub_ID.to_string project_id) ~name () + makeVariables ~project_id:(GitHub_ID.to_string project_id) ~field () |> serializeVariables |> variablesToJson |> send_graphql_query ~bot_info ~query ~parse:(Fn.compose parse unsafe_fromJson) >>= function - | Ok _ -> - Lwt.return_unit + | Ok result -> ( + match result.createProjectV2Field with + | None -> + Lwt.return_error "No field returned after creation." + | Some result -> ( + match result.projectV2Field with + | None -> + Lwt.return_error "No field returned after creation." + | Some (`ProjectV2SingleSelectField result) -> + Lwt.return_ok + ( GitHub_ID.of_string result.id + , result.options |> Array.to_list + |> List.map ~f:(fun {name; id} -> (name, id)) ) + | Some _ -> + Lwt.return_error + "Field returned after creation is not of type single select." ) ) | Error err -> - Lwt_io.printlf "Error while creating new field: %s" err + Lwt.return_error (f "Error while creating new field: %s" err) let post_comment ~bot_info ~id ~message = let open GitHub_GraphQL.PostComment in diff --git a/bot-components/GitHub_mutations.mli b/bot-components/GitHub_mutations.mli index 6b444ccc..082e8b84 100644 --- a/bot-components/GitHub_mutations.mli +++ b/bot-components/GitHub_mutations.mli @@ -15,7 +15,10 @@ val update_field_value : -> unit Lwt.t val create_new_release_management_field : - bot_info:Bot_info.t -> project_id:GitHub_ID.t -> name:string -> unit Lwt.t + bot_info:Bot_info.t + -> project_id:GitHub_ID.t + -> field:string + -> (GitHub_ID.t * (string * string) list, string) result Lwt.t val post_comment : bot_info:Bot_info.t diff --git a/bot-components/GitHub_queries.ml b/bot-components/GitHub_queries.ml index adba97f5..5d79db1a 100644 --- a/bot-components/GitHub_queries.ml +++ b/bot-components/GitHub_queries.ml @@ -960,12 +960,15 @@ let get_project_field_values ~bot_info ~organization ~project ~field ~options = let options = field.options |> Array.to_list in Lwt.return_ok ( GitHub_ID.of_string project.id - , GitHub_ID.of_string field.id - , List.map ~f:(fun {name; id} -> (name, id)) options ) + , Some + ( GitHub_ID.of_string field.id + , List.map ~f:(fun {name; id} -> (name, id)) options ) ) | Some _ -> Lwt.return_error (f "Field %s is not a single select field." field) | None -> - Lwt.return_error (f "Field %s does not exist." field) ) + (* We consider the field not existing in the project to be + acceptable, because it can be created then. *) + Lwt.return_ok (GitHub_ID.of_string project.id, None) ) | None -> Lwt.return_error (f "Unknown project %d of organization %s" project organization) ) diff --git a/bot-components/GitHub_queries.mli b/bot-components/GitHub_queries.mli index ebc126d5..4e467631 100644 --- a/bot-components/GitHub_queries.mli +++ b/bot-components/GitHub_queries.mli @@ -135,4 +135,4 @@ val get_project_field_values : -> project:int -> field:string -> options:string array - -> (GitHub_ID.t * GitHub_ID.t * (string * string) list, string) result Lwt.t + -> (GitHub_ID.t * (GitHub_ID.t * (string * string) list) option, string) result Lwt.t diff --git a/src/actions.ml b/src/actions.ml index 1f0b89f9..08442665 100644 --- a/src/actions.ml +++ b/src/actions.ml @@ -2646,13 +2646,15 @@ let coq_push_action ~bot_info ~base_ref ~commits_msg = Lwt_io.printf "PR was merged into the backporting branch directly.\n" >>= fun () -> + let field = backport_to ^ " status" in GitHub_queries.get_project_field_values ~bot_info - ~organization:"coq" ~project:11 - ~field:(backport_to ^ " status") ~options:[|"Shipped"|] + ~organization:"coq" ~project:11 ~field + ~options:[|"Shipped"|] >>= fun project_info -> match project_info with - | Ok (project_id, field_id, [("Shipped", field_value_id)]) - -> ( + | Ok + ( project_id + , Some (field_id, [("Shipped", field_value_id)]) ) -> ( GitHub_mutations.add_card_to_project ~bot_info ~card_id ~project_id >>= fun result -> @@ -2663,15 +2665,54 @@ let coq_push_action ~bot_info ~base_ref ~commits_msg = | Error err -> Lwt_io.printf "Error while adding card to project: %s\n" err ) - | Ok (_, _, []) -> + | Ok (_, Some (_, [])) -> Lwt_io.printf "Error: Could not find 'Shipped' option in the field.\n" - | Ok (_, _, _) -> + | Ok (_, Some _) -> Lwt_io.printf "Error: Unexpected result when looking for 'Shipped'.\n" + | Ok (project_id, None) -> ( + Lwt_io.printf + "Required backporting field '%s' does not exist yet. \ + Creating it..." + field + >>= fun () -> + GitHub_mutations.create_new_release_management_field + ~bot_info ~project_id ~field + >>= function + | Ok (field_id, options) -> ( + match + List.find_map options ~f:(function + | "Shipped", field_value_id -> + Some field_value_id + | _ -> + None ) + with + | Some field_value_id -> ( + (* duplicated code: not ideal *) + GitHub_mutations.add_card_to_project ~bot_info + ~card_id ~project_id + >>= fun result -> + match result with + | Ok card_id -> + GitHub_mutations.update_field_value ~bot_info + ~card_id ~project_id ~field_id + ~field_value_id + | Error err -> + Lwt_io.printf + "Error while adding card to project: %s\n" + err ) + | None -> + Lwt_io.printlf + "Error new field '%s status' was created, but \ + does not have a 'Shipped' option." + field ) + | Error err -> + Lwt_io.printlf + "Error while creating new backporting field '%s': \ + %s" + field err ) | Error err -> - (* TODO: we could treat the case where the field does not - exist yet and create it. *) Lwt_io.printf "Error while getting project field values: %s\n" err else if String.equal base_ref "refs/heads/master" then @@ -2682,16 +2723,16 @@ let coq_push_action ~bot_info ~base_ref ~commits_msg = Lwt_io.printf "Backporting to %s was requested.\n" backport_to >>= fun () -> + let field = backport_to ^ " status" in GitHub_queries.get_project_field_values ~bot_info - ~organization:"coq" ~project:11 - ~field:(backport_to ^ " status") + ~organization:"coq" ~project:11 ~field ~options:[|"Request inclusion"|] >>= fun project_info -> match project_info with | Ok ( project_id - , field_id - , [("Request inclusion", field_value_id)] ) -> ( + , Some (field_id, [("Request inclusion", field_value_id)]) + ) -> ( GitHub_mutations.add_card_to_project ~bot_info ~card_id ~project_id >>= fun result -> @@ -2702,17 +2743,56 @@ let coq_push_action ~bot_info ~base_ref ~commits_msg = | Error err -> Lwt_io.printf "Error while adding card to project: %s\n" err ) - | Ok (_, _, []) -> + | Ok (_, Some (_, [])) -> Lwt_io.printf "Error: Could not find 'Request inclusion' option in \ the field.\n" - | Ok (_, _, _) -> + | Ok (_, Some _) -> Lwt_io.printf "Error: Unexpected result when looking for 'Request \ inclusion'.\n" + | Ok (project_id, None) -> ( + Lwt_io.printf + "Required backporting field '%s' does not exist yet. \ + Creating it..." + field + >>= fun () -> + GitHub_mutations.create_new_release_management_field + ~bot_info ~project_id ~field + >>= function + | Ok (field_id, options) -> ( + match + List.find_map options ~f:(function + | "Request inclusion", field_value_id -> + Some field_value_id + | _ -> + None ) + with + | Some field_value_id -> ( + (* duplicated code: not ideal *) + GitHub_mutations.add_card_to_project ~bot_info + ~card_id ~project_id + >>= fun result -> + match result with + | Ok card_id -> + GitHub_mutations.update_field_value ~bot_info + ~card_id ~project_id ~field_id + ~field_value_id + | Error err -> + Lwt_io.printf + "Error while adding card to project: %s\n" + err ) + | None -> + Lwt_io.printlf + "Error new field '%s status' was created, but \ + does not have a 'Request inclusion' option." + field ) + | Error err -> + Lwt_io.printlf + "Error while creating new backporting field '%s': \ + %s" + field err ) | Error err -> - (* TODO: we could treat the case where the field does not - exist yet and create it. *) Lwt_io.printf "Error while getting project field values: %s\n" err else @@ -2744,27 +2824,55 @@ let coq_push_action ~bot_info ~base_ref ~commits_msg = >>= fun () -> (* We could avoid this query by looking for this field in the previous query to GitHub. *) + let field = backport_to ^ " status" in GitHub_queries.get_project_field_values ~bot_info - ~organization:"coq" ~project:11 ~field:(backport_to ^ " status") - ~options:[|"Shipped"|] + ~organization:"coq" ~project:11 ~field ~options:[|"Shipped"|] >>= fun project_info -> match project_info with - | Ok (project_id, field_id, [("Shipped", field_value_id)]) -> + | Ok (project_id, Some (field_id, [("Shipped", field_value_id)])) + -> GitHub_mutations.update_field_value ~bot_info ~card_id ~project_id ~field_id ~field_value_id - | Ok (_, _, []) -> + | Ok (_, Some (_, [])) -> Lwt_io.printf - "Error: Could not find 'Shipped' option in the field named \ - '%s status'.\n" + "Error: Field '%s status' exists but does not have a \ + 'Shipped' option.\n" backport_to - | Ok (_, _, _) -> + | Ok (_, Some _) -> Lwt_io.printf "Error: Unexpected result when looking for 'Shipped' \ - option in the field named '%s status'.\n" - backport_to + option in the field named '%s'.\n" + field + | Ok (project_id, None) -> ( + Lwt_io.printf + "Required backporting field '%s' does not exist yet. \ + Creating it..." + field + >>= fun () -> + GitHub_mutations.create_new_release_management_field ~bot_info + ~project_id ~field + >>= function + | Ok (field_id, options) -> ( + match + List.find_map options ~f:(function + | "Shipped", field_value_id -> + Some field_value_id + | _ -> + None ) + with + | Some field_value_id -> + GitHub_mutations.update_field_value ~bot_info ~card_id + ~project_id ~field_id ~field_value_id + | None -> + Lwt_io.printlf + "Error new field '%s status' was created, but does \ + not have a 'Shipped' option." + field ) + | Error err -> + Lwt_io.printlf + "Error while creating new backporting field '%s': %s" + field err ) | Error err -> - (* TODO: we could treat the case where the field does not - exist yet and create it. *) Lwt_io.printf "Error while getting project field values: %s\n" err ) | None ->