| 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.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) |
| } |
| i.CqFinished = pullRequest.GetState() == github.CLOSED_STATE || pullRequest.GetMerged() |
| i.CqSuccess = pullRequest.GetMerged() |
| if i.IsDryRun { |
| i.DryRunFinished = i.AllTrybotsFinished() || pullRequest.GetState() == github.CLOSED_STATE || pullRequest.GetMerged() |
| i.DryRunSuccess = (i.DryRunFinished && i.AllTrybotsSucceeded()) || pullRequest.GetMerged() |
| } else { |
| 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() |
| } |
| } |
| |
| i.CqFinished = ci.IsClosed() |
| i.CqSuccess = ci.Status == gerrit.CHANGE_STATUS_MERGED |
| if i.IsDryRun { |
| i.DryRunFinished = dryRunFinished || ci.IsClosed() |
| i.DryRunSuccess = dryRunSuccess || ci.Status == gerrit.CHANGE_STATUS_MERGED |
| } else { |
| i.CqFinished = i.CqFinished || cqFinished |
| 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] } |