Skip to content

Commit 70038fd

Browse files
dmitshurgopherbot
authored andcommitted
cmd/gopherbot: replace maintner with GitHub API v4 in ping-early-issues
Do this here first to make the diff easier to see. The next CL in stack moves it to relui. Notably the GraphQL implementation here is missing the "avoid duplicate comments" safety net implemented in maintner-powered addGitHubComment. With maintner, it costs no API calls to check all comments in a GitHub issue for a past comment. When using the GitHub API, this unfortunately costs more API quota and needs to be implemented explicitly, adding to the verbosity of the implementation. For golang/go#58856. Change-Id: I2757697e561cb00d041f65ba846d902c0de35e22 Reviewed-on: https://go-review.googlesource.com/c/build/+/473159 Reviewed-by: Heschi Kreinick <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Auto-Submit: Dmitri Shuralyov <[email protected]> Run-TryBot: Dmitri Shuralyov <[email protected]>
1 parent e7051bf commit 70038fd

File tree

1 file changed

+94
-21
lines changed

1 file changed

+94
-21
lines changed

cmd/gopherbot/gopherbot.go

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030

3131
"cloud.google.com/go/compute/metadata"
3232
"github.com/google/go-github/v48/github"
33+
"github.com/shurcooL/githubv4"
3334
"go4.org/strutil"
3435
"golang.org/x/build/devapp/owners"
3536
"golang.org/x/build/gerrit"
@@ -47,7 +48,7 @@ import (
4748
var (
4849
dryRun = flag.Bool("dry-run", false, "just report what would've been done, without changing anything")
4950
daemon = flag.Bool("daemon", false, "run in daemon mode")
50-
githubTokenFile = flag.String("github-token-file", filepath.Join(os.Getenv("HOME"), "keys", "github-gobot"), `File to load Github token from. File should be of form <username>:<token>`)
51+
githubTokenFile = flag.String("github-token-file", filepath.Join(os.Getenv("HOME"), "keys", "github-gobot"), `File to load GitHub token from. File should be of form <username>:<token>`)
5152
// go here: https://go-review.googlesource.com/settings#HTTPCredentials
5253
// click "Obtain Password"
5354
// The next page will have a .gitcookies file - look for the part that has
@@ -112,7 +113,7 @@ type milestone struct {
112113
Name string
113114
}
114115

115-
func getGithubToken(ctx context.Context, sc *secret.Client) (string, error) {
116+
func getGitHubToken(ctx context.Context, sc *secret.Client) (string, error) {
116117
if metadata.OnGCE() && sc != nil {
117118
ctxSc, cancel := context.WithTimeout(ctx, 10*time.Second)
118119
defer cancel()
@@ -163,17 +164,18 @@ func getGerritAuth(ctx context.Context, sc *secret.Client) (username string, pas
163164
return f[0], f[1], nil
164165
}
165166

166-
func getGithubClient(ctx context.Context, sc *secret.Client) (*github.Client, error) {
167-
token, err := getGithubToken(ctx, sc)
167+
func getGitHubClients(ctx context.Context, sc *secret.Client) (*github.Client, *githubv4.Client, error) {
168+
token, err := getGitHubToken(ctx, sc)
168169
if err != nil {
169170
if *dryRun {
170-
return github.NewClient(http.DefaultClient), nil
171+
// Note: GitHub API v4 requires requests to be authenticated, which isn't implemented here.
172+
return github.NewClient(http.DefaultClient), githubv4.NewClient(http.DefaultClient), nil
171173
}
172-
return nil, err
174+
return nil, nil, err
173175
}
174176
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
175177
tc := oauth2.NewClient(context.Background(), ts)
176-
return github.NewClient(tc), nil
178+
return github.NewClient(tc), githubv4.NewClient(tc), nil
177179
}
178180

179181
func getGerritClient(ctx context.Context, sc *secret.Client) (*gerrit.Client, error) {
@@ -230,7 +232,7 @@ func main() {
230232
}
231233
ctx := context.Background()
232234

233-
ghc, err := getGithubClient(ctx, sc)
235+
ghV3, ghV4, err := getGitHubClients(ctx, sc)
234236
if err != nil {
235237
log.Fatal(err)
236238
}
@@ -246,10 +248,11 @@ func main() {
246248
var goRepo = maintner.GitHubRepoID{Owner: "golang", Repo: "go"}
247249
var vscode = maintner.GitHubRepoID{Owner: "golang", Repo: "vscode-go"}
248250
bot := &gopherbot{
249-
ghc: ghc,
251+
ghc: ghV3,
252+
ghV4: ghV4,
250253
gerrit: gerrit,
251254
mc: mc,
252-
is: ghc.Issues,
255+
is: ghV3.Issues,
253256
deletedChanges: map[gerritChange]bool{
254257
{"crypto", 35958}: true,
255258
{"scratch", 71730}: true,
@@ -408,6 +411,7 @@ func main() {
408411

409412
type gopherbot struct {
410413
ghc *github.Client
414+
ghV4 *githubv4.Client
411415
gerrit *gerrit.Client
412416
mc apipb.MaintnerServiceClient
413417
corpus *maintner.Corpus
@@ -949,28 +953,97 @@ func (b *gopherbot) pingEarlyIssues(ctx context.Context) error {
949953
// for general Go 1.x development. Update the openTreeURLs map appropriately when
950954
// running this task.
951955
openTreeURLs := map[string]string{
952-
"1.21": "https://groups.google.com/g/golang-dev/c/09IwUs7cxXA/m/_JHOzNjXBAAJ",
956+
"1.22": "https://groups.google.com/g/golang-dev/c/FNPk2joOsXs/m/roCc_a4fBAAJ",
953957
}
954958
if url, ok := openTreeURLs[nextMajor]; !ok {
955959
return fmt.Errorf("openTreeURLs[%q] is missing a value, please fill it in", nextMajor)
956960
} else if !strings.HasPrefix(url, "https://groups.google.com/g/golang-dev/c/") {
957961
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)
958962
}
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+
}
959969

960-
return b.foreachIssue(b.gorepo, open, func(gi *maintner.GitHubIssue) error {
961-
if !gi.HasLabelID(earlyInCycleID) || gi.Milestone.Title != "Go"+nextMajor {
962-
return nil
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\")"`
9631002
}
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.
9641025
if *dryRun {
965-
log.Printf("[dry run] would ping early-in-cycle issue %d", gi.Number)
966-
return nil
1026+
log.Printf("[dry run] would ping early-in-cycle issue\n\t#%d %s", i.Number, i.Title)
1027+
continue
9671028
}
968-
log.Printf("pinging early-in-cycle issue %d", gi.Number)
1029+
log.Printf("pinging early-in-cycle issue %d (%.32s…)", i.Number, i.Title)
9691030
time.Sleep(3 * time.Second) // Take a moment between pinging issues, since a human will be running this task manually.
970-
msg := fmt.Sprintf("This issue is currently labeled as early-in-cycle for Go %s.\n"+
971-
"That [time is now](%s), so a friendly reminder to look at it again.", nextMajor, openTreeURLs[nextMajor])
972-
return b.addGitHubComment(ctx, b.gorepo, gi.Number, msg)
973-
})
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
9741047
}
9751048

9761049
// freezeOldIssues locks any issue that's old and closed.

0 commit comments

Comments
 (0)