tools/issue-labeler/labeler/backfill.go (159 lines of code) (raw):

package labeler import ( "context" "fmt" "net/http" "sort" "strings" "time" "github.com/golang/glog" "github.com/google/go-github/v61/github" ) type Label struct { Name string } type IssueUpdate struct { Number int Labels []string OldLabels []string } func GetIssues(repository, since string) ([]*github.Issue, error) { client := newGitHubClient() owner, repo, err := splitRepository(repository) if err != nil { return nil, fmt.Errorf("invalid repository format: %w", err) } sinceTime, err := time.Parse("2006-01-02", since) // input format YYYY-MM-DD if err != nil { return nil, fmt.Errorf("invalid since time format: %w", err) } opt := &github.IssueListByRepoOptions{ Since: sinceTime, State: "all", Sort: "updated", Direction: "desc", ListOptions: github.ListOptions{ PerPage: 100, }, } var allIssues []*github.Issue ctx := context.Background() issues, resp, err := client.Issues.ListByRepo(ctx, owner, repo, opt) if err != nil { return nil, fmt.Errorf("listing issues: %w", err) } allIssues = append(allIssues, issues...) for { // use link headers instead of page parameter based pagination as // it is not supported for large datasets next := parseNextLink(resp.Response) if next == "" { break } req, err := client.NewRequest("GET", next, nil) if err != nil { return allIssues, err } req.Header.Set("Accept", "application/vnd.github.raw+json") var issues []*github.Issue resp, err = client.Do(ctx, req, &issues) if err != nil { return allIssues, err } allIssues = append(allIssues, issues...) } return allIssues, nil } // parseNextLink finds the next page for a GitHub API request by parsing the previous response's Link header. // https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api?apiVersion=2022-11-28#using-link-headers func parseNextLink(resp *http.Response) string { var next string for _, hdr := range resp.Header.Values("Link") { links := strings.Split(hdr, ",") for _, link := range links { pair := strings.Split(strings.TrimSpace(link), ";") if len(pair) == 2 { if strings.TrimSpace(pair[0]) == `rel="next"` { next = strings.Trim(pair[1], "<> ") } else if strings.TrimSpace(pair[1]) == `rel="next"` { next = strings.Trim(pair[0], "<> ") } if next != "" { break } } } } return next } // ComputeIssueUpdates remains the same as it doesn't interact with GitHub API func ComputeIssueUpdates(issues []*github.Issue, regexpLabels []RegexpLabel) []IssueUpdate { var issueUpdates []IssueUpdate for _, issue := range issues { // Skip pull requests if issue.IsPullRequest() { continue } desired := make(map[string]struct{}) for _, existing := range issue.Labels { desired[*existing.Name] = struct{}{} } _, terraform := desired["service/terraform"] _, linked := desired["forward/linked"] _, exempt := desired["forward/exempt"] _, testfailure := desired["test-failure"] if terraform || exempt { continue } // Decision was made to no longer add new service labels to linked tickets, because it is // more difficult to know which teams have received those tickets and which haven't. // Forwarding a ticket to a different service team should involve removing the old service // label and `linked` label. if linked { continue } var issueUpdate IssueUpdate for label := range desired { issueUpdate.OldLabels = append(issueUpdate.OldLabels, label) } sort.Strings(issueUpdate.OldLabels) affectedResources := ExtractAffectedResources(issue.GetBody()) for _, needed := range ComputeLabels(affectedResources, regexpLabels) { desired[needed] = struct{}{} } if len(desired) > len(issueUpdate.OldLabels) { // Forwarding test failure ticket directly if !linked && !testfailure { issueUpdate.Labels = append(issueUpdate.Labels, "forward/review") } for label := range desired { issueUpdate.Labels = append(issueUpdate.Labels, label) } sort.Strings(issueUpdate.Labels) issueUpdate.Number = issue.GetNumber() if issueUpdate.Number > 0 { issueUpdates = append(issueUpdates, issueUpdate) } } } return issueUpdates } func UpdateIssues(repository string, issueUpdates []IssueUpdate, dryRun bool) error { client := newGitHubClient() owner, repo, err := splitRepository(repository) if err != nil { return fmt.Errorf("invalid repository format: %w", err) } ctx := context.Background() failed := 0 for _, update := range issueUpdates { fmt.Printf("Existing labels: %v\n", update.OldLabels) fmt.Printf("New labels: %v\n", update.Labels) fmt.Printf("Updating issue: https://github.com/%s/issues/%d\n", repository, update.Number) if dryRun { continue } _, _, err := client.Issues.Edit(ctx, owner, repo, int(update.Number), &github.IssueRequest{ Labels: &update.Labels, }) if err != nil { glog.Errorf("Error updating issue %d: %v", update.Number, err) failed++ continue } fmt.Printf("GitHub Issue %s %d updated successfully\n", repository, update.Number) } if failed > 0 { return fmt.Errorf("failed to update %d / %d issues", failed, len(issueUpdates)) } return nil }