Skip to content

Commit

Permalink
Migrate release management to GitHub projects v2 (#305)
Browse files Browse the repository at this point in the history
Partially implements #278 (~~without removing the project V1 support, so
that we can test this while keeping the other one for now~~). What is
missing:

- [x] Automatically create field if it does not exist yet.
- [ ] Create views when creating a new field.
- [x] Management of PRs removed from the project (now will use a
rejected field value instead).
- [ ] Update
https://github.com/coq/coq/blob/master/dev/doc/release-process.md.

@proux01 @silene I intend to test-deploy this, so
https://github.com/orgs/coq/projects/11/views/4 and
https://github.com/coq/coq/projects/42 should in principle be updated
concurrently when new PRs are merged or backported in the 8.20+rc1
milestone.

Close coq/coq#19156.
Close #278.
Close #303.
  • Loading branch information
Zimmi48 authored Jul 5, 2024
2 parents 8f3d44f + 78d0175 commit 6cc9f25
Show file tree
Hide file tree
Showing 17 changed files with 4,813 additions and 2,007 deletions.
111 changes: 88 additions & 23 deletions bot-components/GitHub_GraphQL.ml
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,17 @@

(* Queries *)

module PullRequest_Milestone_and_Cards =
module PullRequest_Cards =
[%graphql
{|
fragment Column on ProjectColumn {
id
databaseId
}

query backportInfo($owner: String!, $repo: String!, $number: Int!) {
query prCards($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner,name: $repo) {
pullRequest(number: $number) {
milestone {
title
description
}
projectCards(first:100) {
nodes {
id
column { ... Column }
project {
columns(first:100) {
nodes { ... Column }
}
projectItems(first: 100) {
items: nodes {
item_id: id
projectV2: project {
number
}
}
}
Expand All @@ -45,14 +33,28 @@ module PullRequest_ID =
}
|}]

module PullRequest_Milestone =
[%graphql
{|
query prInfo($pr_id: ID!) {
node(id: $pr_id) {
... on PullRequest {
milestone {
title
description
}
}
}
}
|}]

module PullRequest_ID_and_Milestone =
[%graphql
{|
query prInfo($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner,name: $repo) {
pullRequest(number: $number) {
id
databaseId
milestone {
title
description
Expand All @@ -62,6 +64,18 @@ module PullRequest_ID_and_Milestone =
}
|}]

module Milestone_ID =
[%graphql
{|
query milestoneID($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner,name: $repo) {
milestone(number: $number) {
id
}
}
}
|}]

module TeamMembership =
[%graphql
{|
Expand Down Expand Up @@ -407,18 +421,69 @@ query getChecks($appId: Int!, $owner: String!, $repo:String!, $head: String!) {
}
|}]

module GetProjectFieldValues =
[%graphql
{|
query getProjectFieldValues($organization: String!, $project: Int!, $field: String!, $options: [String!]!) {
organization(login: $organization) {
projectV2(number: $project) {
id
field(name: $field) {
... on ProjectV2SingleSelectField {
id
options(names: $options) {
id
name
}
}
}
}
}
}
|}]

(* Mutations *)

module MoveCardToColumn =
module AddCardToProject =
[%graphql
{|
mutation addCard($card_id:ID!, $project_id: ID!) {
addProjectV2ItemById(input:{contentId:$card_id,projectId:$project_id}) {
item {
id
}
}
}
|}]

module UpdateFieldValue =
[%graphql
{|
mutation moveCard($card_id:ID!,$column_id:ID!) {
moveProjectCard(input:{cardId:$card_id,columnId:$column_id}) {
mutation updateFieldValue($card_id:ID!, $project_id: ID!, $field_id: ID!, $field_value_id: String!) {
updateProjectV2ItemFieldValue(input: {projectId: $project_id, itemId: $card_id, fieldId: $field_id, value: {singleSelectOptionId: $field_value_id}}) {
clientMutationId
}
}
|}]

module CreateNewReleaseManagementField =
[%graphql
{|
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
options(names: ["Request inclusion", "Shipped", "Rejected"]) {
id
name
}
}
}
}
}
|}]
module PostComment =
[%graphql
{|
Expand Down
53 changes: 20 additions & 33 deletions bot-components/GitHub_app.ml
Original file line number Diff line number Diff line change
Expand Up @@ -56,40 +56,26 @@ let post ~bot_info ~body ~token ~url =
in
Cohttp_lwt.Body.to_string body

let get_installation_token ~bot_info ~owner ~repo ~jwt :
let get_installation_token ~bot_info ~key ~app_id ~install_id :
(string * float, string) Result.t Lwt.t =
get ~bot_info ~token:jwt
~url:(f "https://api.github.com/repos/%s/%s/installation" owner repo)
>>= (fun body ->
try
let json = Yojson.Basic.from_string body in
let access_token_url =
Yojson.Basic.Util.(json |> member "access_tokens_url" |> to_string)
in
post ~bot_info ~body:None ~token:jwt ~url:access_token_url
>|= Result.return
with
| Yojson.Json_error err ->
Lwt.return_error (f "Json error: %s" err)
| Yojson.Basic.Util.Type_error (err, _) ->
Lwt.return_error (f "Json type error: %s" err) )
>|= Result.bind ~f:(fun resp ->
try
let json = Yojson.Basic.from_string resp in
Ok
(* Installation tokens expire after one hour, let's stop using them after 40 minutes *)
( Yojson.Basic.Util.(json |> member "token" |> to_string)
, Unix.time () +. (40. *. 60.) )
with
| Yojson.Json_error err ->
Error (f "Json error: %s" err)
| Yojson.Basic.Util.Type_error (err, _) ->
Error (f "Json type error: %s" err) )

let get_installation_token ~bot_info ~key ~app_id ~owner ~repo =
match make_jwt ~key ~app_id with
| Ok jwt ->
get_installation_token ~bot_info ~jwt ~owner ~repo
| Ok jwt -> (
let access_token_url =
f "https://api.github.com/app/installations/%d/access_tokens" install_id
in
post ~bot_info ~body:None ~token:jwt ~url:access_token_url
>|= fun resp ->
try
let json = Yojson.Basic.from_string resp in
Ok
(* Installation tokens expire after one hour, let's stop using them after 40 minutes *)
( Yojson.Basic.Util.(json |> member "token" |> to_string)
, Unix.time () +. (40. *. 60.) )
with
| Yojson.Json_error err ->
Error (f "Json error: %s" err)
| Yojson.Basic.Util.Type_error (err, _) ->
Error (f "Json type error: %s" err) )
| Error e ->
Lwt.return (Error e)

Expand All @@ -104,7 +90,8 @@ let get_installations ~bot_info ~key ~app_id =
Ok
( json |> to_list
|> List.map ~f:(fun json ->
json |> member "account" |> member "login" |> to_string ) )
( json |> member "account" |> member "login" |> to_string
, json |> member "id" |> to_int ) ) )
with
| Yojson.Json_error err ->
Error (f "Json error: %s" err)
Expand Down
5 changes: 2 additions & 3 deletions bot-components/GitHub_app.mli
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ val get_installation_token :
bot_info:Bot_info.t
-> key:Mirage_crypto_pk.Rsa.priv
-> app_id:int
-> owner:string
-> repo:string
-> install_id:int
-> (string * float, string) result Lwt.t

val get_installations :
bot_info:Bot_info.t
-> key:Mirage_crypto_pk.Rsa.priv
-> app_id:int
-> (string list, string) result Lwt.t
-> ((string * int) list, string) result Lwt.t
82 changes: 57 additions & 25 deletions bot-components/GitHub_mutations.ml
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,71 @@ open Utils

let send_graphql_query = GraphQL_query.send_graphql_query ~api:GitHub

let mv_card_to_column ~bot_info ({card_id; column_id} : mv_card_to_column_input)
=
let open GitHub_GraphQL.MoveCardToColumn in
let add_card_to_project ~bot_info ~card_id ~project_id =
let open GitHub_GraphQL.AddCardToProject in
makeVariables
~card_id:(GitHub_ID.to_string card_id)
~column_id:(GitHub_ID.to_string column_id)
~project_id:(GitHub_ID.to_string project_id)
()
|> serializeVariables |> variablesToJson
|> send_graphql_query ~bot_info ~query
~parse:(Fn.compose parse unsafe_fromJson)
>>= function
| Ok result -> (
match result.addProjectV2ItemById with
| None ->
Lwt.return_error "No item ID returned."
| Some {item} -> (
match item with
| None ->
Lwt.return_error "No item ID returned."
| Some item ->
Lwt.return_ok (GitHub_ID.of_string item.id) ) )
| Error err ->
Lwt.return (Error ("Error while adding card to project: " ^ err))

let update_field_value ~bot_info ~card_id ~project_id ~field_id ~field_value_id
=
let open GitHub_GraphQL.UpdateFieldValue in
makeVariables
~card_id:(GitHub_ID.to_string card_id)
~project_id:(GitHub_ID.to_string project_id)
~field_id:(GitHub_ID.to_string field_id)
~field_value_id ()
|> serializeVariables |> variablesToJson
|> send_graphql_query ~bot_info ~query
~parse:(Fn.compose parse unsafe_fromJson)
>>= function
| Ok _ ->
Lwt.return_unit
| Error err ->
Lwt_io.printlf "Error while moving project card: %s" err
Lwt_io.printlf "Error while updating field value: %s" err

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) ~field ()
|> serializeVariables |> variablesToJson
|> send_graphql_query ~bot_info ~query
~parse:(Fn.compose parse unsafe_fromJson)
>>= function
| 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.return_error (f "Error while creating new field: %s" err)

let post_comment ~bot_info ~id ~message =
let open GitHub_GraphQL.PostComment in
Expand Down Expand Up @@ -212,21 +262,17 @@ let remove_labels ~bot_info ~labels ~issue =

(* TODO: use GraphQL API *)

let update_milestone ~bot_info new_milestone (issue : issue) =
let remove_milestone ~bot_info (issue : issue) =
let headers = headers (github_header bot_info) bot_info.github_name in
let uri =
f "https://api.github.com/repos/%s/%s/issues/%d" issue.owner issue.repo
issue.number
|> Uri.of_string
in
let body =
f {|{"milestone": %s}|} new_milestone |> Cohttp_lwt.Body.of_string
in
let body = {|{"milestone": null}|} |> Cohttp_lwt.Body.of_string in
Lwt_io.printf "Sending patch request.\n"
>>= fun () -> Client.patch ~headers ~body uri >>= print_response

let remove_milestone = update_milestone "null"

let send_status_check ~bot_info ~repo_full_name ~commit ~state ~url ~context
~description =
Lwt_io.printf "Sending status check to %s (commit %s, state %s)\n"
Expand All @@ -243,17 +289,3 @@ let send_status_check ~bot_info ~repo_full_name ~commit ~state ~url ~context
|> Uri.of_string
in
send_request ~body ~uri (github_header bot_info) bot_info.github_name

let add_pr_to_column ~bot_info ~pr_id ~column_id =
let body =
f {|{"content_id":%d, "content_type": "PullRequest"}|} pr_id
|> Cohttp_lwt.Body.of_string
in
let uri =
"https://api.github.com/projects/columns/" ^ Int.to_string column_id
^ "/cards"
|> Uri.of_string
in
send_request ~body ~uri
(project_api_preview_header @ github_header bot_info)
bot_info.github_name
Loading

0 comments on commit 6cc9f25

Please sign in to comment.