9
9
"fmt"
10
10
"sort"
11
11
"strings"
12
+ "time"
12
13
13
14
"github.com/google/go-github/github"
14
15
"github.com/shurcooL/githubv4"
@@ -36,7 +37,11 @@ const (
36
37
)
37
38
38
39
type ReleaseMilestones struct {
39
- Current , Next int
40
+ // Current is the GitHub milestone number for the current Go release.
41
+ // For example, 279 for the "Go1.21" milestone (https://github.com/golang/go/milestone/279).
42
+ Current int
43
+ // Next is the GitHub milestone number for the next Go release of the same kind.
44
+ Next int
40
45
}
41
46
42
47
// FetchMilestones returns the milestone numbers for the version currently being
@@ -172,11 +177,107 @@ func (m *MilestoneTasks) PushIssues(ctx *wf.TaskContext, milestones ReleaseMiles
172
177
return nil
173
178
}
174
179
180
+ // PingEarlyIssues pings early-in-cycle issues in the development major release milestone.
181
+ // This is done once at the opening of a release cycle, currently via a standalone workflow.
182
+ //
183
+ // develVersion is a value like 22 representing that Go 1.22 is the major version whose
184
+ // development has recently started, and whose early-in-cycle issues are to be pinged.
185
+ func (m * MilestoneTasks ) PingEarlyIssues (ctx * wf.TaskContext , develVersion int , openTreeURL string ) (result struct {}, _ error ) {
186
+ milestoneName := fmt .Sprintf ("Go1.%d" , develVersion )
187
+
188
+ gh , ok := m .Client .(* GitHubClient )
189
+ if ! ok || gh .V4 == nil {
190
+ // TODO(go.dev/issue/58856): Decide if it's worth moving the GraphQL query/mutation
191
+ // into GitHubClientInterface. That kinda harms readability because GraphQL code is
192
+ // basically a flexible API call, so it's most readable when close to where they're
193
+ // used. This also depends on what kind of tests we'll want to use for this.
194
+ return struct {}{}, fmt .Errorf ("no GitHub API v4 client" )
195
+ }
196
+
197
+ // Find all open early-in-cycle issues in the development major release milestone.
198
+ type issue struct {
199
+ ID githubv4.ID
200
+ Number int
201
+ Title string
202
+
203
+ TimelineItems struct {
204
+ Nodes []struct {
205
+ IssueComment struct {
206
+ Author struct { Login string }
207
+ Body string
208
+ } `graphql:"...on IssueComment"`
209
+ }
210
+ } `graphql:"timelineItems(since: $avoidDupSince, itemTypes: ISSUE_COMMENT, last: 100)"`
211
+ }
212
+ var earlyIssues []issue
213
+ milestoneNumber , err := m .Client .FetchMilestone (ctx , m .RepoOwner , m .RepoName , milestoneName , false )
214
+ if err != nil {
215
+ return struct {}{}, err
216
+ }
217
+ variables := map [string ]interface {}{
218
+ "repoOwner" : githubv4 .String (m .RepoOwner ),
219
+ "repoName" : githubv4 .String (m .RepoName ),
220
+ "avoidDupSince" : githubv4.DateTime {Time : time .Now ().Add (- 30 * 24 * time .Hour )},
221
+ "milestoneNumber" : githubv4 .String (fmt .Sprint (milestoneNumber )), // For some reason GitHub API v4 uses string type for milestone numbers.
222
+ "issueCursor" : (* githubv4 .String )(nil ),
223
+ }
224
+ for {
225
+ var q struct {
226
+ Repository struct {
227
+ Issues struct {
228
+ Nodes []issue
229
+ PageInfo struct {
230
+ EndCursor githubv4.String
231
+ HasNextPage bool
232
+ }
233
+ } `graphql:"issues(first: 100, after: $issueCursor, filterBy: {states: OPEN, labels: \"early-in-cycle\", milestoneNumber: $milestoneNumber}, orderBy: {field: CREATED_AT, direction: ASC})"`
234
+ } `graphql:"repository(owner: $repoOwner, name: $repoName)"`
235
+ }
236
+ err := gh .V4 .Query (ctx , & q , variables )
237
+ if err != nil {
238
+ return struct {}{}, err
239
+ }
240
+ earlyIssues = append (earlyIssues , q .Repository .Issues .Nodes ... )
241
+ if ! q .Repository .Issues .PageInfo .HasNextPage {
242
+ break
243
+ }
244
+ variables ["issueCursor" ] = githubv4 .NewString (q .Repository .Issues .PageInfo .EndCursor )
245
+ }
246
+
247
+ // Ping them.
248
+ ctx .Printf ("Processing %d early-in-cycle issues in %s milestone (milestone number %d)." , len (earlyIssues ), milestoneName , milestoneNumber )
249
+ EarlyIssuesLoop:
250
+ for _ , i := range earlyIssues {
251
+ for _ , n := range i .TimelineItems .Nodes {
252
+ if n .IssueComment .Author .Login == "gopherbot" && strings .Contains (n .IssueComment .Body , "friendly reminder" ) {
253
+ ctx .Printf ("Skipping issue %d, it was already pinged." , i .Number )
254
+ continue EarlyIssuesLoop
255
+ }
256
+ }
257
+
258
+ // Post a comment.
259
+ const dryRun = false
260
+ if dryRun {
261
+ ctx .Printf ("[dry run] Would've pinged issue %d (%.32s…)." , i .Number , i .Title )
262
+ continue
263
+ }
264
+ err := m .Client .PostComment (ctx , i .ID , fmt .Sprintf ("This issue is currently labeled as early-in-cycle for Go 1.%d.\n " +
265
+ "That [time is now](%s), so a friendly reminder to look at it again." , develVersion , openTreeURL ))
266
+ if err != nil {
267
+ return struct {}{}, err
268
+ }
269
+ ctx .Printf ("Pinged issue %d (%.32s…)." , i .Number , i .Title )
270
+ time .Sleep (3 * time .Second ) // Take a moment between pinging issues to avoid a high rate of addComment mutations.
271
+ }
272
+
273
+ return struct {}{}, nil
274
+ }
275
+
175
276
// GitHubClientInterface is a wrapper around the GitHub v3 and v4 APIs, for
176
277
// testing and dry-run support.
177
278
type GitHubClientInterface interface {
178
- // FetchMilestone returns the number of the requested milestone. If create is true,
179
- // and the milestone doesn't exist, it will be created.
279
+ // FetchMilestone returns the number of the GitHub milestone with the specified name.
280
+ // If create is true, and the milestone doesn't exist, it will be created.
180
281
FetchMilestone (ctx context.Context , owner , repo , name string , create bool ) (int , error )
181
282
182
283
// FetchMilestoneIssues returns all the open issues in the specified milestone
@@ -186,8 +287,12 @@ type GitHubClientInterface interface {
186
287
// See github.Client.Issues.Edit.
187
288
EditIssue (ctx context.Context , owner string , repo string , number int , issue * github.IssueRequest ) (* github.Issue , * github.Response , error )
188
289
189
- // See github.Client.Issues.EditMilestone
290
+ // See github.Client.Issues.EditMilestone.
190
291
EditMilestone (ctx context.Context , owner string , repo string , number int , milestone * github.Milestone ) (* github.Milestone , * github.Response , error )
292
+
293
+ // PostComment creates a comment on a GitHub issue or pull request
294
+ // identified by the given GitHub Node ID.
295
+ PostComment (_ context.Context , id githubv4.ID , body string ) error
191
296
}
192
297
193
298
type GitHubClient struct {
@@ -326,3 +431,14 @@ func (c *GitHubClient) EditIssue(ctx context.Context, owner string, repo string,
326
431
func (c * GitHubClient ) EditMilestone (ctx context.Context , owner string , repo string , number int , milestone * github.Milestone ) (* github.Milestone , * github.Response , error ) {
327
432
return c .V3 .Issues .EditMilestone (ctx , owner , repo , number , milestone )
328
433
}
434
+
435
+ func (c * GitHubClient ) PostComment (ctx context.Context , id githubv4.ID , body string ) error {
436
+ return c .V4 .Mutate (ctx , new (struct {
437
+ AddComment struct {
438
+ ClientMutationID string // Unused; GraphQL doesn't allow for mutations to return nothing.
439
+ } `graphql:"addComment(input: $input)"`
440
+ }), githubv4.AddCommentInput {
441
+ SubjectID : id ,
442
+ Body : githubv4 .String (body ),
443
+ }, nil )
444
+ }
0 commit comments