-
Notifications
You must be signed in to change notification settings - Fork 2
/
on_issue_comment_event.go
252 lines (231 loc) · 8.63 KB
/
on_issue_comment_event.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/cbrgm/githubevents/githubevents"
"github.com/google/go-github/v50/github"
"github.com/kwk/buildbot-app/cmd/buildbot-app/command"
)
func OnIssueCommentEventAny(srv Server) githubevents.IssueCommentEventHandleFunc {
return func(deliveryID string, eventName string, event *github.IssueCommentEvent) error {
if event == nil {
return nil
}
// Only run on PR comments and not on issue comments
if !event.Issue.IsPullRequest() {
return nil
}
// Only handle new comments or edited ones
switch event.GetAction() {
case "edited":
break
case "created":
break
default:
return nil
}
comment := event.Comment
if comment == nil {
return nil
}
if comment.Body == nil {
return nil
}
// Check if comment body can be parsed as a /buildbot
if !command.StringIsCommand(*comment.Body) {
return nil
}
log.Printf("/buildbot was used")
// tag::thank_you[]
// This comment will be used all over the place
thankYouComment := fmt.Sprintf(
`Thank you @%s for using the <a href="todo:link-to-documentation-here"><code>%s</code></a> command <a href="%s">here</a>! `,
command.BuildbotCommand,
*event.Comment.User.Login,
*event.Comment.HTMLURL,
)
// end::thank_you[]
cmd, err := command.FromString(*comment.Body)
if err != nil {
return fmt.Errorf("failed to parse command: %w", err)
// TODO(kwk): Maybe tell the user that we didn't understand the request
}
cmd.CommentAuthor = *comment.User.Login
// Create a github client based for this app's installation
appInstallationID := *event.GetInstallation().ID
gh, err := srv.NewGithubClient(appInstallationID)
if err != nil {
err = fmt.Errorf("error creating github client: %w", err)
log.Println(err)
return err
}
log.Printf("%s commented here %s", *comment.User.Login, *event.Comment.HTMLURL)
// TODO(kwk): Check for event.Comment.AuthorAssociation == FIRST_TIME_CONTRIBUTOR
// // Possible values are "COLLABORATOR", "CONTRIBUTOR", "FIRST_TIMER", "FIRST_TIME_CONTRIBUTOR", "MEMBER", "OWNER", or "NONE".
// tag::get_pr[]
commentUser := *event.Comment.User.Login
repoOwner := *event.Repo.Owner.Login
repoName := *event.Repo.Name
prNumber := *event.Issue.Number
pr, _, err := gh.PullRequests.Get(context.Background(), repoOwner, repoName, prNumber)
// end::get_pr[]
if err != nil {
return fmt.Errorf("failed to get pull request: %w", err)
}
// tag::check_mergable[]
if !pr.GetMergeable() {
// end::check_mergable[]
_, _, err1 := gh.Issues.CreateComment(context.Background(), repoOwner, repoName, prNumber, &github.IssueComment{
Body: github.String(thankYouComment + "Sorry, but this pull request is currently not mergable."),
})
if err1 != nil {
err1 = fmt.Errorf("failed to write comment aobut mergability: %w", err1)
return fmt.Errorf("pr is not mergable: %w", err1)
}
// TODO(kwk): Do we just want to return?
return fmt.Errorf("pr is not mergable")
// tag::check_mergable[]
}
// end::check_mergable[]
// If there already is a check run for the same HEAD and the force
// option is no, then say: Sorry, no can't do.
// ----
checkRuns, err := GetAllCheckRunsForPullRequest(gh, appInstallationID, pr)
if err != nil {
return fmt.Errorf("failed to get all check runs for pull request: %w", err)
}
currentCheckRunName := cmd.ToGithubCheckNameString()
for _, checkRun := range checkRuns {
if *checkRun.Name != currentCheckRunName {
continue
}
// The requested check has already been run for the given PR. Let's see if a build is forced this time.
if cmd.Force {
// Break because we will continue with the build as planned
break
}
msg := fmt.Sprintf(thankYouComment+`
The same build request exists for this pull request's SHA (%s) <a href="%s">here</a>.
Consider specifying the <code>%s=true</code> option to enforce a new build.
`, *pr.Head.SHA, *checkRun.HTMLURL, command.CommandOptionForce)
_, _, err := gh.Issues.CreateComment(context.Background(), repoOwner, repoName, prNumber, &github.IssueComment{
Body: github.String(msg),
})
if err != nil {
return fmt.Errorf("failed to create comment: %w", err)
}
// We return here because the force option was false
return nil
}
// ----
buildLogCommentID := int64(0)
// tag::thank_you[]
newComment, _, err := gh.Issues.CreateComment(context.Background(), repoOwner, repoName, prNumber, &github.IssueComment{
Body: github.String(thankYouComment +
`<sub>This very comment will be used to continously log build state changes for your request. We decided to do this in addition to using Github's Check Runs below so you can inspect previous check runs better.</sub>`,
),
})
// end::thank_you[]
if err != nil {
return fmt.Errorf("failed to create build-log comment: %w", err)
}
if newComment != nil {
buildLogCommentID = newComment.GetID()
}
// IDEA: We could set up one try-builder for all jobs and have that
// try-builder trigger other jobs depending on the given properties. The
// try builder would need to have a Trigger build step.
// (http://docs.buildbot.net/current/manual/configuration/steps/trigger.html)
//
// Or we could have one try-builder fast workers and one try-builder for
// slowers non-mandatory workers.
//---------------------------------------------------------------------
// Lets add a check for the try bot run
// NOTE: It is important to first create the check so that we can pass
// its ID to buildbot. This way whenever buildbot tells us
// anything about a build we know how to reflect this in the
// check run on github.
//---------------------------------------------------------------------
opts := github.CreateCheckRunOptions{
Name: cmd.ToGithubCheckNameString(),
HeadSHA: *pr.Head.SHA,
Status: github.String(string(CheckRunStateQueued)),
Output: &github.CheckRunOutput{
Title: github.String("Buildbot Status Log"),
Summary: github.String(WrapMsgWithTimePrefix("We're about to forward your request to buildbot.", time.Now())),
Text: github.String("Please wait for the URL to your buildbot job to appear here."),
Images: nil,
},
}
optActions := []*github.CheckRunAction{
{
Label: "Make check required",
Description: "Make check required to pass",
Identifier: "MakeMandatory",
},
{
Label: "Make check optional",
Description: "This check is optional",
Identifier: "MakeOptional",
},
{
Label: "Rerun check",
Description: "Reruns the check",
Identifier: "ReRunCheck",
},
}
opts.Actions = optActions
checkRunTryBot, _, err := gh.Checks.CreateCheckRun(context.Background(), repoOwner, repoName, opts)
if err != nil {
return fmt.Errorf("failed to create try bot check run: %w", err)
}
// To simulate latency
// log.Printf("Sleep for 10 seconds before sending request to buildbot")
// time.Sleep(10 * time.Second)
// Make a buildbot try call with an empty diff (!!!)
props := NewGithubPullRequest(pr).ToTryBotPropertyArray()
props = append(props, fmt.Sprintf("--property=github_check_run_id=%d", *checkRunTryBot.ID))
props = append(props, fmt.Sprintf("--property=github_app_installation_id=%d", appInstallationID))
props = append(props, fmt.Sprintf("--property=github_build_log_comment_id=%d", buildLogCommentID))
props = append(props, cmd.ToTryBotPropertyArray()...)
combinedOutput, err := srv.RunTryBot(commentUser, repoOwner, repoName, props...)
if err != nil {
return fmt.Errorf("failed to run trybot: %s: %w", combinedOutput, err)
}
log.Printf("trybot command executed: %s", combinedOutput)
return nil
}
}
func GetAllCheckRunsForPullRequest(gh *github.Client, appInstallID int64, pr *github.PullRequest) ([]*github.CheckRun, error) {
if gh == nil {
return nil, fmt.Errorf("github client object is nil")
}
// For pagination see: https://docs.github.com/en/rest/guides/using-pagination-in-the-rest-api?apiVersion=2022-11-28
if pr == nil {
return nil, fmt.Errorf("pull request object is nil")
}
pagesRemaining := true
pages := []*github.CheckRun{}
listOpts := &github.ListCheckRunsOptions{
AppID: &appInstallID,
ListOptions: github.ListOptions{
Page: 1,
PerPage: 30,
},
}
for pagesRemaining {
checkRunResults, resp, err := gh.Checks.ListCheckRunsForRef(context.Background(), *pr.Base.Repo.Owner.Login, *pr.Base.Repo.Name, *pr.Head.SHA, listOpts)
if err != nil {
return nil, fmt.Errorf("failed to list check runs: %w", err)
}
if resp.NextPage == 0 {
pagesRemaining = false
} else {
listOpts.ListOptions.Page = resp.NextPage
}
pages = append(pages, checkRunResults.CheckRuns...)
}
return pages, nil
}