Skip to content

Commit bbdcf64

Browse files
dmitshurgopherbot
authored andcommitted
cmd/relui: add a "ping early-in-cycle issues" workflow
When adding a dedicated relui workflow, it was tempting to make it fancy with each issue being a sub-task, etc. But we wouldn't want that if/when this task joins others as part of a "reopen the dev tree" workflow, so keep it a simple atomic (and safe to re-run) task. This is similar to how pushing issues or checking for release blockers is one atomic task. For golang/go#58856. Change-Id: Idc8ef3cd62b7bb98c97b37d38ef9cdc2beccc24c Reviewed-on: https://go-review.googlesource.com/c/build/+/473160 Reviewed-by: Heschi Kreinick <[email protected]> Auto-Submit: Dmitri Shuralyov <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Dmitri Shuralyov <[email protected]>
1 parent 70038fd commit bbdcf64

File tree

8 files changed

+232
-122
lines changed

8 files changed

+232
-122
lines changed

cmd/gopherbot/gopherbot.go

Lines changed: 0 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,6 @@ var tasks = []struct {
440440
// Tasks that are specific to the golang/go repo.
441441
{"kicktrain", (*gopherbot).getOffKickTrain},
442442
{"unwait-release", (*gopherbot).unwaitRelease},
443-
{"ping-early-issues", (*gopherbot).pingEarlyIssues},
444443
{"label build issues", (*gopherbot).labelBuildIssues},
445444
{"label compiler/runtime issues", (*gopherbot).labelCompilerRuntimeIssues},
446445
{"label mobile issues", (*gopherbot).labelMobileIssues},
@@ -932,120 +931,6 @@ func (b *gopherbot) unwaitRelease(ctx context.Context) error {
932931
return nil
933932
}
934933

935-
// pingEarlyIssues pings early-in-cycle issues in the next major release milestone.
936-
// This is run manually (with --only-run) at the opening of a release cycle.
937-
func (b *gopherbot) pingEarlyIssues(ctx context.Context) error {
938-
// We only run this task if it was explicitly requested via
939-
// the --only-run flag.
940-
if *onlyRun == "" {
941-
return nil
942-
}
943-
944-
// Compute nextMajor, a value like "1.17" representing that Go 1.17
945-
// is the next major version (the version whose development just started).
946-
majorReleases, _, err := b.fetchReleases(ctx)
947-
if err != nil {
948-
return err
949-
}
950-
nextMajor := majorReleases[len(majorReleases)-1]
951-
952-
// The message posted in this task links to an announcement that the tree is open
953-
// for general Go 1.x development. Update the openTreeURLs map appropriately when
954-
// running this task.
955-
openTreeURLs := map[string]string{
956-
"1.22": "https://groups.google.com/g/golang-dev/c/FNPk2joOsXs/m/roCc_a4fBAAJ",
957-
}
958-
if url, ok := openTreeURLs[nextMajor]; !ok {
959-
return fmt.Errorf("openTreeURLs[%q] is missing a value, please fill it in", nextMajor)
960-
} else if !strings.HasPrefix(url, "https://groups.google.com/g/golang-dev/c/") {
961-
return fmt.Errorf("openTreeURLs[%q] is %q, which doesn't begin with the usual prefix, so please double-check that the URL is correct", nextMajor, url)
962-
}
963-
var milestoneNumber int // TODO: Determine the milestone number dynamically.
964-
if nextMajor == "1.22" {
965-
milestoneNumber = 298
966-
} else {
967-
return fmt.Errorf("major to milestone number mapping is not implemented")
968-
}
969-
970-
// Find all open early-in-cycle issues in the current major release milestone.
971-
type issue struct {
972-
ID githubv4.ID
973-
Number int
974-
Title string
975-
976-
TimelineItems struct {
977-
Nodes []struct {
978-
IssueComment struct {
979-
Author struct{ Login string }
980-
Body string
981-
} `graphql:"...on IssueComment"`
982-
}
983-
} `graphql:"timelineItems(since: $avoidDupSince, itemTypes: ISSUE_COMMENT, last: 100)"`
984-
}
985-
var earlyIssues []issue
986-
variables := map[string]interface{}{
987-
"avoidDupSince": githubv4.DateTime{Time: time.Now().Add(-30 * 24 * time.Hour)},
988-
"milestoneNumber": githubv4.String(fmt.Sprint(milestoneNumber)), // For some reason GitHub API v4 uses string type for milestone numbers.
989-
"issueCursor": (*githubv4.String)(nil),
990-
}
991-
for {
992-
var q struct {
993-
Repository struct {
994-
Issues struct {
995-
Nodes []issue
996-
PageInfo struct {
997-
EndCursor githubv4.String
998-
HasNextPage bool
999-
}
1000-
} `graphql:"issues(first: 100, after: $issueCursor, filterBy: {states: OPEN, labels: \"early-in-cycle\", milestoneNumber: $milestoneNumber}, orderBy: {field: CREATED_AT, direction: ASC})"`
1001-
} `graphql:"repository(owner: \"golang\", name: \"go\")"`
1002-
}
1003-
err := b.ghV4.Query(ctx, &q, variables)
1004-
if err != nil {
1005-
return err
1006-
}
1007-
earlyIssues = append(earlyIssues, q.Repository.Issues.Nodes...)
1008-
if !q.Repository.Issues.PageInfo.HasNextPage {
1009-
break
1010-
}
1011-
variables["issueCursor"] = githubv4.NewString(q.Repository.Issues.PageInfo.EndCursor)
1012-
}
1013-
1014-
// Ping them.
1015-
EarlyIssuesLoop:
1016-
for _, i := range earlyIssues {
1017-
for _, n := range i.TimelineItems.Nodes {
1018-
if n.IssueComment.Author.Login == "gopherbot" && strings.Contains(n.IssueComment.Body, "friendly reminder") {
1019-
log.Printf("skipping early-in-cycle issue %d, it was already pinged", i.Number)
1020-
continue EarlyIssuesLoop
1021-
}
1022-
}
1023-
1024-
// Post a comment.
1025-
if *dryRun {
1026-
log.Printf("[dry run] would ping early-in-cycle issue\n\t#%d %s", i.Number, i.Title)
1027-
continue
1028-
}
1029-
log.Printf("pinging early-in-cycle issue %d (%.32s…)", i.Number, i.Title)
1030-
time.Sleep(3 * time.Second) // Take a moment between pinging issues, since a human will be running this task manually.
1031-
var m struct {
1032-
AddComment struct {
1033-
ClientMutationID string // GraphQL doesn't permit empty mutations.
1034-
} `graphql:"addComment(input: $input)"`
1035-
}
1036-
err := b.ghV4.Mutate(ctx, &m, githubv4.AddCommentInput{
1037-
SubjectID: i.ID,
1038-
Body: githubv4.String(fmt.Sprintf("This issue is currently labeled as early-in-cycle for Go %s.\n"+
1039-
"That [time is now](%s), so a friendly reminder to look at it again.", nextMajor, openTreeURLs[nextMajor])),
1040-
}, nil)
1041-
if err != nil {
1042-
return err
1043-
}
1044-
}
1045-
1046-
return nil
1047-
}
1048-
1049934
// freezeOldIssues locks any issue that's old and closed.
1050935
// (Otherwise people find ancient bugs via searches and start asking questions
1051936
// into a void and it's sad for everybody.)

internal/relui/buildrelease_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/google/go-cmp/cmp/cmpopts"
3131
"github.com/google/go-github/github"
3232
"github.com/google/uuid"
33+
"github.com/shurcooL/githubv4"
3334
"golang.org/x/build/gerrit"
3435
"golang.org/x/build/internal"
3536
"golang.org/x/build/internal/gcsfs"
@@ -741,6 +742,10 @@ func (fakeGitHub) EditMilestone(_ context.Context, owner string, repo string, nu
741742
return nil, nil, nil
742743
}
743744

745+
func (fakeGitHub) PostComment(_ context.Context, _ githubv4.ID, _ string) error {
746+
return fmt.Errorf("pretend that PostComment failed")
747+
}
748+
744749
type verboseListener struct {
745750
t *testing.T
746751
onStall func()

internal/relui/workflows.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,27 @@ func RegisterReleaseWorkflows(ctx context.Context, h *DefinitionHolder, build *B
266266
h.RegisterDefinition("pre-announce "+r.name, wd)
267267
}
268268

269+
// Register workflows for miscellaneous tasks that happen as part of the Go release cycle.
270+
{
271+
// Register a "ping early-in-cycle issues" workflow.
272+
wd := wf.New()
273+
openTreeURL := wf.Param(wd, wf.ParamDef[string]{
274+
Name: "Open Tree URL",
275+
Doc: `Open Tree URL is the URL of an announcement that the tree is open for general Go 1.x development.`,
276+
Example: "https://groups.google.com/g/golang-dev/c/09IwUs7cxXA/m/c2jyIhECBQAJ",
277+
Check: func(openTreeURL string) error {
278+
if !strings.HasPrefix(openTreeURL, "https://groups.google.com/g/golang-dev/c/") {
279+
return fmt.Errorf("openTreeURL value %q doesn't begin with the usual prefix, so please double-check that the URL is correct", openTreeURL)
280+
}
281+
return nil
282+
},
283+
})
284+
devVer := wf.Task0(wd, "Get development version", version.GetDevelVersion)
285+
pinged := wf.Task2(wd, "Ping early-in-cycle issues", milestone.PingEarlyIssues, devVer, openTreeURL)
286+
wf.Output(wd, "pinged", pinged)
287+
h.RegisterDefinition("ping early-in-cycle issues in development milestone", wd)
288+
}
289+
269290
// Register dry-run release workflows.
270291
registerBuildTestSignOnlyWorkflow(h, version, build, currentMajor+1, task.KindBeta)
271292

internal/task/gerrit.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type GerritClient interface {
3838
// ListProjects lists all the projects on the server.
3939
ListProjects(ctx context.Context) ([]string, error)
4040
// ReadFile reads a file from project at the specified commit.
41+
// If the file doesn't exist, it returns an error matching gerrit.ErrResourceNotExist.
4142
ReadFile(ctx context.Context, project, commit, file string) ([]byte, error)
4243
// GetCommitsInRefs gets refs in which the specified commits were merged into.
4344
GetCommitsInRefs(ctx context.Context, project string, commits, refs []string) (map[string][]string, error)

internal/task/milestones.go

Lines changed: 120 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"sort"
1111
"strings"
12+
"time"
1213

1314
"github.com/google/go-github/github"
1415
"github.com/shurcooL/githubv4"
@@ -36,7 +37,11 @@ const (
3637
)
3738

3839
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
4045
}
4146

4247
// FetchMilestones returns the milestone numbers for the version currently being
@@ -172,11 +177,107 @@ func (m *MilestoneTasks) PushIssues(ctx *wf.TaskContext, milestones ReleaseMiles
172177
return nil
173178
}
174179

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+
175276
// GitHubClientInterface is a wrapper around the GitHub v3 and v4 APIs, for
176277
// testing and dry-run support.
177278
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.
180281
FetchMilestone(ctx context.Context, owner, repo, name string, create bool) (int, error)
181282

182283
// FetchMilestoneIssues returns all the open issues in the specified milestone
@@ -186,8 +287,12 @@ type GitHubClientInterface interface {
186287
// See github.Client.Issues.Edit.
187288
EditIssue(ctx context.Context, owner string, repo string, number int, issue *github.IssueRequest) (*github.Issue, *github.Response, error)
188289

189-
// See github.Client.Issues.EditMilestone
290+
// See github.Client.Issues.EditMilestone.
190291
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
191296
}
192297

193298
type GitHubClient struct {
@@ -326,3 +431,14 @@ func (c *GitHubClient) EditIssue(ctx context.Context, owner string, repo string,
326431
func (c *GitHubClient) EditMilestone(ctx context.Context, owner string, repo string, number int, milestone *github.Milestone) (*github.Milestone, *github.Response, error) {
327432
return c.V3.Issues.EditMilestone(ctx, owner, repo, number, milestone)
328433
}
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+
}

internal/task/milestones_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ func (fakeGitHub) EditMilestone(_ context.Context, owner string, repo string, nu
9898
return nil, nil, nil
9999
}
100100

101+
func (fakeGitHub) PostComment(_ context.Context, _ githubv4.ID, _ string) error {
102+
return fmt.Errorf("pretend that PostComment failed")
103+
}
104+
101105
var (
102106
flagRun = flag.Bool("run-destructive-milestones-test", false, "Run the milestone test. Requires repository owner and name flags, and GITHUB_TOKEN set in the environment.")
103107
flagOwner = flag.String("milestones-github-owner", "", "Owner of testing repository")

0 commit comments

Comments
 (0)