[tools] Add a new tool to auto-submit changes

The change must be authored by the user and requires the following labels:
* Code-Review+2
* Auto-Submit+2
* Kokoro+2

And must not have failed CQ with the latest patchset.

Change-Id: Ic7b76a69a8dd134c11cb1c2a9964ab11d9fdde34
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/133468
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
Auto-Submit: Ben Clayton <bclayton@google.com>
This commit is contained in:
Ben Clayton 2023-05-18 11:30:07 +00:00 committed by Dawn LUCI CQ
parent 84d750e982
commit dededb1e5d
2 changed files with 261 additions and 4 deletions

View File

@ -0,0 +1,216 @@
// Copyright 2023 The Tint Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// auto-submit applies the 'Commit-Queue+2' label to Gerrit changes authored by the user
// that are ready to be submitted
package main
import (
"flag"
"fmt"
"log"
"os"
"os/exec"
"strings"
"time"
"dawn.googlesource.com/dawn/tools/src/dawn"
"dawn.googlesource.com/dawn/tools/src/gerrit"
"dawn.googlesource.com/dawn/tools/src/git"
)
const (
toolName = "auto-submit"
cqEmailAccount = "dawn-scoped@luci-project-accounts.iam.gserviceaccount.com"
)
var (
// See https://dawn-review.googlesource.com/new-password for obtaining
// username and password for gerrit.
gerritUser = flag.String("gerrit-user", "", "gerrit authentication username")
gerritPass = flag.String("gerrit-pass", "", "gerrit authentication password")
repoFlag = flag.String("repo", "dawn", "the repo")
userFlag = flag.String("user", defaultUser(), "user name / email")
verboseFlag = flag.Bool("v", false, "verbose mode")
dryrunFlag = flag.Bool("dry", false, "dry mode. Don't apply any labels")
)
func defaultUser() string {
if gitExe, err := exec.LookPath("git"); err == nil {
if g, err := git.New(gitExe); err == nil {
if cwd, err := os.Getwd(); err == nil {
if r, err := g.Open(cwd); err == nil {
if cfg, err := r.Config(nil); err == nil {
return cfg["user.email"]
}
}
}
}
}
return ""
}
func main() {
flag.Usage = func() {
out := flag.CommandLine.Output()
fmt.Fprintf(out,
`%v applies the 'Commit-Queue+2' label to Gerrit changes authored by the user that are ready to be submitted.
The tool monitors Gerrit changes authored by the user, looking for changes that have the labels
'Kokoro+1', 'Auto-Submit +1' and 'Code-Review +2' and applies the 'Commit-Queue +2' label.
`, toolName)
fmt.Fprintf(out, "\n")
flag.PrintDefaults()
}
flag.Parse()
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run() error {
user := *userFlag
if user == "" {
return fmt.Errorf("Missing required 'user' flag")
}
g, err := gerrit.New(dawn.GerritURL, gerrit.Credentials{
Username: *gerritUser, Password: *gerritPass,
})
if err != nil {
return err
}
app := app{Gerrit: g, user: user}
log.Println("Monitoring for changes ready to be submitted...")
for {
err := app.submitReadyChanges()
if err != nil {
fmt.Println("error: ", err)
time.Sleep(time.Minute * 10)
}
time.Sleep(time.Minute * 5)
}
}
type app struct {
*gerrit.Gerrit
user string // User account of changes to submit
}
func (a *app) submitReadyChanges() error {
if *verboseFlag {
log.Println("Scanning for changes to submit...")
}
changes, _, err := a.QueryChangesWith(
gerrit.QueryExtraData{
Labels: true,
Messages: true,
CurrentRevision: true,
DetailedAccounts: true,
},
"status:open",
"author:"+a.user,
"-is:wip",
"label:auto-submit",
"label:kokoro",
"repo:"+*repoFlag)
if err != nil {
return fmt.Errorf("failed to Query changes: %w", err)
}
for _, change := range changes {
// Returns true if the change has the label with the given value
hasLabel := func(name string, value int) bool {
if label, ok := change.Labels[name]; ok {
for _, vote := range label.All {
if vote.Value == value {
return true
}
}
}
return false
}
isReadyToSubmit := true &&
hasLabel("Kokoro", 1) &&
hasLabel("Auto-Submit", 1) &&
hasLabel("Code-Review", 2) &&
!hasLabel("Code-Review", -1) &&
!hasLabel("Code-Review", -2)
if !isReadyToSubmit {
// Change does not have all the required labels to submit
continue
}
if hasLabel("Commit-Queue", 2) {
// Change already in the process of submitting
continue
}
switch parseCQStatus(change) {
case cqUnknown, cqPassed:
if *dryrunFlag {
log.Printf("Would submit %v: %v... (--dry)\n", change.ChangeID, change.Subject)
continue
}
log.Printf("Submitting %v: %v...\n", change.ChangeID, change.Subject)
err := a.AddLabel(change.ChangeID, change.CurrentRevision, "Auto submitting change", "Commit-Queue", 2)
if err != nil {
return fmt.Errorf("failed to set Commit-Queue label: %w", err)
}
case cqFailed:
if *verboseFlag {
log.Printf("Change failed CQ: %v: %v...\n", change.ChangeID, change.Subject)
}
}
}
return nil
}
// CQ result status enumerator
type cqStatus int
// CQ result status enumerator values
const (
cqUnknown cqStatus = iota
cqPassed
cqFailed
)
// Attempt to parse the CQ result from the latest patchset's messages from CQ
func parseCQStatus(change gerrit.ChangeInfo) cqStatus {
currentPatchset := change.Revisions[change.CurrentRevision].Number
for _, msg := range change.Messages {
if msg.RevisionNumber != currentPatchset {
continue
}
if msg.Author.Email == cqEmailAccount {
if strings.Contains(msg.Message, "This CL has passed the run") {
return cqPassed
}
if strings.Contains(msg.Message, "This CL has failed the run") {
return cqFailed
}
}
}
return cqUnknown
}

View File

@ -122,15 +122,39 @@ func New(url string, cred Credentials) (*Gerrit, error) {
return &Gerrit{client, cred.Username != ""}, nil return &Gerrit{client, cred.Username != ""}, nil
} }
// QueryExtraData holds extra data to query for with QueryChangesWith()
type QueryExtraData struct {
Labels bool
Messages bool
CurrentRevision bool
DetailedAccounts bool
}
// QueryChanges returns the changes that match the given query strings. // QueryChanges returns the changes that match the given query strings.
// See: https://gerrit-review.googlesource.com/Documentation/user-search.html#search-operators // See: https://gerrit-review.googlesource.com/Documentation/user-search.html#search-operators
func (g *Gerrit) QueryChanges(querys ...string) (changes []gerrit.ChangeInfo, query string, err error) { func (g *Gerrit) QueryChangesWith(extras QueryExtraData, queries ...string) (changes []gerrit.ChangeInfo, query string, err error) {
changes = []gerrit.ChangeInfo{} changes = []gerrit.ChangeInfo{}
query = strings.Join(querys, "+") query = strings.Join(queries, "+")
changeOpts := gerrit.ChangeOptions{}
if extras.Labels {
changeOpts.AdditionalFields = append(changeOpts.AdditionalFields, "LABELS")
}
if extras.Messages {
changeOpts.AdditionalFields = append(changeOpts.AdditionalFields, "MESSAGES")
}
if extras.CurrentRevision {
changeOpts.AdditionalFields = append(changeOpts.AdditionalFields, "CURRENT_REVISION")
}
if extras.DetailedAccounts {
changeOpts.AdditionalFields = append(changeOpts.AdditionalFields, "DETAILED_ACCOUNTS")
}
for { for {
batch, _, err := g.client.Changes.QueryChanges(&gerrit.QueryChangeOptions{ batch, _, err := g.client.Changes.QueryChanges(&gerrit.QueryChangeOptions{
QueryOptions: gerrit.QueryOptions{Query: []string{query}}, QueryOptions: gerrit.QueryOptions{Query: []string{query}},
Skip: len(changes), Skip: len(changes),
ChangeOptions: changeOpts,
}) })
if err != nil { if err != nil {
return nil, "", g.maybeWrapError(err) return nil, "", g.maybeWrapError(err)
@ -144,6 +168,23 @@ func (g *Gerrit) QueryChanges(querys ...string) (changes []gerrit.ChangeInfo, qu
return changes, query, nil return changes, query, nil
} }
// QueryChanges returns the changes that match the given query strings.
// See: https://gerrit-review.googlesource.com/Documentation/user-search.html#search-operators
func (g *Gerrit) QueryChanges(queries ...string) (changes []gerrit.ChangeInfo, query string, err error) {
return g.QueryChangesWith(QueryExtraData{}, queries...)
}
func (g *Gerrit) AddLabel(changeID, revisionID, message, label string, value int) error {
_, _, err := g.client.Changes.SetReview(changeID, revisionID, &gerrit.ReviewInput{
Message: message,
Labels: map[string]string{label: fmt.Sprint(value)},
})
if err != nil {
return g.maybeWrapError(err)
}
return nil
}
// Abandon abandons the change with the given changeID. // Abandon abandons the change with the given changeID.
func (g *Gerrit) Abandon(changeID string) error { func (g *Gerrit) Abandon(changeID string) error {
_, _, err := g.client.Changes.AbandonChange(changeID, &gerrit.AbandonInput{}) _, _, err := g.client.Changes.AbandonChange(changeID, &gerrit.AbandonInput{})