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 }