// Munge is the workhorse the will actually make updates to the PR func (r *RebuildMunger) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } comments, err := obj.ListComments() if err != nil { glog.Errorf("unexpected error getting comments: %v", err) } for ix := range comments { comment := comments[ix] // Skip all robot comments if r.robots.Has(*comment.User.Login) { glog.V(4).Infof("Skipping comment by robot %s: %s", *comment.User.Login, *comment.Body) continue } if isRebuildComment(comment) && rebuildCommentMissingIssueNumber(comment) { if err := obj.DeleteComment(comment); err != nil { glog.Errorf("Error deleting comment: %v", err) continue } body := fmt.Sprintf(rebuildFormat, *comment.User.Login) err := obj.WriteComment(body) if err != nil { glog.Errorf("unexpected error adding comment: %v", err) continue } } } }
// Munge is the workhorse the will actually make updates to the PR func (CommentDeleter) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } comments, err := obj.ListComments() if err != nil { return } for i := range comments { comment := &comments[i] if comment.User == nil || comment.User.Login == nil { continue } if *comment.User.Login != botName { continue } if comment.Body == nil { continue } for _, f := range funcs { if f(obj, comment) { obj.DeleteComment(comment) break } } } }
// Munge is the workhorse that will actually close the PRs func (CloseStalePR) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } if obj.HasLabel(keepOpenLabel) { return } lastModif, err := findLastModificationTime(obj) if err != nil { glog.Errorf("Failed to find last modification: %v", err) return } closeIn := -time.Since(lastModif.Add(stalePullRequest)) inactiveFor := time.Since(*lastModif) if closeIn <= 0 { closePullRequest(obj, inactiveFor) } else if closeIn <= startWarning { checkAndWarn(obj, inactiveFor, closeIn) } else { // Pull-request is active. Remove previous potential warning // Ignore potential errors, we just want to remove old comments ... comment, _ := findLatestWarningComment(obj) if comment != nil { obj.DeleteComment(comment) } } }
// MungeIssue is the real worker. It is called for every open github Issue // But note that all PRs are Issues. (Not all Issues are PRs.) This particular // function ignores all issues that are not PRs and prints the number for the // PRs. func MungeIssue(obj *github.MungeObject) error { if !obj.IsPR() { return nil } glog.Infof("PR: %d", *obj.Issue.Number) return nil }
// Munge is the workhorse the will actually make updates to the PR func (c *CherrypickQueue) Munge(obj *github.MungeObject) { if !obj.HasLabel(cpCandidateLabel) { return } if !obj.IsPR() { return } // This will cache the PR and events so when we try to view the queue we don't // hit github while trying to load the page obj.GetPR() num := *obj.Issue.Number c.Lock() merged, _ := obj.IsMerged() if merged { if obj.HasLabel(cpApprovedLabel) { c.mergedAndApproved[num] = obj } else { c.merged[num] = obj } } else { c.unmerged[num] = obj } c.Unlock() return }
// Munge is the workhorse the will actually make updates to the PR func (p *PathLabelMunger) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } commits, err := obj.GetCommits() if err != nil { return } needsLabels := sets.NewString() for _, c := range commits { for _, f := range c.Files { for _, lm := range p.labelMap { if lm.regexp.MatchString(*f.Filename) { if !obj.HasLabel(lm.label) { needsLabels.Insert(lm.label) } } } } } if needsLabels.Len() != 0 { obj.AddLabels(needsLabels.List()) } }
// Munge is the workhorse the will actually make updates to the PR func (b *BlockPath) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } if obj.HasLabel(doNotMergeLabel) { return } commits, err := obj.GetCommits() if err != nil { return } for _, c := range commits { for _, f := range c.Files { if matchesAny(*f.Filename, b.blockRegexp) { if matchesAny(*f.Filename, b.doNotBlockRegexp) { continue } body := fmt.Sprintf(`Adding label:%s because PR changes docs prohibited to auto merge See http://kubernetes.io/editdocs/ for information about editing docs`, doNotMergeLabel) obj.WriteComment(body) obj.AddLabels([]string{doNotMergeLabel}) return } } } }
// Munge is the workhorse the will actually make updates to the PR // The algorithm goes as: // - Initially, we build an approverSet // - Go through all comments after latest commit. // - If anyone said "/approve", add them to approverSet. // - Then, for each file, we see if any approver of this file is in approverSet and keep track of files without approval // - An approver of a file is defined as: // - Someone listed as an "approver" in an OWNERS file in the files directory OR // - in one of the file's parent directorie // - Iff all files have been approved, the bot will add the "approved" label. // - Iff a cancel command is found, that reviewer will be removed from the approverSet // and the munger will remove the approved label if it has been applied func (h *ApprovalHandler) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } files, err := obj.ListFiles() if err != nil { glog.Errorf("failed to list files in this PR: %v", err) return } comments, err := getCommentsAfterLastModified(obj) if err != nil { glog.Errorf("failed to get comments in this PR: %v", err) return } ownersMap := h.getApprovedOwners(files, createApproverSet(comments)) if err := h.updateNotification(obj, ownersMap); err != nil { return } for _, approverSet := range ownersMap { if approverSet.Len() == 0 { if obj.HasLabel(approvedLabel) { obj.RemoveLabel(approvedLabel) } return } } if !obj.HasLabel(approvedLabel) { obj.AddLabel(approvedLabel) } }
// Munge is the workhorse the will actually make updates to the PR func (h LGTMHandler) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } reviewers := getReviewers(obj) if len(reviewers) == 0 { return } comments, err := getCommentsAfterLastModified(obj) if err != nil { glog.Errorf("unexpected error getting comments: %v", err) return } events, err := obj.GetEvents() if err != nil { glog.Errorf("unexpected error getting events: %v", err) return } if !obj.HasLabel(lgtmLabel) { h.addLGTMIfCommented(obj, comments, events, reviewers) return } h.removeLGTMIfCancelled(obj, comments, events, reviewers) }
// Munge is the workhorse the will actually make updates to the PR func (NagFlakeIssues) Munge(obj *mgh.MungeObject) { if obj.IsPR() || !obj.HasLabel("kind/flake") { return } comments, err := obj.ListComments() if err != nil { glog.Error(err) return } // Use the pinger to notify assignees: // - Set time period based on configuration (at the top of this file) // - Mention list of assignees as an argument // - Start the ping timer after the last HumanActor comment // How often should we ping period := findTimePeriod(obj.Issue.Labels) // Who are we pinging who := mungerutil.GetIssueUsers(obj.Issue).Assignees.Mention().Join() // When does the pinger start startDate := c.LastComment(comments, c.HumanActor(), obj.Issue.CreatedAt) // Get a notification if it's time to ping. notif := pinger.SetTimePeriod(period).PingNotification( comments, who, startDate, ) if notif != nil { obj.WriteComment(notif.String()) } }
// Munge is the workhorse the will actually make updates to the PR func (LGTMAfterCommitMunger) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } if !obj.HasLabel("lgtm") { return } lastModified := obj.LastModifiedTime() lgtmTime := obj.LabelTime("lgtm") if lastModified == nil || lgtmTime == nil { glog.Errorf("PR %d unable to determine lastModified or lgtmTime", *obj.Issue.Number) return } if lastModified.After(*lgtmTime) { glog.Infof("PR: %d lgtm:%s lastModified:%s", *obj.Issue.Number, lgtmTime.String(), lastModified.String()) lgtmRemovedBody := "PR changed after LGTM, removing LGTM." if err := obj.WriteComment(lgtmRemovedBody); err != nil { return } obj.RemoveLabel("lgtm") } }
// Munge is the workhorse the will actually make updates to the PR func (a *AssignFixesMunger) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } // we need the PR for the "User" (creator of the PR not the assignee) pr, err := obj.GetPR() if err != nil { glog.Infof("Couldn't get PR %v", obj.Issue.Number) return } prOwner := github.DescribeUser(pr.User) issuesFixed := obj.GetPRFixesList() if issuesFixed == nil { return } for _, fixesNum := range issuesFixed { // "issue" is the issue referenced by the "fixes #<num>" issueObj, err := a.config.GetObject(fixesNum) if err != nil { glog.Infof("Couldn't get issue %v", fixesNum) continue } issue := issueObj.Issue if !a.AssignfixesReassign && issue.Assignee != nil { glog.V(6).Infof("skipping %v: reassign: %v assignee: %v", *issue.Number, a.AssignfixesReassign, github.DescribeUser(issue.Assignee)) continue } glog.Infof("Assigning %v to %v (previously assigned to %v)", *issue.Number, prOwner, github.DescribeUser(issue.Assignee)) // although it says "AssignPR" it's more generic than that and is really just an issue. issueObj.AssignPR(prOwner) } }
// Munge is the workhorse the will actually make updates to the PR func (LGTMAfterCommitMunger) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } if !obj.HasLabel(lgtmLabel) { return } lastModified := obj.LastModifiedTime() lgtmTime := obj.LabelTime(lgtmLabel) if lastModified == nil || lgtmTime == nil { glog.Errorf("PR %d unable to determine lastModified or lgtmTime", *obj.Issue.Number) return } if lastModified.After(*lgtmTime) { glog.Infof("PR: %d lgtm:%s lastModified:%s", *obj.Issue.Number, lgtmTime.String(), lastModified.String()) body := fmt.Sprintf(lgtmRemovedBody, mungerutil.GetIssueUsers(obj.Issue).AllUsers().Mention().Join()) if err := obj.WriteComment(body); err != nil { return } obj.RemoveLabel(lgtmLabel) } }
// Munge is the workhorse the will actually make updates to the PR func (b *BlockPath) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } if obj.HasLabel(doNotMergeLabel) { return } files, err := obj.ListFiles() if err != nil { return } for _, f := range files { if matchesAny(*f.Filename, b.blockRegexp) { if matchesAny(*f.Filename, b.doNotBlockRegexp) { continue } obj.WriteComment(blockPathBody) obj.AddLabels([]string{doNotMergeLabel}) return } } }
// Munge is the workhorse the will actually make updates to the PR func (PingCIMunger) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } if !obj.HasLabel("lgtm") { return } mergeable, err := obj.IsMergeable() if err != nil { glog.V(2).Infof("Skipping %d - problem determining mergeability", *obj.Issue.Number) return } if !mergeable { glog.V(2).Infof("Skipping %d - not mergeable", *obj.Issue.Number) return } if state := obj.GetStatusState([]string{jenkinsCIContext, travisContext}); state != "incomplete" { glog.V(2).Info("Have %s status - skipping ping CI", jenkinsCIContext) return } state := obj.GetStatusState([]string{shippableContext, travisContext}) if state == "incomplete" { msg := "Continuous integration appears to have missed, closing and re-opening to trigger it" obj.WriteComment(msg) obj.ClosePR() time.Sleep(5 * time.Second) obj.OpenPR(10) } }
// Munge is the workhorse the will actually make updates to the PR func (h AssignUnassignHandler) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } comments, err := obj.ListComments() if err != nil { glog.Errorf("unexpected error getting comments: %v", err) return } fileList, err := obj.ListFiles() if err != nil { glog.Errorf("Could not list the files for PR %v: %v", obj.Issue.Number, err) return } //get ALL (not just leaf) the people that could potentially own the file based on the blunderbuss.go implementation potentialOwners, _ := getPotentialOwners(*obj.Issue.User.Login, h.features, fileList, false) toAssign, toUnassign := h.getAssigneesAndUnassignees(obj, comments, fileList, potentialOwners) for _, username := range toAssign.List() { obj.AssignPR(username) } obj.UnassignPR(toUnassign.List()...) }
// Munge is the workhorse the will actually make updates to the PR func (CommentDeleter) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } comments, err := obj.ListComments() if err != nil { return } validComments := []githubapi.IssueComment{} for i := range comments { comment := comments[i] if !validComment(comment) { continue } validComments = append(validComments, comment) } for _, d := range deleters { stale := d.StaleComments(obj, validComments) for _, comment := range stale { obj.DeleteComment(&comment) } } }
// Munge is the workhorse the will actually make updates to the PR func (p *PathLabelMunger) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } files, err := obj.ListFiles() if err != nil { return } needsLabels := sets.NewString() for _, f := range files { for _, lm := range p.labelMap { if lm.regexp.MatchString(*f.Filename) { needsLabels.Insert(lm.label) } } } // This is all labels on the issue that the path munger controls hasLabels := obj.LabelSet().Intersection(p.allLabels) missingLabels := needsLabels.Difference(hasLabels) if missingLabels.Len() != 0 { obj.AddLabels(needsLabels.List()) } extraLabels := hasLabels.Difference(needsLabels) for _, label := range extraLabels.List() { creator := obj.LabelCreator(label) if creator == botName { obj.RemoveLabel(label) } } }
// Munge is the workhorse the will actually make updates to the PR func (r *ReleaseNoteLabel) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } if completedReleaseNoteProcess(obj) { r.ensureNoRelNoteNeededLabel(obj) return } if !r.prMustFollowRelNoteProcess(obj) { r.ensureNoRelNoteNeededLabel(obj) return } if !obj.HasLabel(releaseNoteLabelNeeded) { obj.AddLabel(releaseNoteLabelNeeded) } if !obj.HasLabel(lgtmLabel) { return } obj.WriteComment(releaseNoteBody) obj.RemoveLabel(lgtmLabel) }
// Munge is the workhorse the will actually make updates to the PR func (StaleGreenCI) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } if !obj.HasLabel(lgtmLabel) { return } if mergeable, err := obj.IsMergeable(); !mergeable || err != nil { return } if !obj.IsStatusSuccess(requiredContexts) { return } for _, context := range requiredContexts { statusTime := obj.GetStatusTime(context) if statusTime == nil { glog.Errorf("%d: unable to determine time %q context was set", *obj.Issue.Number, context) return } if time.Since(*statusTime) > staleGreenCIHours*time.Hour { obj.WriteComment(greenMsgBody) err := obj.WaitForPending(requiredContexts) if err != nil { glog.Errorf("Failed waiting for PR to start testing: %v", err) } return } } }
// Munge is the workhorse the will actually make updates to the PR func (StalePendingCI) Munge(obj *github.MungeObject) { requiredContexts := []string{jenkinsUnitContext, jenkinsE2EContext} if !obj.IsPR() { return } if !obj.HasLabel(lgtmLabel) { return } if mergeable, err := obj.IsMergeable(); !mergeable || err != nil { return } status := obj.GetStatusState(requiredContexts) if status != "pending" { return } for _, context := range requiredContexts { statusTime := obj.GetStatusTime(context) if statusTime == nil { glog.Errorf("%d: unable to determine time %q context was set", *obj.Issue.Number, context) return } if time.Since(*statusTime) > stalePendingCIHours*time.Hour { obj.WriteComment(pendingMsgBody) return } } }
// Munge is the workhorse that will actually close the PRs func (CloseStalePR) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } if obj.HasLabel(keepOpenLabel) { return } lastModif, err := findLastModificationTime(obj) if err != nil { glog.Errorf("Failed to find last modification: %v", err) return } closeIn := -time.Since(lastModif.Add(stalePullRequest)) inactiveFor := time.Since(*lastModif) if closeIn <= 0 { closePullRequest(obj, inactiveFor) } else if closeIn <= startWarning { checkAndWarn(obj, inactiveFor, closeIn) } else { // Pull-request is active. Do nothing } }
// Munge is the workhorse the will actually make updates to the PR func (p *PathLabelMunger) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } commits, err := obj.GetCommits() if err != nil { return } labelMap := *p.labelMap needsLabels := sets.NewString() for _, c := range commits { for _, f := range c.Files { for prefix, label := range labelMap { if strings.HasPrefix(*f.Filename, prefix) && !obj.HasLabel(label) { needsLabels.Insert(label) } } } } if needsLabels.Len() != 0 { obj.AddLabels(needsLabels.List()) } }
// Munge is the workhorse the will actually make updates to the PR func (s *SizeMunger) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } issue := obj.Issue s.getGeneratedFiles(obj) genFiles := *s.genFiles genPrefixes := *s.genPrefixes files, err := obj.ListFiles() if err != nil { return } adds := 0 dels := 0 for _, f := range files { skip := false for _, p := range genPrefixes { if strings.HasPrefix(*f.Filename, p) { skip = true break } } if skip { continue } if genFiles.Has(*f.Filename) { continue } if f.Additions != nil { adds += *f.Additions } if f.Deletions != nil { dels += *f.Deletions } } newSize := calculateSize(adds, dels) newLabel := labelSizePrefix + newSize existing := github.GetLabelsWithPrefix(issue.Labels, labelSizePrefix) needsUpdate := true for _, l := range existing { if l == newLabel { needsUpdate = false continue } obj.RemoveLabel(l) } if needsUpdate { obj.AddLabels([]string{newLabel}) body := fmt.Sprintf("Labelling this PR as %s", newLabel) obj.WriteComment(body) } }
// Munge is unused by this munger. func (cla *ClaMunger) Munge(obj *githubhelper.MungeObject) { if !obj.IsPR() { return } if obj.HasLabel(claHumanLabel) { return } status := obj.GetStatusState([]string{cla.CLAStatusContext}) // Check for pending status and exit. if status == contextPending { // do nothing and wait for state to be updated. return } if status == contextSuccess { if obj.HasLabel(cncfClaYesLabel) { // status is success and we've already applied 'cncf-cla: yes'. return } if obj.HasLabel(cncfClaNoLabel) { obj.RemoveLabel(cncfClaNoLabel) } obj.AddLabel(cncfClaYesLabel) return } // If we are here, that means that the context is failure/error. comments, err := obj.ListComments() if err != nil { glog.Error(err) return } who := mungerutil.GetIssueUsers(obj.Issue).Author.Mention().Join() // Get a notification if it's time to ping. notif := cla.pinger.PingNotification( comments, who, nil, ) if notif != nil { obj.WriteComment(notif.String()) } if obj.HasLabel(cncfClaNoLabel) { // status reported error/failure and we've already applied 'cncf-cla: no' label. return } if obj.HasLabel(cncfClaYesLabel) { obj.RemoveLabel(cncfClaYesLabel) } obj.AddLabel(cncfClaNoLabel) }
// Munge is the workhorse the will actually make updates to the PR func (c *ClearPickAfterMerge) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } if !obj.HasLabel(cpCandidateLabel) { return } if merged, err := obj.IsMerged(); !merged || err != nil { return } releaseMilestone := obj.ReleaseMilestone() if releaseMilestone == "" || len(releaseMilestone) != 4 { glog.Errorf("Found invalid milestone: %q", releaseMilestone) return } rel := releaseMilestone[1:] branch := "release-" + rel sha := obj.MergeCommit() if sha == nil { glog.Errorf("Unable to get SHA of merged %d", sha) return } logMsg := fmt.Sprintf("Merge pull request #%d from ", *obj.Issue.Number) bLogMsg := []byte(logMsg) cherrypickMsg := fmt.Sprintf("(cherry picked from commit %s)", *sha) args := []string{"log", "--pretty=tformat:%H%n%s%n%b", "--grep", cherrypickMsg, "origin/" + branch} out, err := c.features.Repos.GitCommand(args) if err != nil { glog.Errorf("Error grepping for cherrypick -x message out=%q: %v", string(out), err) return } if bytes.Contains(out, bLogMsg) { glog.Infof("Found cherry-pick using -x information") handleFound(obj, out, branch) return } args = []string{"log", "--pretty=tformat:%H%n%s%n%b", "--grep", logMsg, "origin/" + branch} out, err = c.features.Repos.GitCommand(args) if err != nil { glog.Errorf("Error grepping for log message out=%q: %v", string(out), err) return } if bytes.Contains(out, bLogMsg) { glog.Infof("Found cherry-pick using log matching") handleFound(obj, out, branch) return } return }
// Munge is the workhorse the will actually make updates to the PR func (c *CherrypickAutoApprove) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } if obj.IsForBranch("master") { return } if obj.HasLabel(cpApprovedLabel) && obj.ReleaseMilestone() != "" { return } parents := getCherrypickParentPRs(obj, c.config) if len(parents) == 0 { return } major := 0 minor := 0 branch := obj.Branch() if l, err := fmt.Sscanf(branch, "release-%d.%d", &major, &minor); err != nil || l != 2 { return } branchImpliedMilestone := fmt.Sprintf("v%d.%d", major, minor) milestone := obj.ReleaseMilestone() if milestone != "" && milestone != branchImpliedMilestone { glog.Errorf("Found PR %d on branch %q but have milestone %q", *obj.Issue.Number, branch, milestone) return } for _, parent := range parents { if !parent.HasLabel(cpApprovedLabel) { return } // If the parent was for milestone v1.2 but this PR has // comments saying it was 'on branch release-1.1' we should // not auto approve parentMilestone := parent.ReleaseMilestone() if parentMilestone != branchImpliedMilestone { glog.Errorf("%d: parentReleaseMilestone=%q but branch is %q", *obj.Issue.Number, parentMilestone, obj.Branch()) return } } if milestone == "" { obj.SetMilestone(branchImpliedMilestone) } if !obj.HasLabel(cpApprovedLabel) { obj.AddLabel(cpApprovedLabel) } }
// Munge is the workhorse the will actually make updates to the PR func (OkToTestMunger) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } if !obj.HasLabel(lgtmLabel) { return } state := obj.GetStatusState(requiredContexts) if state == "incomplete" { glog.V(2).Infof("status is incomplete, adding ok to test") obj.WriteComment(okToTestBody) } }
// Munge is the workhorse the will actually make updates to the PR func (p *IssueCacher) Munge(obj *github.MungeObject) { if obj.IsPR() { return } if !obj.HasLabels(p.labelFilter.List()) { return } key, ok := p.keyFromIssue(obj) if !ok { return } p.addNumberToKey(key, *obj.Issue.Number) }
// ComputeState can be used to compute the state of each PR. func ComputeState(obj *github.MungeObject) error { if !obj.IsPR() { return nil } // Every PR starts in the pre-review state. var currentState State = &PreReview{} for currentState.Name() != endState { var err error currentState, err = currentState.Process(obj) if err != nil { return err } } return nil }