[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:
parent
84d750e982
commit
dededb1e5d
|
@ -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
|
||||
}
|
|
@ -122,15 +122,39 @@ func New(url string, cred Credentials) (*Gerrit, error) {
|
|||
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.
|
||||
// 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{}
|
||||
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 {
|
||||
batch, _, err := g.client.Changes.QueryChanges(&gerrit.QueryChangeOptions{
|
||||
QueryOptions: gerrit.QueryOptions{Query: []string{query}},
|
||||
Skip: len(changes),
|
||||
QueryOptions: gerrit.QueryOptions{Query: []string{query}},
|
||||
Skip: len(changes),
|
||||
ChangeOptions: changeOpts,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, "", g.maybeWrapError(err)
|
||||
|
@ -144,6 +168,23 @@ func (g *Gerrit) QueryChanges(querys ...string) (changes []gerrit.ChangeInfo, qu
|
|||
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.
|
||||
func (g *Gerrit) Abandon(changeID string) error {
|
||||
_, _, err := g.client.Changes.AbandonChange(changeID, &gerrit.AbandonInput{})
|
||||
|
|
Loading…
Reference in New Issue