blob: 177b05f52481ced458b18f0fbe442b4d3c16e2bc [file] [log] [blame]
package autoroll
/*
Convenience functions for retrieving AutoRoll CLs.
*/
import (
"errors"
"fmt"
"regexp"
"sort"
"time"
github_api "github.com/google/go-github/github"
"go.skia.org/infra/go/buildbucket"
"go.skia.org/infra/go/comment"
"go.skia.org/infra/go/gerrit"
"go.skia.org/infra/go/github"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
)
const (
AUTOROLL_STATUS_URL = "https://autoroll.skia.org/json/status"
POLLER_ROLLS_LIMIT = 10
RECENT_ROLLS_LIMIT = 200
ROLL_RESULT_DRY_RUN_SUCCESS = "dry run succeeded"
ROLL_RESULT_DRY_RUN_FAILURE = "dry run failed"
ROLL_RESULT_DRY_RUN_IN_PROGRESS = "dry run in progress"
ROLL_RESULT_IN_PROGRESS = "in progress"
ROLL_RESULT_SUCCESS = "succeeded"
ROLL_RESULT_FAILURE = "failed"
TRYBOT_CATEGORY_CQ = "cq"
TRYBOT_STATUS_STARTED = "STARTED"
TRYBOT_STATUS_COMPLETED = "COMPLETED"
TRYBOT_STATUS_SCHEDULED = "SCHEDULED"
TRYBOT_RESULT_CANCELED = "CANCELED"
TRYBOT_RESULT_SUCCESS = "SUCCESS"
TRYBOT_RESULT_FAILURE = "FAILURE"
)
var (
// "RESTRICT AUTOMERGE: " is from skbug.com/8998
ROLL_REV_REGEX = regexp.MustCompile(`^(?:(?:\[\S+\] )|(?:RESTRICT AUTOMERGE: ))?Roll \S+(?:\s+\S+)* (?:from )?(\S+)(?:(?:\.\.)|(?: to ))(\S+)(?: \(\d+ commit.*\))?\.?`)
OPEN_ROLL_VALID_RESULTS = []string{
ROLL_RESULT_DRY_RUN_FAILURE,
ROLL_RESULT_DRY_RUN_IN_PROGRESS,
ROLL_RESULT_DRY_RUN_SUCCESS,
ROLL_RESULT_IN_PROGRESS,
}
DRY_RUN_RESULTS = []string{
ROLL_RESULT_DRY_RUN_FAILURE,
ROLL_RESULT_DRY_RUN_IN_PROGRESS,
ROLL_RESULT_DRY_RUN_SUCCESS,
}
FAILURE_RESULTS = []string{
ROLL_RESULT_DRY_RUN_FAILURE,
ROLL_RESULT_FAILURE,
}
SUCCESS_RESULTS = []string{
ROLL_RESULT_DRY_RUN_SUCCESS,
ROLL_RESULT_SUCCESS,
}
)
// AutoRollIssue is a struct containing the information we care about for
// AutoRoll CLs.
type AutoRollIssue struct {
Closed bool `json:"closed"`
Comments []*comment.Comment `json:"comments"`
Committed bool `json:"committed"`
Created time.Time `json:"created"`
IsDryRun bool `json:"isDryRun"`
DryRunFinished bool `json:"dryRunFinished"`
DryRunSuccess bool `json:"dryRunSuccess"`
CqFinished bool `json:"cqFinished"`
CqSuccess bool `json:"cqSuccess"`
Issue int64 `json:"issue"`
Modified time.Time `json:"modified"`
Patchsets []int64 `json:"patchSets"`
Result string `json:"result"`
RollingFrom string `json:"rollingFrom"`
RollingTo string `json:"rollingTo"`
Subject string `json:"subject"`
TryResults []*TryResult `json:"tryResults"`
}
// Validate returns an error iff there is some problem with the issue.
func (i *AutoRollIssue) Validate() error {
if i.Closed {
if i.Result == ROLL_RESULT_DRY_RUN_IN_PROGRESS || i.Result == ROLL_RESULT_IN_PROGRESS {
return fmt.Errorf("AutoRollIssue cannot have a Result of %q if it is Closed.", i.Result)
}
} else {
if i.Committed {
return errors.New("AutoRollIssue cannot be Committed without being Closed.")
}
}
if i.CqFinished && i.IsDryRun {
return errors.New("CqFinished cannot be true for dry runs.")
}
if i.DryRunFinished && !i.IsDryRun {
return errors.New("DryRunFinished cannot be true unless the roll is a dry run.")
}
if i.CqSuccess && !i.CqFinished {
return errors.New("CqSuccess cannot be true if CqFinished is not true.")
}
if i.DryRunSuccess && !i.DryRunFinished {
return errors.New("DryRunSuccess cannot be true if DryRunFinished is not true.")
}
return nil
}
// Copy returns a copy of the AutoRollIssue.
func (i *AutoRollIssue) Copy() *AutoRollIssue {
var commentsCpy []*comment.Comment
if i.Comments != nil {
commentsCpy = make([]*comment.Comment, 0, len(i.Comments))
for _, c := range i.Comments {
commentsCpy = append(commentsCpy, c.Copy())
}
}
var patchsetsCpy []int64
if i.Patchsets != nil {
patchsetsCpy = make([]int64, len(i.Patchsets))
copy(patchsetsCpy, i.Patchsets)
}
var tryResultsCpy []*TryResult
if i.TryResults != nil {
tryResultsCpy = make([]*TryResult, 0, len(i.TryResults))
for _, t := range i.TryResults {
tryResultsCpy = append(tryResultsCpy, t.Copy())
}
}
return &AutoRollIssue{
Closed: i.Closed,
Comments: commentsCpy,
Committed: i.Committed,
Created: i.Created,
CqFinished: i.CqFinished,
CqSuccess: i.CqSuccess,
DryRunFinished: i.DryRunFinished,
DryRunSuccess: i.DryRunSuccess,
IsDryRun: i.IsDryRun,
Issue: i.Issue,
Modified: i.Modified,
Patchsets: patchsetsCpy,
Result: i.Result,
RollingFrom: i.RollingFrom,
RollingTo: i.RollingTo,
Subject: i.Subject,
TryResults: tryResultsCpy,
}
}
// UpdateFromGitHubPullRequest updates the AutoRollIssue instance based on the
// given PullRequest. If an error is returned, the AutoRollIssue is not changed.
func (i *AutoRollIssue) UpdateFromGitHubPullRequest(pullRequest *github_api.PullRequest) error {
prNum := int64(pullRequest.GetNumber())
if i.Issue == 0 {
i.Issue = prNum
} else if i.Issue != prNum {
return fmt.Errorf("Pull request number %d differs from existing issue number %d!", prNum, i.Issue)
}
if i.IsDryRun {
i.CqFinished = false
i.CqSuccess = false
i.DryRunFinished = i.AllTrybotsFinished() || pullRequest.GetState() == github.CLOSED_STATE || pullRequest.GetMerged()
i.DryRunSuccess = (i.DryRunFinished && i.AllTrybotsSucceeded()) || pullRequest.GetMerged()
} else {
i.CqFinished = pullRequest.GetState() == github.CLOSED_STATE || pullRequest.GetMerged()
i.CqSuccess = pullRequest.GetMerged()
i.DryRunFinished = false
i.DryRunSuccess = false
}
ps := make([]int64, 0, *pullRequest.Commits)
for i := 1; i <= *pullRequest.Commits; i++ {
ps = append(ps, int64(i))
}
i.Closed = pullRequest.GetState() == github.CLOSED_STATE
i.Committed = pullRequest.GetMerged()
i.Created = pullRequest.GetCreatedAt()
i.Modified = pullRequest.GetUpdatedAt()
i.Patchsets = ps
i.Subject = pullRequest.GetTitle()
i.Result = rollResult(i)
return i.Validate()
}
// UpdateFromGerritChangeInfo updates the AutoRollIssue instance based on the
// given gerrit.ChangeInfo. If an error is returned, the AutoRollIssue is not
// changed.
func (i *AutoRollIssue) UpdateFromGerritChangeInfo(ci *gerrit.ChangeInfo, rollIntoAndroid bool) error {
if i.Issue == 0 {
i.Issue = ci.Issue
} else if i.Issue != ci.Issue {
return fmt.Errorf("CL ID %d differs from existing issue number %d!", ci.Issue, i.Issue)
}
cqFinished := false
dryRunFinished := false
dryRunSuccess := false
if rollIntoAndroid {
if _, ok := ci.Labels[gerrit.PRESUBMIT_VERIFIED_LABEL]; ok {
for _, lb := range ci.Labels[gerrit.PRESUBMIT_VERIFIED_LABEL].All {
if lb.Value == gerrit.PRESUBMIT_VERIFIED_LABEL_REJECTED {
cqFinished = true
dryRunFinished = true
break
} else if lb.Value == gerrit.PRESUBMIT_VERIFIED_LABEL_ACCEPTED {
// Not marking cqSuccess or cqFinished
// true here; those are only true if the
// change is merged.
dryRunFinished = true
dryRunSuccess = true
}
}
}
} else {
foundCqLabel := false
foundDryRunLabel := false
if _, ok := ci.Labels[gerrit.COMMITQUEUE_LABEL]; ok {
for _, lb := range ci.Labels[gerrit.COMMITQUEUE_LABEL].All {
if lb.Value == gerrit.COMMITQUEUE_LABEL_DRY_RUN {
foundDryRunLabel = true
} else if lb.Value == gerrit.COMMITQUEUE_LABEL_SUBMIT {
foundCqLabel = true
}
}
}
if !foundCqLabel {
cqFinished = true
}
if !foundDryRunLabel {
dryRunFinished = true
}
if i.IsDryRun && dryRunFinished {
dryRunSuccess = i.AllTrybotsSucceeded()
}
}
if i.IsDryRun {
i.CqFinished = false
i.CqSuccess = false
i.DryRunFinished = dryRunFinished || ci.IsClosed()
i.DryRunSuccess = dryRunSuccess || ci.Status == gerrit.CHANGE_STATUS_MERGED
} else {
i.CqFinished = ci.IsClosed() || cqFinished
i.CqSuccess = ci.Status == gerrit.CHANGE_STATUS_MERGED
i.DryRunFinished = false
i.DryRunSuccess = false
}
ps := make([]int64, 0, len(ci.Patchsets))
for _, p := range ci.Patchsets {
ps = append(ps, p.Number)
}
i.Closed = ci.IsClosed()
i.Committed = ci.Committed
i.Created = ci.Created
i.Modified = ci.Updated
i.Patchsets = ps
i.Subject = ci.Subject
i.Result = rollResult(i)
return i.Validate()
}
// rollResult derives a result string for the roll.
func rollResult(roll *AutoRollIssue) string {
if roll.IsDryRun {
if roll.DryRunFinished {
if roll.DryRunSuccess {
return ROLL_RESULT_DRY_RUN_SUCCESS
} else {
return ROLL_RESULT_DRY_RUN_FAILURE
}
} else {
return ROLL_RESULT_DRY_RUN_IN_PROGRESS
}
}
if roll.CqFinished {
if roll.CqSuccess {
return ROLL_RESULT_SUCCESS
} else {
return ROLL_RESULT_FAILURE
}
}
return ROLL_RESULT_IN_PROGRESS
}
// AllTrybotsFinished returns true iff all CQ trybots have finished for the
// given issue.
func (a *AutoRollIssue) AllTrybotsFinished() bool {
for _, t := range a.TryResults {
if t.Category != TRYBOT_CATEGORY_CQ {
continue
}
if !t.Finished() {
return false
}
}
return true
}
// AtleastOneTrybotFailure returns true iff there is atleast one trybot that has
// failed for the given issue.
func (a *AutoRollIssue) AtleastOneTrybotFailure() bool {
// For each trybot, find the most recent result.
bots := map[string]*TryResult{}
for _, t := range a.TryResults {
if prev, ok := bots[t.Builder]; !ok || prev.Created.Before(t.Created) {
bots[t.Builder] = t
}
}
for _, t := range bots {
sklog.Infof(" %s: %s (%s)", t.Builder, t.Result, t.Category)
if t.Category != TRYBOT_CATEGORY_CQ {
continue
}
if t.Failed() {
return true
}
}
return false
}
// AllTrybotsSucceeded returns true iff all CQ trybots have succeeded for the
// given issue. Note that some trybots may fail and be retried, in which case a
// successful retry counts as a success.
func (a *AutoRollIssue) AllTrybotsSucceeded() bool {
// For each trybot, find the most recent result.
bots := map[string]*TryResult{}
for _, t := range a.TryResults {
if prev, ok := bots[t.Builder]; !ok || prev.Created.Before(t.Created) {
bots[t.Builder] = t
}
}
sklog.Infof("AllTrybotsSucceeded? %d results.", len(bots))
for _, t := range bots {
sklog.Infof(" %s: %s (%s)", t.Builder, t.Result, t.Category)
if t.Category != TRYBOT_CATEGORY_CQ {
sklog.Infof(" ...skipping, not a CQ bot (category %q not %q)", t.Category, TRYBOT_CATEGORY_CQ)
continue
}
if !t.Succeeded() {
sklog.Infof(" ...failed")
return false
}
}
return true
}
// Failed returns true iff the roll failed (including dry run failure).
func (a *AutoRollIssue) Failed() bool {
return util.In(a.Result, FAILURE_RESULTS)
}
// Succeeded returns true iff the roll succeeded (including dry run success).
func (a *AutoRollIssue) Succeeded() bool {
return util.In(a.Result, SUCCESS_RESULTS)
}
// TryResult is a struct which contains trybot result details.
type TryResult struct {
Builder string `json:"builder"`
Category string `json:"category"`
Created time.Time `json:"created_ts"`
Result string `json:"result"`
Status string `json:"status"`
Url string `json:"url"`
}
// TryResultFromBuildbucket returns a new TryResult based on a buildbucketpb.Build.
func TryResultFromBuildbucket(b *buildbucket.Build) (*TryResult, error) {
return &TryResult{
Builder: b.Parameters.BuilderName,
Category: b.Parameters.Properties.Category,
Created: time.Time(b.Created),
Result: b.Result,
Status: b.Status,
Url: b.Url,
}, nil
}
// TryResultsFromBuildbucket returns a slice of TryResults based on a slice of
// buildbucket.Builds.
func TryResultsFromBuildbucket(tries []*buildbucket.Build) ([]*TryResult, error) {
res := make([]*TryResult, 0, len(tries))
for _, t := range tries {
tryResult, err := TryResultFromBuildbucket(t)
if err != nil {
return nil, err
}
res = append(res, tryResult)
}
sort.Sort(tryResultSlice(res))
return res, nil
}
// Finished returns true iff the trybot is done running.
func (t TryResult) Finished() bool {
return t.Status == TRYBOT_STATUS_COMPLETED
}
// Failed returns true iff the trybot completed and failed.
func (t TryResult) Failed() bool {
return t.Finished() && t.Result == TRYBOT_RESULT_FAILURE
}
// Succeeded returns true iff the trybot completed successfully.
func (t TryResult) Succeeded() bool {
return t.Finished() && t.Result == TRYBOT_RESULT_SUCCESS
}
// Copy returns a copy of the TryResult.
func (t *TryResult) Copy() *TryResult {
return &TryResult{
Builder: t.Builder,
Category: t.Category,
Created: t.Created,
Result: t.Result,
Status: t.Status,
Url: t.Url,
}
}
type autoRollIssueSlice []*AutoRollIssue
func (s autoRollIssueSlice) Len() int { return len(s) }
func (s autoRollIssueSlice) Less(i, j int) bool { return s[i].Modified.After(s[j].Modified) }
func (s autoRollIssueSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
type tryResultSlice []*TryResult
func (s tryResultSlice) Len() int { return len(s) }
func (s tryResultSlice) Less(i, j int) bool { return s[i].Builder < s[j].Builder }
func (s tryResultSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }