golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/task/version.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package task
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"go/ast"
    12  	"go/parser"
    13  	"go/token"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"golang.org/x/build/gerrit"
    19  	"golang.org/x/build/internal/workflow"
    20  )
    21  
    22  // VersionTasks contains tasks related to versioning the release.
    23  type VersionTasks struct {
    24  	Gerrit     GerritClient
    25  	CloudBuild CloudBuildClient
    26  	GoProject  string
    27  	UpdateProxyTestRepoTasks
    28  }
    29  
    30  // GetCurrentMajor returns the most recent major Go version, and the time at
    31  // which its tag was created.
    32  func (t *VersionTasks) GetCurrentMajor(ctx context.Context) (int, time.Time, error) {
    33  	_, currentMajor, currentMajorTag, err := t.tagInfo(ctx)
    34  	if err != nil {
    35  		return 0, time.Time{}, err
    36  	}
    37  	info, err := t.Gerrit.GetTag(ctx, t.GoProject, currentMajorTag)
    38  	if err != nil {
    39  		return 0, time.Time{}, err
    40  	}
    41  	return currentMajor, info.Created.Time(), nil
    42  }
    43  
    44  func (t *VersionTasks) tagInfo(ctx context.Context) (tags map[string]bool, currentMajor int, currentMajorTag string, _ error) {
    45  	tagList, err := t.Gerrit.ListTags(ctx, t.GoProject)
    46  	if err != nil {
    47  		return nil, 0, "", err
    48  	}
    49  	tags = map[string]bool{}
    50  	for _, tag := range tagList {
    51  		tags[tag] = true
    52  	}
    53  	// Find the most recently released major version.
    54  	// Going down from a high number is convenient for testing.
    55  	for currentMajor := 100; currentMajor > 0; currentMajor-- {
    56  		base := fmt.Sprintf("go1.%d", currentMajor)
    57  		// Handle either go1.20 or go1.21.0.
    58  		for _, tag := range []string{base, base + ".0"} {
    59  			if tags[tag] {
    60  				return tags, currentMajor, tag, nil
    61  			}
    62  		}
    63  	}
    64  	return nil, 0, "", fmt.Errorf("couldn't find the most recently released major version out of %d tags", len(tagList))
    65  }
    66  
    67  // GetNextMinorVersions returns the next minor for each of the given major series.
    68  // It uses the same format as Go tags (for example, "go1.23.4").
    69  func (t *VersionTasks) GetNextMinorVersions(ctx context.Context, majors []int) ([]string, error) {
    70  	var next []string
    71  	for _, major := range majors {
    72  		n, err := t.GetNextVersion(ctx, major, KindMinor)
    73  		if err != nil {
    74  			return nil, err
    75  		}
    76  		next = append(next, n)
    77  	}
    78  	return next, nil
    79  }
    80  
    81  // GetNextVersion returns the next for the given major series and kind of release.
    82  // It uses the same format as Go tags (for example, "go1.23.4").
    83  func (t *VersionTasks) GetNextVersion(ctx context.Context, major int, kind ReleaseKind) (string, error) {
    84  	tags, _, _, err := t.tagInfo(ctx)
    85  	if err != nil {
    86  		return "", err
    87  	}
    88  	findUnused := func(v string) (string, error) {
    89  		for {
    90  			if !tags[v] {
    91  				return v, nil
    92  			}
    93  			v, err = nextVersion(v)
    94  			if err != nil {
    95  				return "", err
    96  			}
    97  		}
    98  	}
    99  	switch kind {
   100  	case KindMinor:
   101  		return findUnused(fmt.Sprintf("go1.%d.1", major))
   102  	case KindBeta:
   103  		return findUnused(fmt.Sprintf("go1.%dbeta1", major))
   104  	case KindRC:
   105  		return findUnused(fmt.Sprintf("go1.%drc1", major))
   106  	case KindMajor:
   107  		return fmt.Sprintf("go1.%d.0", major), nil
   108  	default:
   109  		return "", fmt.Errorf("unknown release kind %v", kind)
   110  	}
   111  }
   112  
   113  // GetDevelVersion returns the current major Go 1.x version in development.
   114  //
   115  // This value is determined by reading the value of the Version constant in
   116  // the internal/goversion package of the main Go repository at HEAD commit.
   117  func (t *VersionTasks) GetDevelVersion(ctx context.Context) (int, error) {
   118  	mainBranch, err := t.Gerrit.ReadBranchHead(ctx, t.GoProject, "HEAD")
   119  	if err != nil {
   120  		return 0, err
   121  	}
   122  	tipCommit, err := t.Gerrit.ReadBranchHead(ctx, t.GoProject, mainBranch)
   123  	if err != nil {
   124  		return 0, err
   125  	}
   126  	// Fetch the goversion.go file, extract the declaration from the parsed AST.
   127  	//
   128  	// This is a pragmatic approach that relies on the trajectory of the
   129  	// internal/goversion package being predictable and unlikely to change.
   130  	// If that stops being true, this implementation is easy to re-write.
   131  	const goversionPath = "src/internal/goversion/goversion.go"
   132  	b, err := t.Gerrit.ReadFile(ctx, t.GoProject, tipCommit, goversionPath)
   133  	if errors.Is(err, gerrit.ErrResourceNotExist) {
   134  		return 0, fmt.Errorf("did not find goversion.go file (%v); possibly the internal/goversion package changed (as it's permitted to)", err)
   135  	} else if err != nil {
   136  		return 0, err
   137  	}
   138  	f, err := parser.ParseFile(token.NewFileSet(), goversionPath, b, 0)
   139  	if err != nil {
   140  		return 0, err
   141  	}
   142  	for _, d := range f.Decls {
   143  		g, ok := d.(*ast.GenDecl)
   144  		if !ok {
   145  			continue
   146  		}
   147  		for _, s := range g.Specs {
   148  			v, ok := s.(*ast.ValueSpec)
   149  			if !ok || len(v.Names) != 1 || v.Names[0].String() != "Version" || len(v.Values) != 1 {
   150  				continue
   151  			}
   152  			l, ok := v.Values[0].(*ast.BasicLit)
   153  			if !ok || l.Kind != token.INT {
   154  				continue
   155  			}
   156  			return strconv.Atoi(l.Value)
   157  		}
   158  	}
   159  	return 0, fmt.Errorf("did not find Version declaration in %s; possibly the internal/goversion package changed (as it's permitted to)", goversionPath)
   160  }
   161  
   162  func nextVersion(version string) (string, error) {
   163  	lastNonDigit := strings.LastIndexFunc(version, func(r rune) bool {
   164  		return r < '0' || r > '9'
   165  	})
   166  	if lastNonDigit == -1 || len(version) == lastNonDigit {
   167  		return "", fmt.Errorf("malformatted Go version %q", version)
   168  	}
   169  	n, err := strconv.Atoi(version[lastNonDigit+1:])
   170  	if err != nil {
   171  		return "", fmt.Errorf("malformatted Go version %q (%v)", version, err)
   172  	}
   173  	return fmt.Sprintf("%s%d", version[:lastNonDigit+1], n+1), nil
   174  }
   175  
   176  func (t *VersionTasks) GenerateVersionFile(_ *workflow.TaskContext, version string, timestamp time.Time) (string, error) {
   177  	return fmt.Sprintf("%v\ntime %v\n", version, timestamp.Format(time.RFC3339)), nil
   178  }
   179  
   180  // CreateAutoSubmitVersionCL mails an auto-submit change to update VERSION file on branch.
   181  func (t *VersionTasks) CreateAutoSubmitVersionCL(ctx *workflow.TaskContext, branch, version string, reviewers []string, versionFile string) (string, error) {
   182  	return t.Gerrit.CreateAutoSubmitChange(ctx, gerrit.ChangeInput{
   183  		Project: t.GoProject,
   184  		Branch:  branch,
   185  		Subject: fmt.Sprintf("[%v] %v", branch, version),
   186  	}, reviewers, map[string]string{
   187  		"VERSION": versionFile,
   188  	})
   189  }
   190  
   191  // AwaitCL waits for the specified CL to be submitted, and returns the new
   192  // branch head. Callers can pass baseCommit, the current branch head, to verify
   193  // that no CLs were submitted between when the CL was created and when it was
   194  // merged. If changeID is blank because the intended CL was a no-op, baseCommit
   195  // is returned immediately.
   196  func (t *VersionTasks) AwaitCL(ctx *workflow.TaskContext, changeID, baseCommit string) (string, error) {
   197  	if changeID == "" {
   198  		ctx.Printf("No CL was necessary")
   199  		return baseCommit, nil
   200  	}
   201  
   202  	ctx.Printf("Awaiting review/submit of %v", ChangeLink(changeID))
   203  	return AwaitCondition(ctx, 10*time.Second, func() (string, bool, error) {
   204  		return t.Gerrit.Submitted(ctx, changeID, baseCommit)
   205  	})
   206  }
   207  
   208  // ReadBranchHead returns the current HEAD revision of branch.
   209  func (t *VersionTasks) ReadBranchHead(ctx *workflow.TaskContext, branch string) (string, error) {
   210  	return t.Gerrit.ReadBranchHead(ctx, t.GoProject, branch)
   211  }
   212  
   213  // TagRelease tags commit as version.
   214  func (t *VersionTasks) TagRelease(ctx *workflow.TaskContext, version, commit string) error {
   215  	return t.Gerrit.Tag(ctx, t.GoProject, version, commit)
   216  }
   217  
   218  func (t *VersionTasks) CreateUpdateStdlibIndexCL(ctx *workflow.TaskContext, reviewers []string, version string) (string, error) {
   219  	build, err := t.CloudBuild.RunScript(ctx, "go generate ./internal/stdlib", "tools", []string{"internal/stdlib/manifest.go"})
   220  	if err != nil {
   221  		return "", err
   222  	}
   223  
   224  	files, err := buildToOutputs(ctx, t.CloudBuild, build)
   225  	if err != nil {
   226  		return "", err
   227  	}
   228  
   229  	changeInput := gerrit.ChangeInput{
   230  		Project: "tools",
   231  		Subject: fmt.Sprintf("internal/stdlib: update stdlib index for %s\n\nFor golang/go#38706.", strings.Replace(version, "go", "Go ", 1)),
   232  		Branch:  "master",
   233  	}
   234  	return t.Gerrit.CreateAutoSubmitChange(ctx, changeInput, reviewers, files)
   235  }
   236  
   237  // UnwaitWaitReleaseCLs changes all open Gerrit CLs with hashtag "wait-release" into "ex-wait-release".
   238  // This is done once at the opening of a release cycle, currently via a standalone workflow.
   239  func (t *VersionTasks) UnwaitWaitReleaseCLs(ctx *workflow.TaskContext) (result struct{}, _ error) {
   240  	waitingCLs, err := t.Gerrit.QueryChanges(ctx, "status:open hashtag:wait-release")
   241  	if err != nil {
   242  		return struct{}{}, nil
   243  	}
   244  	ctx.Printf("Processing %d open Gerrit CL with wait-release hashtag.", len(waitingCLs))
   245  	for _, cl := range waitingCLs {
   246  		const dryRun = false
   247  		if dryRun {
   248  			ctx.Printf("[dry run] Would've unwaited CL %d (%.32s…).", cl.ChangeNumber, cl.Subject)
   249  			continue
   250  		}
   251  		err := t.Gerrit.SetHashtags(ctx, cl.ID, gerrit.HashtagsInput{
   252  			Remove: []string{"wait-release"},
   253  			Add:    []string{"ex-wait-release"},
   254  		})
   255  		if err != nil {
   256  			return struct{}{}, err
   257  		}
   258  		ctx.Printf("Unwaited CL %d (%.32s…).", cl.ChangeNumber, cl.Subject)
   259  		time.Sleep(3 * time.Second) // Take a moment between updating CLs to avoid a high rate of modify operations.
   260  	}
   261  	return struct{}{}, nil
   262  }