blob: 11daafcf044f8e05617bbf2a0f30a8765d7485f8 [file] [log] [blame]
// update_go_deps modifies the go.mod and go.sum files to sync to the most
// recent versions of all listed dependencies.
//
// If the go.mod file is not being updated, check the recent runs of this Task
// Driver to verify that:
//
// 1. It is running at all. If not, there may be a bot capacity problem, or a
// problem with the Task Scheduler.
// 2. It is succeeding. There are a number of reasons why it might fail, but the
// most common is that a change has landed in one of the dependencies which
// is not compatible with the current version of our code. Check the logs for
// the failing step(s).
// 3. The CL uploaded by this task driver is passing the commit queue and
// landing. This task driver does not run all of the tests and so the CL it
// uploads may fail the commit queue for legitimate reasons. Look into the
// failures and determine what actions to take.
//
// If update_go_deps itself is failing, or if the CL it uploads is failing to
// land, you may need to take one of the following actions:
//
// 1. If possible, update call sites in our repo(s) to match the upstream
// changes. Include the update to go.mod in the same CL. This is only
// possible if our repo is the only user of the modified dependency, or if
// all other users have already updated to account for the change.
// 2. Add an "exclude" directive in go.mod. Ideally, this is temporary and can
// be removed, eg. when all of our dependencies have updated to account for
// a breaking change in a shared dependency. If you expect the exclude to be
// temporary, file a bug and add a comment next to the exclude. Note that
// only specific versions can be excluded, so we may need to exclude
// additional versions for the same breaking change as versions are released.
// 3. If the breaking change is intentional and we never expect to be able to
// update to a newer version of the dependency (eg. a required feature was
// removed), fork the broken dependency. Update all references in our repo(s)
// to use the fork, or add a "replace" directive in go.mod. Generally we
// should file a bug against the dependency first to verify that the breaking
// change is both intentional and not going to be reversed. Forking implies
// some amount of maintenance headache (eg. what if the dependency is shared
// by others which assume they're using the most recent version?), so this
// should be a last resort.
package main
import (
"bytes"
"context"
"flag"
"fmt"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"go.skia.org/infra/go/auth"
"go.skia.org/infra/go/common"
"go.skia.org/infra/go/exec"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/util"
"go.skia.org/infra/task_driver/go/lib/auth_steps"
"go.skia.org/infra/task_driver/go/lib/checkout"
"go.skia.org/infra/task_driver/go/lib/gerrit_steps"
"go.skia.org/infra/task_driver/go/lib/golang"
"go.skia.org/infra/task_driver/go/lib/os_steps"
"go.skia.org/infra/task_driver/go/lib/rotations"
"go.skia.org/infra/task_driver/go/td"
)
var (
// Required properties for this task.
gerritProject = flag.String("gerrit_project", "", "Gerrit project name.")
gerritUrl = flag.String("gerrit_url", "", "URL of the Gerrit server.")
projectId = flag.String("project_id", "", "ID of the Google Cloud project.")
taskId = flag.String("task_id", "", "ID of this task.")
taskName = flag.String("task_name", "", "Name of the task.")
workdir = flag.String("workdir", ".", "Working directory")
checkoutFlags = checkout.SetupFlags(nil)
// Optional flags.
local = flag.Bool("local", false, "True if running locally (as opposed to on the bots)")
output = flag.String("o", "", "If provided, dump a JSON blob of step data to the given file. Prints to stdout if '-' is given.")
)
func main() {
// Setup.
ctx := td.StartRun(projectId, taskId, taskName, output, local)
defer td.EndRun(ctx)
rs, err := checkout.GetRepoState(checkoutFlags)
if err != nil {
td.Fatal(ctx, err)
}
if *gerritProject == "" {
td.Fatalf(ctx, "--gerrit_project is required.")
}
if *gerritUrl == "" {
td.Fatalf(ctx, "--gerrit_url is required.")
}
wd, err := os_steps.Abs(ctx, *workdir)
if err != nil {
td.Fatal(ctx, err)
}
// Check out the code.
co, err := checkout.EnsureGitCheckout(ctx, path.Join(wd, "repo"), rs)
if err != nil {
td.Fatal(ctx, err)
}
// Setup go.
ctx = golang.WithEnv(ctx, wd)
// Perform steps to update the dependencies.
{
// By default, the Go env includes GOFLAGS=-mod=readonly, which prevents
// commands from modifying go.mod; in this case, we want to modify it,
// so unset that variable.
ctx := td.WithEnv(ctx, []string{"GOFLAGS="})
// This "go list" command obtains the set of direct dependencies; that
// is, the modules containing packages which are imported directly by
// our code.
var buf bytes.Buffer
// Print the package import path and the module path for every package
// imported in this repo.
const format = "{{if .Module}}{{if not (or .Module.Main .Module.Indirect)}}{{.ImportPath}} {{.Module.Path}}{{end}}{{end}}"
listCmd := &exec.Command{
Name: "go",
Args: []string{"list", "-f", format, "all"},
Dir: co.Dir(),
Stdout: &buf,
}
if _, err := exec.RunCommand(ctx, listCmd); err != nil {
td.Fatal(ctx, err)
}
// Organize the direct dependencies by module.
deps := map[string][]string{}
for _, line := range strings.Split(strings.TrimSpace(buf.String()), "\n") {
split := strings.Split(line, " ")
if len(split) != 2 {
td.Fatalf(ctx, "Incorrect format for line; expected \"<package path> <module path>\" but got: %s", line)
}
deps[split[1]] = append(deps[split[1]], split[0])
}
modules := make([]string, 0, len(deps))
for module := range deps {
modules = append(modules, module)
}
sort.Strings(modules)
// Perform the update.
getCmd := []string{
"get",
"-u", // Update the named modules.
"-t", // Also update modules only used in tests.
"-d", // Download the updated modules but don't build or install them.
}
if err := td.Do(ctx, td.Props("go "+strings.Join(getCmd, " ")), func(ctx context.Context) error {
// We can't simply "go get" the module, because that will fail if it
// has no .go files at the root level. It's also possible that
// packages have been renamed or removed between versions, so
// "go get" might fail for any single package as well. Try a series
// of updates, starting with the module proper, and iterating over
// the packages we import. Stop after the first success.
var lastErr error
for _, module := range modules {
pkgs := deps[module]
for _, target := range append([]string{module}, pkgs...) {
_, lastErr = golang.Go(ctx, co.Dir(), append(getCmd, target)...)
if lastErr == nil {
break
}
}
}
return skerr.Wrap(lastErr)
}); err != nil {
td.Fatal(ctx, err)
}
// Explicitly build the infra module, because "go build ./..." doesn't
// update go.sum for dependencies of the infra module when run in the
// Skia repo. We have some Skia bots which install things from the infra
// repo (eg. task drivers which are used directly and not imported), and
// go.mod and go.sum need to account for that.
if _, err := golang.Go(ctx, co.Dir(), "build", "-i", "go.skia.org/infra/..."); err != nil {
td.Fatal(ctx, err)
}
// "go build" may also update dependencies, or its results may
// change based on the updated dependencies.
if _, err := golang.Go(ctx, co.Dir(), "build", "./..."); err != nil {
td.Fatal(ctx, err)
}
// Setting -exec=echo causes the tests to not actually run; therefore
// this compiles the tests but doesn't run them.
if _, err := golang.Go(ctx, co.Dir(), "test", "-exec=echo", "./..."); err != nil {
td.Fatal(ctx, err)
}
}
// The below commands run with GOFLAGS=-mod=readonly and thus act as a
// self-check to ensure that we've updated go.mod and go.sum correctly.
// Tool dependencies; these should be listed in the top-level tools.go
// file and should therefore be updated via "go get" above. If this
// fails, it's likely because one of the tools we're installing is not
// present in tools.go and therefore not present in go.mod.
if err := golang.InstallCommonDeps(ctx, co.Dir()); err != nil {
td.Fatal(ctx, err)
}
// The generators may have been updated, so run "go generate".
if _, err := golang.Go(ctx, co.Dir(), "generate", "./..."); err != nil {
td.Fatal(ctx, err)
}
// Regenerate the licenses file.
if rs.Repo == common.REPO_SKIA_INFRA {
if _, err := exec.RunCwd(ctx, filepath.Join(co.Dir(), "licenses"), "make", "regenerate"); err != nil {
td.Fatal(ctx, err)
}
}
// Regenerate infra/bots/tasks.json in case a dependency changed its
// behavior.
if _, err := golang.Go(ctx, filepath.Join(co.Dir(), "infra", "bots"), "run", "./gen_tasks.go"); err != nil {
td.Fatal(ctx, err)
}
// If we changed anything, upload a CL.
c, err := auth_steps.InitHttpClient(ctx, *local, auth.SCOPE_USERINFO_EMAIL)
if err != nil {
td.Fatal(ctx, err)
}
reviewers, err := rotations.GetCurrentTrooper(ctx, c)
if err != nil {
td.Fatal(ctx, err)
}
g, err := gerrit_steps.Init(ctx, *local, *gerritUrl)
if err != nil {
td.Fatal(ctx, err)
}
isTryJob := *local || rs.Issue != ""
if isTryJob {
var i int64
if err := td.Do(ctx, td.Props(fmt.Sprintf("Parse %q as int", rs.Issue)).Infra(), func(ctx context.Context) error {
var err error
i, err = strconv.ParseInt(rs.Issue, 10, 64)
return err
}); err != nil {
td.Fatal(ctx, err)
}
ci, err := gerrit_steps.GetIssueProperties(ctx, g, i)
if err != nil {
td.Fatal(ctx, err)
}
if !util.In(ci.Owner.Email, reviewers) {
reviewers = append(reviewers, ci.Owner.Email)
}
}
if err := gerrit_steps.UploadCL(ctx, g, co, *gerritProject, "master", rs.Revision, "Update Go Deps", reviewers, isTryJob); err != nil {
td.Fatal(ctx, err)
}
}