blob: c3b9a78344b4e55238d3a6162540f3604bf3bf24 [file] [log] [blame]
package task
import (
"fmt"
"strconv"
"strings"
"time"
"golang.org/x/build/buildlet"
"golang.org/x/build/gerrit"
"golang.org/x/build/internal/workflow"
"golang.org/x/net/context"
)
// VersionTasks contains tasks related to versioning the release.
type VersionTasks struct {
Gerrit GerritClient
GerritURL string
GoProject string
CreateBuildlet func(context.Context, string) (buildlet.RemoteClient, error)
LatestGoBinaries func(context.Context) (string, error)
}
func (t *VersionTasks) GetCurrentMajor(ctx context.Context) (int, error) {
_, currentMajor, err := t.tagInfo(ctx)
return currentMajor, err
}
func (t *VersionTasks) tagInfo(ctx context.Context) (tags map[string]bool, currentMajor int, _ error) {
tagList, err := t.Gerrit.ListTags(ctx, t.GoProject)
if err != nil {
return nil, 0, err
}
tags = map[string]bool{}
for _, tag := range tagList {
tags[tag] = true
}
// Find the most recently released major version.
// Going down from a high number is convenient for testing.
currentMajor = 100
for ; ; currentMajor-- {
if tags[fmt.Sprintf("go1.%d", currentMajor)] {
break
}
}
return tags, currentMajor, nil
}
// GetNextVersions returns the next for each of the given types of release.
func (t *VersionTasks) GetNextVersions(ctx context.Context, kinds []ReleaseKind) ([]string, error) {
var next []string
for _, k := range kinds {
n, err := t.GetNextVersion(ctx, k)
if err != nil {
return nil, err
}
next = append(next, n)
}
return next, nil
}
// GetNextVersion returns the next for the given type of release.
func (t *VersionTasks) GetNextVersion(ctx context.Context, kind ReleaseKind) (string, error) {
tags, currentMajor, err := t.tagInfo(ctx)
if err != nil {
return "", err
}
findUnused := func(v string) (string, error) {
for {
if !tags[v] {
return v, nil
}
v, err = nextVersion(v)
if err != nil {
return "", err
}
}
}
switch kind {
case KindCurrentMinor:
return findUnused(fmt.Sprintf("go1.%d.1", currentMajor))
case KindPrevMinor:
return findUnused(fmt.Sprintf("go1.%d.1", currentMajor-1))
case KindBeta:
return findUnused(fmt.Sprintf("go1.%dbeta1", currentMajor+1))
case KindRC:
return findUnused(fmt.Sprintf("go1.%drc1", currentMajor+1))
case KindMajor:
return fmt.Sprintf("go1.%d", currentMajor+1), nil
}
return "", fmt.Errorf("unknown release kind %v", kind)
}
func nextVersion(version string) (string, error) {
lastNonDigit := strings.LastIndexFunc(version, func(r rune) bool {
return r < '0' || r > '9'
})
if lastNonDigit == -1 || len(version) == lastNonDigit {
return "", fmt.Errorf("malformatted Go version %q", version)
}
n, err := strconv.Atoi(version[lastNonDigit+1:])
if err != nil {
return "", fmt.Errorf("malformatted Go version %q (%v)", version, err)
}
return fmt.Sprintf("%s%d", version[:lastNonDigit+1], n+1), nil
}
// CreateAutoSubmitVersionCL mails an auto-submit change to update VERSION on branch.
func (t *VersionTasks) CreateAutoSubmitVersionCL(ctx *workflow.TaskContext, branch string, reviewers []string, version string) (string, error) {
return t.Gerrit.CreateAutoSubmitChange(ctx, gerrit.ChangeInput{
Project: t.GoProject,
Branch: branch,
Subject: fmt.Sprintf("[%v] %v", branch, version),
}, reviewers, map[string]string{
"VERSION": version,
})
}
// AwaitCL waits for the specified CL to be submitted, and returns the new
// branch head. Callers can pass baseCommit, the current branch head, to verify
// that no CLs were submitted between when the CL was created and when it was
// merged. If changeID is blank because the intended CL was a no-op, baseCommit
// is returned immediately.
func (t *VersionTasks) AwaitCL(ctx *workflow.TaskContext, changeID, baseCommit string) (string, error) {
if changeID == "" {
ctx.Printf("No CL was necessary")
return baseCommit, nil
}
ctx.Printf("Awaiting review/submit of %v", ChangeLink(changeID))
return AwaitCondition(ctx, 10*time.Second, func() (string, bool, error) {
return t.Gerrit.Submitted(ctx, changeID, baseCommit)
})
}
// ReadBranchHead returns the current HEAD revision of branch.
func (t *VersionTasks) ReadBranchHead(ctx *workflow.TaskContext, branch string) (string, error) {
return t.Gerrit.ReadBranchHead(ctx, t.GoProject, branch)
}
// TagRelease tags commit as version.
func (t *VersionTasks) TagRelease(ctx *workflow.TaskContext, version, commit string) error {
return t.Gerrit.Tag(ctx, t.GoProject, version, commit)
}
func (t *VersionTasks) CreateUpdateStdlibIndexCL(ctx *workflow.TaskContext, branch string, reviewers []string, version string) (string, error) {
var files = make(map[string]string) // Map key is relative path, and map value is file content.
binaries, err := t.LatestGoBinaries(ctx)
if err != nil {
return "", err
}
bc, err := t.CreateBuildlet(ctx, "linux-amd64-longtest")
if err != nil {
return "", err
}
defer bc.Close()
if err := bc.PutTarFromURL(ctx, binaries, ""); err != nil {
return "", err
}
toolsTarURL := fmt.Sprintf("%s/%s/+archive/%s.tar.gz", t.GerritURL, "tools", "master")
if err := bc.PutTarFromURL(ctx, toolsTarURL, "tools"); err != nil {
return "", err
}
writer := &LogWriter{Logger: ctx}
go writer.Run(ctx)
remoteErr, execErr := bc.Exec(ctx, "go/bin/go", buildlet.ExecOpts{
Dir: "tools",
Args: []string{"generate", "./internal/imports"},
Output: writer,
})
if execErr != nil {
return "", fmt.Errorf("Exec failed: %v", execErr)
}
if remoteErr != nil {
return "", fmt.Errorf("Command failed: %v", remoteErr)
}
tgz, err := bc.GetTar(context.Background(), "tools/internal/imports")
if err != nil {
return "", err
}
defer tgz.Close()
tools, err := tgzToMap(tgz)
if err != nil {
return "", err
}
files["internal/imports/zstdlib.go"] = tools["zstdlib.go"]
major, err := t.GetCurrentMajor(ctx)
if err != nil {
return "", err
}
changeInput := gerrit.ChangeInput{
Project: "tools",
Subject: fmt.Sprintf("internal/imports: update stdlib index for %v", major),
Branch: "master",
}
return t.Gerrit.CreateAutoSubmitChange(ctx, changeInput, reviewers, files)
}