[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
|
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{})
|
||||||
|
|
Loading…
Reference in New Issue