Skip to content

Commit 6cc9f25

Browse files
authored
Migrate release management to GitHub projects v2 (#305)
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 rocq-prover/rocq#19156. Close #278. Close #303.
2 parents 8f3d44f + 78d0175 commit 6cc9f25

17 files changed

+4813
-2007
lines changed

bot-components/GitHub_GraphQL.ml

Lines changed: 88 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,17 @@
22

33
(* Queries *)
44

5-
module PullRequest_Milestone_and_Cards =
5+
module PullRequest_Cards =
66
[%graphql
77
{|
8-
fragment Column on ProjectColumn {
9-
id
10-
databaseId
11-
}
12-
13-
query backportInfo($owner: String!, $repo: String!, $number: Int!) {
8+
query prCards($owner: String!, $repo: String!, $number: Int!) {
149
repository(owner: $owner,name: $repo) {
1510
pullRequest(number: $number) {
16-
milestone {
17-
title
18-
description
19-
}
20-
projectCards(first:100) {
21-
nodes {
22-
id
23-
column { ... Column }
24-
project {
25-
columns(first:100) {
26-
nodes { ... Column }
27-
}
11+
projectItems(first: 100) {
12+
items: nodes {
13+
item_id: id
14+
projectV2: project {
15+
number
2816
}
2917
}
3018
}
@@ -45,14 +33,28 @@ module PullRequest_ID =
4533
}
4634
|}]
4735

36+
module PullRequest_Milestone =
37+
[%graphql
38+
{|
39+
query prInfo($pr_id: ID!) {
40+
node(id: $pr_id) {
41+
... on PullRequest {
42+
milestone {
43+
title
44+
description
45+
}
46+
}
47+
}
48+
}
49+
|}]
50+
4851
module PullRequest_ID_and_Milestone =
4952
[%graphql
5053
{|
5154
query prInfo($owner: String!, $repo: String!, $number: Int!) {
5255
repository(owner: $owner,name: $repo) {
5356
pullRequest(number: $number) {
5457
id
55-
databaseId
5658
milestone {
5759
title
5860
description
@@ -62,6 +64,18 @@ module PullRequest_ID_and_Milestone =
6264
}
6365
|}]
6466

67+
module Milestone_ID =
68+
[%graphql
69+
{|
70+
query milestoneID($owner: String!, $repo: String!, $number: Int!) {
71+
repository(owner: $owner,name: $repo) {
72+
milestone(number: $number) {
73+
id
74+
}
75+
}
76+
}
77+
|}]
78+
6579
module TeamMembership =
6680
[%graphql
6781
{|
@@ -407,18 +421,69 @@ query getChecks($appId: Int!, $owner: String!, $repo:String!, $head: String!) {
407421
}
408422
|}]
409423

424+
module GetProjectFieldValues =
425+
[%graphql
426+
{|
427+
query getProjectFieldValues($organization: String!, $project: Int!, $field: String!, $options: [String!]!) {
428+
organization(login: $organization) {
429+
projectV2(number: $project) {
430+
id
431+
field(name: $field) {
432+
... on ProjectV2SingleSelectField {
433+
id
434+
options(names: $options) {
435+
id
436+
name
437+
}
438+
}
439+
}
440+
}
441+
}
442+
}
443+
|}]
444+
410445
(* Mutations *)
411446

412-
module MoveCardToColumn =
447+
module AddCardToProject =
448+
[%graphql
449+
{|
450+
mutation addCard($card_id:ID!, $project_id: ID!) {
451+
addProjectV2ItemById(input:{contentId:$card_id,projectId:$project_id}) {
452+
item {
453+
id
454+
}
455+
}
456+
}
457+
|}]
458+
459+
module UpdateFieldValue =
413460
[%graphql
414461
{|
415-
mutation moveCard($card_id:ID!,$column_id:ID!) {
416-
moveProjectCard(input:{cardId:$card_id,columnId:$column_id}) {
462+
mutation updateFieldValue($card_id:ID!, $project_id: ID!, $field_id: ID!, $field_value_id: String!) {
463+
updateProjectV2ItemFieldValue(input: {projectId: $project_id, itemId: $card_id, fieldId: $field_id, value: {singleSelectOptionId: $field_value_id}}) {
417464
clientMutationId
418465
}
419466
}
420467
|}]
421468

469+
module CreateNewReleaseManagementField =
470+
[%graphql
471+
{|
472+
mutation createNewField($project_id: ID!, $field: String!) {
473+
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."}]}) {
474+
projectV2Field {
475+
... on ProjectV2SingleSelectField {
476+
id
477+
options(names: ["Request inclusion", "Shipped", "Rejected"]) {
478+
id
479+
name
480+
}
481+
}
482+
}
483+
}
484+
}
485+
|}]
486+
422487
module PostComment =
423488
[%graphql
424489
{|

bot-components/GitHub_app.ml

Lines changed: 20 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -56,40 +56,26 @@ let post ~bot_info ~body ~token ~url =
5656
in
5757
Cohttp_lwt.Body.to_string body
5858

59-
let get_installation_token ~bot_info ~owner ~repo ~jwt :
59+
let get_installation_token ~bot_info ~key ~app_id ~install_id :
6060
(string * float, string) Result.t Lwt.t =
61-
get ~bot_info ~token:jwt
62-
~url:(f "https://api.github.com/repos/%s/%s/installation" owner repo)
63-
>>= (fun body ->
64-
try
65-
let json = Yojson.Basic.from_string body in
66-
let access_token_url =
67-
Yojson.Basic.Util.(json |> member "access_tokens_url" |> to_string)
68-
in
69-
post ~bot_info ~body:None ~token:jwt ~url:access_token_url
70-
>|= Result.return
71-
with
72-
| Yojson.Json_error err ->
73-
Lwt.return_error (f "Json error: %s" err)
74-
| Yojson.Basic.Util.Type_error (err, _) ->
75-
Lwt.return_error (f "Json type error: %s" err) )
76-
>|= Result.bind ~f:(fun resp ->
77-
try
78-
let json = Yojson.Basic.from_string resp in
79-
Ok
80-
(* Installation tokens expire after one hour, let's stop using them after 40 minutes *)
81-
( Yojson.Basic.Util.(json |> member "token" |> to_string)
82-
, Unix.time () +. (40. *. 60.) )
83-
with
84-
| Yojson.Json_error err ->
85-
Error (f "Json error: %s" err)
86-
| Yojson.Basic.Util.Type_error (err, _) ->
87-
Error (f "Json type error: %s" err) )
88-
89-
let get_installation_token ~bot_info ~key ~app_id ~owner ~repo =
9061
match make_jwt ~key ~app_id with
91-
| Ok jwt ->
92-
get_installation_token ~bot_info ~jwt ~owner ~repo
62+
| Ok jwt -> (
63+
let access_token_url =
64+
f "https://api.github.com/app/installations/%d/access_tokens" install_id
65+
in
66+
post ~bot_info ~body:None ~token:jwt ~url:access_token_url
67+
>|= fun resp ->
68+
try
69+
let json = Yojson.Basic.from_string resp in
70+
Ok
71+
(* Installation tokens expire after one hour, let's stop using them after 40 minutes *)
72+
( Yojson.Basic.Util.(json |> member "token" |> to_string)
73+
, Unix.time () +. (40. *. 60.) )
74+
with
75+
| Yojson.Json_error err ->
76+
Error (f "Json error: %s" err)
77+
| Yojson.Basic.Util.Type_error (err, _) ->
78+
Error (f "Json type error: %s" err) )
9379
| Error e ->
9480
Lwt.return (Error e)
9581

@@ -104,7 +90,8 @@ let get_installations ~bot_info ~key ~app_id =
10490
Ok
10591
( json |> to_list
10692
|> List.map ~f:(fun json ->
107-
json |> member "account" |> member "login" |> to_string ) )
93+
( json |> member "account" |> member "login" |> to_string
94+
, json |> member "id" |> to_int ) ) )
10895
with
10996
| Yojson.Json_error err ->
11097
Error (f "Json error: %s" err)

bot-components/GitHub_app.mli

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ val get_installation_token :
22
bot_info:Bot_info.t
33
-> key:Mirage_crypto_pk.Rsa.priv
44
-> app_id:int
5-
-> owner:string
6-
-> repo:string
5+
-> install_id:int
76
-> (string * float, string) result Lwt.t
87

98
val get_installations :
109
bot_info:Bot_info.t
1110
-> key:Mirage_crypto_pk.Rsa.priv
1211
-> app_id:int
13-
-> (string list, string) result Lwt.t
12+
-> ((string * int) list, string) result Lwt.t

bot-components/GitHub_mutations.ml

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,71 @@ open Utils
66

77
let send_graphql_query = GraphQL_query.send_graphql_query ~api:GitHub
88

9-
let mv_card_to_column ~bot_info ({card_id; column_id} : mv_card_to_column_input)
10-
=
11-
let open GitHub_GraphQL.MoveCardToColumn in
9+
let add_card_to_project ~bot_info ~card_id ~project_id =
10+
let open GitHub_GraphQL.AddCardToProject in
1211
makeVariables
1312
~card_id:(GitHub_ID.to_string card_id)
14-
~column_id:(GitHub_ID.to_string column_id)
13+
~project_id:(GitHub_ID.to_string project_id)
1514
()
1615
|> serializeVariables |> variablesToJson
1716
|> send_graphql_query ~bot_info ~query
1817
~parse:(Fn.compose parse unsafe_fromJson)
1918
>>= function
19+
| Ok result -> (
20+
match result.addProjectV2ItemById with
21+
| None ->
22+
Lwt.return_error "No item ID returned."
23+
| Some {item} -> (
24+
match item with
25+
| None ->
26+
Lwt.return_error "No item ID returned."
27+
| Some item ->
28+
Lwt.return_ok (GitHub_ID.of_string item.id) ) )
29+
| Error err ->
30+
Lwt.return (Error ("Error while adding card to project: " ^ err))
31+
32+
let update_field_value ~bot_info ~card_id ~project_id ~field_id ~field_value_id
33+
=
34+
let open GitHub_GraphQL.UpdateFieldValue in
35+
makeVariables
36+
~card_id:(GitHub_ID.to_string card_id)
37+
~project_id:(GitHub_ID.to_string project_id)
38+
~field_id:(GitHub_ID.to_string field_id)
39+
~field_value_id ()
40+
|> serializeVariables |> variablesToJson
41+
|> send_graphql_query ~bot_info ~query
42+
~parse:(Fn.compose parse unsafe_fromJson)
43+
>>= function
2044
| Ok _ ->
2145
Lwt.return_unit
2246
| Error err ->
23-
Lwt_io.printlf "Error while moving project card: %s" err
47+
Lwt_io.printlf "Error while updating field value: %s" err
48+
49+
let create_new_release_management_field ~bot_info ~project_id ~field =
50+
let open GitHub_GraphQL.CreateNewReleaseManagementField in
51+
makeVariables ~project_id:(GitHub_ID.to_string project_id) ~field ()
52+
|> serializeVariables |> variablesToJson
53+
|> send_graphql_query ~bot_info ~query
54+
~parse:(Fn.compose parse unsafe_fromJson)
55+
>>= function
56+
| Ok result -> (
57+
match result.createProjectV2Field with
58+
| None ->
59+
Lwt.return_error "No field returned after creation."
60+
| Some result -> (
61+
match result.projectV2Field with
62+
| None ->
63+
Lwt.return_error "No field returned after creation."
64+
| Some (`ProjectV2SingleSelectField result) ->
65+
Lwt.return_ok
66+
( GitHub_ID.of_string result.id
67+
, result.options |> Array.to_list
68+
|> List.map ~f:(fun {name; id} -> (name, id)) )
69+
| Some _ ->
70+
Lwt.return_error
71+
"Field returned after creation is not of type single select." ) )
72+
| Error err ->
73+
Lwt.return_error (f "Error while creating new field: %s" err)
2474

2575
let post_comment ~bot_info ~id ~message =
2676
let open GitHub_GraphQL.PostComment in
@@ -212,21 +262,17 @@ let remove_labels ~bot_info ~labels ~issue =
212262

213263
(* TODO: use GraphQL API *)
214264

215-
let update_milestone ~bot_info new_milestone (issue : issue) =
265+
let remove_milestone ~bot_info (issue : issue) =
216266
let headers = headers (github_header bot_info) bot_info.github_name in
217267
let uri =
218268
f "https://api.github.com/repos/%s/%s/issues/%d" issue.owner issue.repo
219269
issue.number
220270
|> Uri.of_string
221271
in
222-
let body =
223-
f {|{"milestone": %s}|} new_milestone |> Cohttp_lwt.Body.of_string
224-
in
272+
let body = {|{"milestone": null}|} |> Cohttp_lwt.Body.of_string in
225273
Lwt_io.printf "Sending patch request.\n"
226274
>>= fun () -> Client.patch ~headers ~body uri >>= print_response
227275

228-
let remove_milestone = update_milestone "null"
229-
230276
let send_status_check ~bot_info ~repo_full_name ~commit ~state ~url ~context
231277
~description =
232278
Lwt_io.printf "Sending status check to %s (commit %s, state %s)\n"
@@ -243,17 +289,3 @@ let send_status_check ~bot_info ~repo_full_name ~commit ~state ~url ~context
243289
|> Uri.of_string
244290
in
245291
send_request ~body ~uri (github_header bot_info) bot_info.github_name
246-
247-
let add_pr_to_column ~bot_info ~pr_id ~column_id =
248-
let body =
249-
f {|{"content_id":%d, "content_type": "PullRequest"}|} pr_id
250-
|> Cohttp_lwt.Body.of_string
251-
in
252-
let uri =
253-
"https://api.github.com/projects/columns/" ^ Int.to_string column_id
254-
^ "/cards"
255-
|> Uri.of_string
256-
in
257-
send_request ~body ~uri
258-
(project_api_preview_header @ github_header bot_info)
259-
bot_info.github_name

0 commit comments

Comments
 (0)