blob: b472ba1f8eca0aa88dacf6622aee42e31a5121b3 [file] [log] [blame]
package try
import (
"context"
"fmt"
"io/ioutil"
"os"
"regexp"
"strconv"
"github.com/urfave/cli/v2"
"go.skia.org/infra/go/auth"
"go.skia.org/infra/go/common"
"go.skia.org/infra/go/gerrit"
"go.skia.org/infra/go/git"
"go.skia.org/infra/go/gitiles"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/supported_branches/cmd/new-branch/helper"
)
const (
refsHeadsPrefix = "refs/heads/"
chromeRefTmpl = refsHeadsPrefix + "chrome/m%d"
flagDryRun = "dry-run"
gerritProject = "skia"
milestoneFile = "include/core/SkMilestone.h"
milestoneTmpl = "#define SK_MILESTONE %s"
supportedChromeBranches = 3
updateMilestoneCommitMsgTmpl = "Update Skia milestone to %d"
)
var (
milestoneRegex = regexp.MustCompile(fmt.Sprintf(milestoneTmpl, `(\d+)`))
excludeTrybotsOnReleaseBranches = []string{
"chromium.*",
".*Android_Framework.*",
".*G3_Framework.*",
".*CanvasKit.*",
".*PathKit.*",
}
)
// Command returns a cli.Command instance which represents the "release-branch"
// command.
func Command() *cli.Command {
return &cli.Command{
Name: "release-branch",
Usage: "release-branch <commit hash>",
Description: "Create a new Skia release branch at the given hash.",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: flagDryRun,
Value: false,
Usage: "Create the branch CLs but do not submit them or create the branch itself.",
},
},
Action: func(ctx *cli.Context) error {
args := ctx.Args().Slice()
if len(args) != 1 {
return skerr.Fmt("Exactly one positional argument is expected.")
}
return releaseBranch(ctx.Context, args[0], ctx.Bool(flagDryRun))
},
}
}
// releaseBranch performs the actions necessary to create a new Skia release
// branch.
func releaseBranch(ctx context.Context, commit string, dryRun bool) error {
// Setup.
ts, err := auth.NewDefaultTokenSource(true, auth.SCOPE_GERRIT)
if err != nil {
return skerr.Wrap(err)
}
client := httputils.DefaultClientConfig().WithTokenSource(ts).Client()
repo := gitiles.NewRepo(common.REPO_SKIA, client)
g, err := gerrit.NewGerrit(gerrit.GerritSkiaURL, client)
if err != nil {
return skerr.Wrap(err)
}
// Retrieve the current milestone.
baseCommit, err := repo.ResolveRef(ctx, git.DefaultRef)
if err != nil {
return skerr.Wrap(err)
}
milestone, milestoneFileContents, err := getCurrentMilestone(ctx, repo, baseCommit)
if err != nil {
return skerr.Wrap(err)
}
newBranch := fmt.Sprintf(chromeRefTmpl, milestone)
oldBranch := fmt.Sprintf(chromeRefTmpl, milestone-supportedChromeBranches)
fmt.Println(fmt.Sprintf("Creating branch %s and removing support (eg. CQ) for %s", newBranch, oldBranch))
fmt.Println(fmt.Sprintf("Creating branch %s...", newBranch))
if err := createNewBranch(ctx, newBranch, commit, dryRun); err != nil {
return skerr.Wrap(err)
}
fmt.Println("Creating CL to update milestone...")
if err := updateMilestone(ctx, g, baseCommit, milestone+1, milestoneFileContents, dryRun); err != nil {
return skerr.Wrap(err)
}
fmt.Println("Creating CL to update CQ...")
if err := updateInfraConfig(ctx, g, oldBranch, newBranch, dryRun); err != nil {
return skerr.Wrap(err)
}
return nil
}
// getCurrentMilestone retrieves the current milestone value.
func getCurrentMilestone(ctx context.Context, repo gitiles.GitilesRepo, baseCommit string) (int, string, error) {
contents, err := repo.ReadFileAtRef(ctx, milestoneFile, baseCommit)
if err != nil {
return 0, "", skerr.Wrap(err)
}
match := milestoneRegex.FindAllStringSubmatch(string(contents), 1)
if len(match) != 1 {
return 0, "", skerr.Fmt("Unable to parse milestone number from: %s", string(contents))
}
if len(match[0]) != 2 {
return 0, "", skerr.Fmt("Unable to parse milestone number from: %s", string(contents))
}
milestone, err := strconv.Atoi(match[0][1])
if err != nil {
return 0, "", skerr.Wrap(err)
}
return milestone, string(contents), nil
}
// createNewBranch creates a branch with the given name at the given commit.
func createNewBranch(ctx context.Context, name, commit string, dryRun bool) (rvErr error) {
// Create a temporary checkout of Skia.
tmp, err := ioutil.TempDir("", "")
if err != nil {
return skerr.Wrap(err)
}
if !dryRun {
defer func() {
if err := os.RemoveAll(tmp); err != nil {
if rvErr == nil {
rvErr = err
}
}
}()
}
co, err := git.NewCheckout(ctx, common.REPO_SKIA, tmp)
if err != nil {
return skerr.Wrap(err)
}
// Create and push the branch.
if _, err := co.Git(ctx, "checkout", "-b", name); err != nil {
return skerr.Wrap(err)
}
if _, err := co.Git(ctx, "reset", "--hard", commit); err != nil {
return skerr.Wrap(err)
}
if dryRun {
fmt.Fprintf(os.Stderr, "Branch %s created in %s; not pushing because --dry-run was specified.\n", name, co.Dir())
} else {
if _, err := co.Git(ctx, "push", "--set-upstream", "origin", name); err != nil {
return skerr.Wrap(err)
}
}
return nil
}
// updateMilestone creates a CL to update the Skia milestone.
func updateMilestone(ctx context.Context, g gerrit.GerritInterface, baseCommit string, milestone int, oldMilestoneContents string, dryRun bool) error {
commitMsg := fmt.Sprintf(updateMilestoneCommitMsgTmpl, milestone)
newContents := milestoneRegex.ReplaceAllString(oldMilestoneContents, fmt.Sprintf(milestoneTmpl, strconv.Itoa(milestone)))
changes := map[string]string{
milestoneFile: newContents,
}
ci, err := gerrit.CreateCLWithChanges(ctx, g, gerritProject, git.DefaultBranch, commitMsg, baseCommit, changes, !dryRun)
if ci != nil {
fmt.Println(fmt.Sprintf("Uploaded change %s", g.Url(ci.Issue)))
}
return skerr.Wrap(err)
}
// updateInfraConfig updates the infra/config branch to edit the supported
// branches and commit queue config to add the new branch and remove the old.
func updateInfraConfig(ctx context.Context, g gerrit.GerritInterface, oldBranch, newBranch string, dryRun bool) error {
owner, err := g.GetUserEmail(ctx)
if err != nil {
return skerr.Wrap(err)
}
return skerr.Wrap(helper.AddSupportedBranch(common.REPO_SKIA, newBranch, owner, oldBranch, excludeTrybotsOnReleaseBranches, !dryRun))
}