blob: 60fd1129d0e01b31181c4ba6ce6d619b5dd8d4b1 [file] [log] [blame]
package repo_manager
import (
"context"
"fmt"
"net/http"
"os"
"path"
"strings"
"time"
cipd_api "go.chromium.org/luci/cipd/client/cipd"
"go.skia.org/infra/autoroll/go/codereview"
"go.skia.org/infra/autoroll/go/revision"
"go.skia.org/infra/autoroll/go/strategy"
"go.skia.org/infra/go/cipd"
"go.skia.org/infra/go/exec"
"go.skia.org/infra/go/git"
"go.skia.org/infra/go/github"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
)
const (
TMPL_COMMIT_MSG_GITHUB_CIPD_DEPS = `Roll {{.ChildPath}} from {{.RollingFrom.String}} to {{.RollingTo.String}}
If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
{{.ServerURL}}
Please CC {{stringsJoin .Reviewers ","}} on the revert to ensure that a human
is aware of the problem.
To report a problem with the AutoRoller itself, please file a bug:
https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug
Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+/master/autoroll/README.md
`
cipdPackageUrlTmpl = "%s/p/%s/+/%s"
)
var (
// Use this function to instantiate a NewGithubCipdDEPSRepoManager. This is able to be
// overridden for testing.
NewGithubCipdDEPSRepoManager func(context.Context, *GithubCipdDEPSRepoManagerConfig, string, string, *github.GitHub, string, string, *http.Client, codereview.CodeReview, bool) (RepoManager, error) = newGithubCipdDEPSRepoManager
)
// GithubCipdDEPSRepoManagerConfig provides configuration for the Github RepoManager.
type GithubCipdDEPSRepoManagerConfig struct {
GithubDEPSRepoManagerConfig
CipdAssetName string `json:"cipdAssetName"`
CipdAssetTag string `json:"cipdAssetTag"`
}
// See documentation for RepoManagerConfig interface.
func (r *GithubCipdDEPSRepoManagerConfig) ValidStrategies() []string {
return []string{
strategy.ROLL_STRATEGY_BATCH,
}
}
// Validate the config.
func (c *GithubCipdDEPSRepoManagerConfig) Validate() error {
if c.CipdAssetName == "" {
return fmt.Errorf("CipdAssetName is required.")
}
if c.CipdAssetTag == "" {
return fmt.Errorf("CipdAssetTag is required.")
}
return c.GithubDEPSRepoManagerConfig.Validate()
}
// githubCipdDEPSRepoManager is a struct used by the autoroller for managing checkouts.
type githubCipdDEPSRepoManager struct {
*githubDEPSRepoManager
cipdAssetName string
cipdAssetTag string
CipdClient cipd.CIPDClient
}
// newGithubCipdDEPSRepoManager returns a RepoManager instance which operates in the given
// working directory and updates at the given frequency.
func newGithubCipdDEPSRepoManager(ctx context.Context, c *GithubCipdDEPSRepoManagerConfig, workdir, rollerName string, githubClient *github.GitHub, recipeCfgFile, serverURL string, httpClient *http.Client, cr codereview.CodeReview, local bool) (RepoManager, error) {
if err := c.Validate(); err != nil {
return nil, err
}
wd := path.Join(workdir, strings.TrimSuffix(path.Base(c.DepotToolsRepoManagerConfig.ParentRepo), ".git"))
if c.CommitMsgTmpl == "" {
c.CommitMsgTmpl = TMPL_COMMIT_MSG_GITHUB_CIPD_DEPS
}
drm, err := newDepotToolsRepoManager(ctx, c.DepotToolsRepoManagerConfig, wd, recipeCfgFile, serverURL, nil, httpClient, cr, local)
if err != nil {
return nil, err
}
dr := &depsRepoManager{
depotToolsRepoManager: drm,
}
if c.GithubParentPath != "" {
dr.parentDir = path.Join(wd, c.GithubParentPath)
}
gr := &githubDEPSRepoManager{
depsRepoManager: dr,
githubClient: githubClient,
rollBranchName: rollerName,
}
sklog.Infof("Roller name is: %s\n", rollerName)
cipdClient, err := cipd.NewClient(httpClient, path.Join(workdir, "cipd"))
if err != nil {
return nil, err
}
gcr := &githubCipdDEPSRepoManager{
githubDEPSRepoManager: gr,
cipdAssetName: c.CipdAssetName,
cipdAssetTag: c.CipdAssetTag,
CipdClient: cipdClient,
}
return gcr, nil
}
// See documentation for RepoManager interface.
func (rm *githubCipdDEPSRepoManager) Update(ctx context.Context) (*revision.Revision, *revision.Revision, []*revision.Revision, error) {
// Sync the projects.
rm.repoMtx.Lock()
defer rm.repoMtx.Unlock()
sklog.Info("Updating github repository")
// If parentDir does not exist yet then create the directory structure and
// populate it.
if _, err := os.Stat(rm.parentDir); err != nil {
if os.IsNotExist(err) {
if err := rm.createAndSyncParentWithRemoteAndBranch(ctx, "origin", rm.rollBranchName, rm.rollBranchName); err != nil {
return nil, nil, nil, fmt.Errorf("Could not create and sync %s: %s", rm.parentDir, err)
}
// Run gclient hooks to bring in any required binaries.
if _, err := exec.RunCommand(ctx, &exec.Command{
Dir: rm.parentDir,
Env: rm.depotToolsEnv,
Name: rm.gclient,
Args: []string{"runhooks"},
}); err != nil {
return nil, nil, nil, fmt.Errorf("Error when running gclient runhooks on %s: %s", rm.parentDir, err)
}
} else {
return nil, nil, nil, fmt.Errorf("Error when running os.Stat on %s: %s", rm.parentDir, err)
}
}
// Check to see whether there is an upstream yet.
remoteOutput, err := git.GitDir(rm.parentDir).Git(ctx, "remote", "show")
if err != nil {
return nil, nil, nil, err
}
remoteFound := false
remoteLines := strings.Split(remoteOutput, "\n")
for _, remoteLine := range remoteLines {
if remoteLine == GITHUB_UPSTREAM_REMOTE_NAME {
remoteFound = true
break
}
}
if !remoteFound {
if _, err := git.GitDir(rm.parentDir).Git(ctx, "remote", "add", GITHUB_UPSTREAM_REMOTE_NAME, rm.parentRepo); err != nil {
return nil, nil, nil, err
}
}
// Fetch upstream.
if _, err := git.GitDir(rm.parentDir).Git(ctx, "fetch", GITHUB_UPSTREAM_REMOTE_NAME, rm.parentBranch); err != nil {
return nil, nil, nil, err
}
// gclient sync to get latest version of child repo to find the next roll
// rev from.
if err := rm.createAndSyncParentWithRemoteAndBranch(ctx, GITHUB_UPSTREAM_REMOTE_NAME, rm.rollBranchName, rm.parentBranch); err != nil {
return nil, nil, nil, fmt.Errorf("Could not create and sync parent repo: %s", err)
}
// Get the last roll revision.
lastRollRev, err := rm.getLastRollRev(ctx)
if err != nil {
return nil, nil, nil, err
}
// Find the not-rolled child repo commits.
notRolledRevs, err := rm.getNotRolledRevs(ctx, lastRollRev)
if err != nil {
return nil, nil, nil, err
}
tipRev := lastRollRev
if len(notRolledRevs) > 0 {
tipRev = notRolledRevs[0]
}
rm.infoMtx.Lock()
defer rm.infoMtx.Unlock()
if rm.childRepoUrl == "" {
childRepo, err := git.GitDir(rm.parentDir).Git(ctx, "remote", "get-url", "origin")
if err != nil {
return nil, nil, nil, err
}
rm.childRepoUrl = strings.TrimSpace(childRepo)
}
return lastRollRev, tipRev, notRolledRevs, nil
}
func (rm *githubCipdDEPSRepoManager) cipdInstanceToRevision(instance *cipd_api.InstanceInfo) *revision.Revision {
return &revision.Revision{
Id: instance.Pin.InstanceID,
Display: instance.Pin.InstanceID[:5] + "...",
Description: instance.Pin.String(),
Timestamp: time.Time(instance.RegisteredTs),
URL: fmt.Sprintf(cipdPackageUrlTmpl, cipd.SERVICE_URL, rm.cipdAssetName, instance.Pin.InstanceID),
}
}
// getNotRolledRevs is a utility function that uses CIPD to find the not-yet-rolled versions of
// the specified package.
// Note: that this just finds all versions of the package between the last rolled version and the
// version currently pointed to by cipdAssetTag; we can't know whether the ref we're tracking was
// ever actually applied to any of the package instances in between.
func (rm *githubCipdDEPSRepoManager) getNotRolledRevs(ctx context.Context, lastRollRev *revision.Revision) ([]*revision.Revision, error) {
head, err := rm.CipdClient.ResolveVersion(ctx, rm.cipdAssetName, rm.cipdAssetTag)
if err != nil {
return nil, err
}
if lastRollRev.Id == head.InstanceID {
return []*revision.Revision{}, nil
}
iter, err := rm.CipdClient.ListInstances(ctx, rm.cipdAssetName)
if err != nil {
return nil, err
}
notRolledRevs := []*revision.Revision{}
foundHead := false
for {
instances, err := iter.Next(ctx, 100)
if err != nil {
return nil, err
}
if len(instances) == 0 {
break
}
for _, instance := range instances {
id := instance.Pin.InstanceID
if id == head.InstanceID {
foundHead = true
}
if id == lastRollRev.Id {
return notRolledRevs, nil
}
if foundHead {
notRolledRevs = append(notRolledRevs, rm.cipdInstanceToRevision(&instance))
}
}
}
return notRolledRevs, nil
}
func (rm *githubCipdDEPSRepoManager) getLastRollRev(ctx context.Context) (*revision.Revision, error) {
output, err := exec.RunCwd(ctx, rm.parentDir, "python", rm.gclient, "getdep", "-r", fmt.Sprintf("%s:%s", rm.childPath, rm.cipdAssetName))
if err != nil {
return nil, err
}
hash := strings.TrimSpace(output)
if hash == "" {
return nil, fmt.Errorf("Got invalid output for `gclient getdep`: %s", output)
}
instance, err := rm.CipdClient.Describe(ctx, rm.cipdAssetName, hash)
if err != nil {
return nil, err
}
return rm.cipdInstanceToRevision(&instance.InstanceInfo), nil
}
// See documentation for RepoManager interface.
func (rm *githubCipdDEPSRepoManager) CreateNewRoll(ctx context.Context, from, to *revision.Revision, rolling []*revision.Revision, emails []string, cqExtraTrybots string, dryRun bool) (int64, error) {
rm.repoMtx.Lock()
defer rm.repoMtx.Unlock()
sklog.Info("Creating a new Github Roll")
// Clean the checkout, get onto a fresh branch.
if err := rm.cleanParentWithRemoteAndBranch(ctx, GITHUB_UPSTREAM_REMOTE_NAME, rm.rollBranchName, rm.parentBranch); err != nil {
return 0, err
}
if _, err := git.GitDir(rm.parentDir).Git(ctx, "checkout", fmt.Sprintf("%s/%s", GITHUB_UPSTREAM_REMOTE_NAME, rm.parentBranch), "-b", rm.rollBranchName); err != nil {
return 0, err
}
// Defer cleanup.
defer func() {
util.LogErr(rm.cleanParentWithRemoteAndBranch(ctx, GITHUB_UPSTREAM_REMOTE_NAME, rm.rollBranchName, rm.parentBranch))
}()
// Make sure the forked repo is at the same hash as the target repo before
// creating the pull request on both parentBranch and rm.rollBranchName.
if _, err := git.GitDir(rm.parentDir).Git(ctx, "push", "origin", rm.parentBranch, "-f"); err != nil {
return 0, err
}
if _, err := git.GitDir(rm.parentDir).Git(ctx, "push", "origin", rm.rollBranchName, "-f"); err != nil {
return 0, err
}
// Make sure the right name and email are set.
if !rm.local {
if _, err := git.GitDir(rm.parentDir).Git(ctx, "config", "user.name", rm.codereview.UserName()); err != nil {
return 0, err
}
if _, err := git.GitDir(rm.parentDir).Git(ctx, "config", "user.email", rm.codereview.UserEmail()); err != nil {
return 0, err
}
}
// Run "gclient setdep".
args := []string{"setdep", "-r", fmt.Sprintf("%s:%s@%s", rm.childPath, rm.cipdAssetName, to.Id)}
if _, err := exec.RunCommand(ctx, &exec.Command{
Dir: rm.parentDir,
Env: rm.depotToolsEnv,
Name: rm.gclient,
Args: args,
}); err != nil {
return 0, err
}
// Make the checkout match the new DEPS.
sklog.Info("Running gclient sync on the checkout")
if _, err := exec.RunCommand(ctx, &exec.Command{
Dir: rm.depsRepoManager.parentDir,
Env: rm.depotToolsEnv,
Name: rm.gclient,
Args: []string{"sync", "-D", "-f"},
}); err != nil {
return 0, fmt.Errorf("Error when running gclient sync to make checkout match the new DEPS: %s", err)
}
// Run the pre-upload steps.
for _, s := range rm.preUploadSteps {
if err := s(ctx, rm.depotToolsEnv, rm.httpClient, rm.parentDir); err != nil {
return 0, fmt.Errorf("Error when running pre-upload step: %s", err)
}
}
// Build the commitMsg.
commitMsg, err := rm.buildCommitMsg(&CommitMsgVars{
ChildPath: rm.cipdAssetName,
RollingFrom: from,
RollingTo: to,
ServerURL: rm.serverURL,
})
if err != nil {
return 0, err
}
// Commit.
if _, err := git.GitDir(rm.parentDir).Git(ctx, "commit", "-a", "-m", commitMsg); err != nil {
return 0, err
}
// Push to the forked repository.
if _, err := git.GitDir(rm.parentDir).Git(ctx, "push", "origin", rm.rollBranchName, "-f"); err != nil {
return 0, err
}
// Create a pull request.
title := strings.Split(commitMsg, "\n")[0]
headBranch := fmt.Sprintf("%s:%s", rm.codereview.UserName(), rm.rollBranchName)
pr, err := rm.githubClient.CreatePullRequest(title, rm.parentBranch, headBranch, commitMsg)
if err != nil {
return 0, err
}
// Add appropriate label to the pull request.
label := github.COMMIT_LABEL
if dryRun {
label = github.DRYRUN_LABEL
}
if err := rm.githubClient.AddLabel(pr.GetNumber(), label); err != nil {
return 0, err
}
return int64(pr.GetNumber()), nil
}
// See documentation for RepoManager interface.
func (r *githubCipdDEPSRepoManager) GetRevision(ctx context.Context, id string) (*revision.Revision, error) {
instance, err := r.CipdClient.Describe(ctx, r.cipdAssetName, id)
if err != nil {
return nil, err
}
return r.cipdInstanceToRevision(&instance.InstanceInfo), nil
}