blob: 2e6fc2d5916e9ce8b2d12149c8f76824a382a6b0 [file] [log] [blame]
package parent
import (
"context"
"fmt"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"go.skia.org/infra/autoroll/go/codereview"
"go.skia.org/infra/autoroll/go/config"
"go.skia.org/infra/autoroll/go/config_vars"
"go.skia.org/infra/autoroll/go/repo_manager/common/gerrit_common"
"go.skia.org/infra/autoroll/go/repo_manager/common/git_common"
autoroll_git_common "go.skia.org/infra/autoroll/go/repo_manager/common/git_common"
"go.skia.org/infra/autoroll/go/revision"
"go.skia.org/infra/go/exec"
"go.skia.org/infra/go/gerrit"
"go.skia.org/infra/go/git"
"go.skia.org/infra/go/golang"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
)
const (
goModFile = "go.mod"
)
type goModParent struct {
*git_common.Checkout
modulePath string
regex *regexp.Regexp
createRoll git_common.CreateRollFunc
uploadRoll git_common.UploadRollFunc
}
// NewGoModGerritParent returns a Parent which updates go.mod and uploads CLs to
// Gerrit.
func NewGoModGerritParent(ctx context.Context, c *config.GoModGerritParentConfig, reg *config_vars.Registry, client *http.Client, workdir string, cr codereview.CodeReview) (*goModParent, error) {
if err := c.Validate(); err != nil {
return nil, skerr.Wrap(err)
}
gerritClient, ok := cr.Client().(*gerrit.Gerrit)
if !ok {
return nil, skerr.Fmt("GitCheckoutGithub must use GitHub for code review.")
}
uploadRoll := GitCheckoutUploadGerritRollFunc(gerritClient)
parentRM, err := NewGoModParent(ctx, c.GoMod, reg, client, workdir, cr, uploadRoll)
if err != nil {
return nil, skerr.Wrap(err)
}
if err := gerrit_common.SetupGerrit(ctx, parentRM.Checkout.Checkout, gerritClient); err != nil {
return nil, skerr.Wrap(err)
}
return parentRM, nil
}
// NewGoModParent returns a Parent which updates go.mod.
func NewGoModParent(ctx context.Context, c *config.GoModParentConfig, reg *config_vars.Registry, client *http.Client, workdir string, cr codereview.CodeReview, uploadRoll autoroll_git_common.UploadRollFunc) (*goModParent, error) {
if err := c.Validate(); err != nil {
return nil, skerr.Wrap(err)
}
co, err := git_common.NewCheckout(ctx, c.GitCheckout, reg, workdir, cr.UserName(), cr.UserEmail(), nil)
if err != nil {
return nil, skerr.Wrap(err)
}
regex, err := regexp.Compile(fmt.Sprintf(`%s (\S+)`, c.ModulePath))
if err != nil {
return nil, skerr.Wrap(err)
}
// Pre-upload steps are run after setting the new dependency version and
// syncing, but before committing and uploading.
preUploadSteps, err := GetPreUploadSteps(c.PreUploadSteps, c.PreUploadCommands)
if err != nil {
return nil, skerr.Wrap(err)
}
// Support a custom wrapper around Go, so that we can support hermetic
// installation eg. via Bazel.
var goCmd []string
if c.GoCmd != "" {
goCmd = strings.Fields(c.GoCmd)
} else {
goBin, err := golang.FindGo()
if err != nil {
return nil, skerr.Wrap(err)
}
goCmd = []string{goBin}
}
runGo := func(ctx context.Context, dir string, cmd ...string) (string, error) {
return exec.RunCwd(ctx, dir, append(goCmd, cmd...)...)
}
createRoll := func(ctx context.Context, co *git.Checkout, from *revision.Revision, to *revision.Revision, rolling []*revision.Revision, commitMsg string) (string, error) {
// Update the Go module.
if _, err := runGo(ctx, co.Dir(), "get", fmt.Sprintf("%s@%s", c.ModulePath, to.Id)); err != nil {
return "", skerr.Wrap(err)
}
if _, err := runGo(ctx, co.Dir(), "mod", "tidy"); err != nil {
return "", skerr.Wrap(err)
}
// Run the pre-upload steps.
sklog.Infof("Running %d pre-upload steps.", len(preUploadSteps))
for _, s := range preUploadSteps {
if err := s(ctx, nil, client, co.Dir(), from, to); err != nil {
return "", skerr.Wrapf(err, "failed pre-upload step")
}
}
// Commit.
if _, err := co.Git(ctx, "commit", "-a", "-m", commitMsg); err != nil {
return "", skerr.Wrap(err)
}
out, err := co.RevParse(ctx, "HEAD")
if err != nil {
return "", skerr.Wrap(err)
}
return strings.TrimSpace(out), nil
}
return &goModParent{
Checkout: co,
modulePath: c.ModulePath,
regex: regex,
createRoll: createRoll,
uploadRoll: uploadRoll,
}, nil
}
// getPinnedRevision reads the go.mod file and extracts the pinned revision of
// the configured dependency.
func (p *goModParent) getPinnedRevision() (string, error) {
// Note: this flow only handles module dependencies which use Git revisions,
// ie. the dependency looks like:
// `go.skia.org/infra v0.0.0-20221018142618-5ea492a442f6`
// We extract the version after the module path and split it on "-", taking
// the last element, which is an abbreviated commit hash. If we wanted to
// generalize this, we'd need a special type of Child which retrieves
// semantic version Git tags instead of individual commit hashes, and we'd
// have to distinguish between the two flows here.
b, err := os.ReadFile(filepath.Join(p.Checkout.Dir(), goModFile))
if err != nil {
return "", skerr.Wrapf(err, "failed to read %s", goModFile)
}
matches := p.regex.FindSubmatch(b)
if len(matches) != 2 {
return "", skerr.Fmt("failed to find %s in:\n%s", p.modulePath, string(b))
}
split := strings.Split(string(matches[1]), "-")
return split[len(split)-1], nil
}
// See documentation for Parent interface.
func (p *goModParent) Update(ctx context.Context) (string, error) {
_, _, err := p.Checkout.Update(ctx)
if err != nil {
return "", skerr.Wrapf(err, "failed to update repo")
}
return p.getPinnedRevision()
}
// See documentation for Parent interface.
func (p *goModParent) CreateNewRoll(ctx context.Context, from, to *revision.Revision, rolling []*revision.Revision, emails []string, dryRun bool, commitMsg string) (int64, error) {
return p.Checkout.CreateNewRoll(ctx, from, to, rolling, emails, dryRun, commitMsg, p.createRoll, p.uploadRoll)
}
var _ Parent = &goModParent{}