diff --git a/internal/kokoro/discogen/go.mod b/internal/kokoro/discogen/go.mod new file mode 100644 index 00000000000..f4065c5ef8e --- /dev/null +++ b/internal/kokoro/discogen/go.mod @@ -0,0 +1,16 @@ +module google.golang.org/api/internal/kokoro/discogen + +go 1.17 + +require ( + github.com/google/go-github/v42 v42.0.0 + golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be +) + +require ( + github.com/golang/protobuf v1.3.2 // indirect + github.com/google/go-querystring v1.1.0 // indirect + golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect + google.golang.org/appengine v1.6.7 // indirect +) diff --git a/internal/kokoro/discogen/go.sum b/internal/kokoro/discogen/go.sum new file mode 100644 index 00000000000..65405080ee1 --- /dev/null +++ b/internal/kokoro/discogen/go.sum @@ -0,0 +1,32 @@ +github.com/bradleyfalzon/ghinstallation/v2 v2.0.3/go.mod h1:tlgi+JWCXnKFx/Y4WtnDbZEINo31N5bcvnCoqieefmk= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github/v39 v39.0.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= +github.com/google/go-github/v42 v42.0.0 h1:YNT0FwjPrEysRkLIiKuEfSvBPCGKphW5aS5PxwaoLec= +github.com/google/go-github/v42 v42.0.0/go.mod h1:jgg/jvyI0YlDOM1/ps6XYh04HNQ3vKf0CVko62/EhRg= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= diff --git a/internal/kokoro/discogen/main.go b/internal/kokoro/discogen/main.go new file mode 100644 index 00000000000..5291436e475 --- /dev/null +++ b/internal/kokoro/discogen/main.go @@ -0,0 +1,180 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "context" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "os/user" + "path" + "path/filepath" + "strings" + + "github.com/google/go-github/v42/github" + "golang.org/x/oauth2" +) + +const ( + branchName = "discogen" + commitTitle = "feat(all): auto-regenerate discovery clients" + owner = "googleapis" + repo = "google-api-go-client" +) + +func main() { + ctx := context.Background() + + githubAccessToken := flag.String("github-access-token", os.Getenv("GITHUB_ACCESS_TOKEN"), "The token used to open pull requests. Required.") + githubUsername := flag.String("github-username", os.Getenv("GITHUB_USERNAME"), "The GitHub user name for the author. Required.") + githubName := flag.String("github-name", os.Getenv("GITHUB_NAME"), "The name of the author for git commits. Required.") + githubEmail := flag.String("github-email", os.Getenv("GITHUB_EMAIL"), "The email address of the author. Required.") + discoDir := flag.String("discovery-dir", os.Getenv("DISCOVERY_DIR"), "Directory where sources of googleapis/google-api-go-client resides. Required.") + + flag.Parse() + + if *githubAccessToken == "" || *githubUsername == "" || *githubName == "" || *githubEmail == "" || *discoDir == "" { + log.Fatal("all required flags not set") + } + + if err := setGitCreds(*githubName, *githubEmail, *githubUsername, *githubAccessToken); err != nil { + log.Fatalf("unable to set git credentials: %v", err) + } + + if prIsOpen, err := isPROpen(ctx, *githubAccessToken, *githubUsername); err != nil || prIsOpen { + if err != nil { + log.Fatalf("unable to check PR status: %v", err) + } + log.Println("a regen PR is already open, nothing to do here") + os.Exit(0) + } + + if err := generate(*discoDir); err != nil { + log.Fatalf("unable to generate discovery clients: %v", err) + } + + if hasChanges, err := hasChanges(*discoDir); err != nil || !hasChanges { + if err != nil { + log.Fatalf("unable to check git status: %v", err) + } + log.Println("no local changes, exiting") + os.Exit(0) + } + + if err := makePR(ctx, *githubAccessToken, *discoDir); err != nil { + log.Fatalf("unable to make regen PR: %v", err) + } +} + +// setGitCreds configures credentials for GitHub. +func setGitCreds(githubName, githubEmail, githubUsername, accessToken string) error { + u, err := user.Current() + if err != nil { + return err + } + gitCredentials := []byte(fmt.Sprintf("https://%s:%s@github.com", githubUsername, accessToken)) + if err := ioutil.WriteFile(path.Join(u.HomeDir, ".git-credentials"), gitCredentials, 0644); err != nil { + return err + } + c := exec.Command("git", "config", "--global", "user.name", githubName) + if err := c.Run(); err != nil { + return err + } + + c = exec.Command("git", "config", "--global", "user.email", githubEmail) + return c.Run() +} + +// isPROpen checks if a regen PR is already open. +func isPROpen(ctx context.Context, accessToken, username string) (bool, error) { + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: accessToken}, + ) + tc := oauth2.NewClient(ctx, ts) + githubClient := github.NewClient(tc) + opt := &github.PullRequestListOptions{ + ListOptions: github.ListOptions{PerPage: 50}, + State: "open", + } + prs, _, err := githubClient.PullRequests.List(ctx, owner, repo, opt) + if err != nil { + return false, err + } + for _, pr := range prs { + if !strings.Contains(pr.GetTitle(), "auto-regenerate") { + continue + } + if pr.GetUser().GetLogin() != username { + continue + } + return true, nil + } + return false, nil +} + +// generate regenerates the whole project. +func generate(dir string) error { + fp := filepath.Join(dir, "google-api-go-generator") + cmd := exec.Command("make", "all") + cmd.Dir = fp + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + return cmd.Run() +} + +// hasChanges reports if any files have been updated. +func hasChanges(dir string) (bool, error) { + c := exec.Command("git", "status", "--short") + c.Dir = dir + b, err := c.Output() + return len(b) > 0, err +} + +// makePR commits local changes and makes a regen PR. +func makePR(ctx context.Context, accessToken, dir string) error { + c := exec.Command("/bin/bash", "-c", ` + set -ex + + git config credential.helper store + + git branch -D $BRANCH_NAME || true + git push -d origin $BRANCH_NAME || true + + git add -A + git checkout -b $BRANCH_NAME + git commit -m "$COMMIT_TITLE" + git push origin $BRANCH_NAME + `) + c.Env = []string{ + fmt.Sprintf("COMMIT_TITLE=%s", commitTitle), + fmt.Sprintf("BRANCH_NAME=%s", branchName), + } + c.Dir = dir + if err := c.Run(); err != nil { + return err + } + + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: accessToken}, + ) + tc := oauth2.NewClient(ctx, ts) + githubClient := github.NewClient(tc) + head := owner + ":" + branchName + base := "main" + t := commitTitle + _, _, err := githubClient.PullRequests.Create(ctx, owner, repo, &github.NewPullRequest{ + Title: &t, + Head: &head, + Base: &base, + }) + if err != nil { + return err + } + return nil +}