Skip to content

Commit a4d4fa3

Browse files
h9jianggopherbot
authored andcommitted
internal/task: update x/tools dependency and create a CL for review
updateXToolsDependency will drop the replace directive in gopls sub module and pin the x/tools version to the head of the current gopls release branch. A local relui screenshot is at https://go.dev/issue/57643#issuecomment-2263725465 For golang/go#57643 Change-Id: I0d9f4462b71b394ff432bc65142cf35e70977e6a Reviewed-on: https://go-review.googlesource.com/c/build/+/602516 Reviewed-by: Dmitri Shuralyov <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]> Auto-Submit: Hongxiang Jiang <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 7cb6469 commit a4d4fa3

File tree

2 files changed

+215
-11
lines changed

2 files changed

+215
-11
lines changed

internal/task/releasegopls.go

Lines changed: 153 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"errors"
99
"fmt"
1010
"slices"
11+
"strconv"
1112
"strings"
1213
"time"
1314

@@ -34,7 +35,9 @@ func (r *ReleaseGoplsTasks) NewDefinition() *wf.Definition {
3435
semversion := wf.Task1(wd, "validating input version", r.isValidVersion, version)
3536
branchCreated := wf.Action1(wd, "creating new branch if minor release", r.createBranchIfMinor, semversion)
3637
changeID := wf.Task2(wd, "updating branch's codereview.cfg", r.updateCodeReviewConfig, semversion, reviewers, wf.After(branchCreated))
37-
_ = wf.Action1(wd, "await config CL submission", r.AwaitSubmission, changeID)
38+
submitted := wf.Action1(wd, "await config CL submission", r.AwaitSubmission, changeID)
39+
changeID = wf.Task2(wd, "updating gopls' x/tools dependency", r.updateXToolsDependency, semversion, reviewers, wf.After(submitted))
40+
_ = wf.Action1(wd, "await gopls' x/tools dependency CL submission", r.AwaitSubmission, changeID)
3841
return wd
3942
}
4043

@@ -73,10 +76,29 @@ func (r *ReleaseGoplsTasks) createBranchIfMinor(ctx *wf.TaskContext, semv semver
7376
return err
7477
}
7578

76-
// updateCodeReviewConfig ensures codereview.cfg contains the expected
77-
// configuration.
79+
// openCL checks if an open CL with the given title exists in the specified
80+
// branch.
7881
//
79-
// It returns the change ID, or "" if the CL was not created.
82+
// It returns an empty string if no such CL is found, otherwise it returns the
83+
// CL's change ID.
84+
func (r *ReleaseGoplsTasks) openCL(ctx *wf.TaskContext, branch, title string) (string, error) {
85+
// Query for an existing pending config CL, to avoid duplication.
86+
query := fmt.Sprintf(`message:%q status:open owner:[email protected] repo:tools branch:%q -age:7d`, title, branch)
87+
changes, err := r.Gerrit.QueryChanges(ctx, query)
88+
if err != nil {
89+
return "", err
90+
}
91+
if len(changes) == 0 {
92+
return "", nil
93+
}
94+
95+
return changes[0].ChangeID, nil
96+
}
97+
98+
// updateCodeReviewConfig checks if codereview.cfg has the desired configuration.
99+
//
100+
// It returns the change ID required to update the config if changes are needed,
101+
// otherwise it returns an empty string indicating no update is necessary.
80102
func (r *ReleaseGoplsTasks) updateCodeReviewConfig(ctx *wf.TaskContext, semv semversion, reviewers []string) (string, error) {
81103
const configFile = "codereview.cfg"
82104
const configFmt = `issuerepo: golang/go
@@ -87,15 +109,13 @@ parent-branch: master
87109
branch := goplsReleaseBranchName(semv)
88110
clTitle := fmt.Sprintf("all: update %s for %s", configFile, branch)
89111

90-
// Query for an existing pending config CL, to avoid duplication.
91-
query := fmt.Sprintf(`message:%q status:open owner:[email protected] repo:tools branch:%q -age:7d`, clTitle, branch)
92-
changes, err := r.Gerrit.QueryChanges(ctx, query)
112+
openCL, err := r.openCL(ctx, branch, clTitle)
93113
if err != nil {
94-
return "", err
114+
return "", fmt.Errorf("failed to find the open CL of title %q in branch %q: %w", clTitle, branch, err)
95115
}
96-
if len(changes) > 0 {
97-
ctx.Printf("not creating CL: found existing CL %d", changes[0].ChangeNumber)
98-
return changes[0].ChangeID, nil
116+
if openCL != "" {
117+
ctx.Printf("not creating CL: found existing CL %s", openCL)
118+
return openCL, nil
99119
}
100120

101121
head, err := r.Gerrit.ReadBranchHead(ctx, "tools", branch)
@@ -128,6 +148,110 @@ parent-branch: master
128148
return r.Gerrit.CreateAutoSubmitChange(ctx, changeInput, reviewers, files)
129149
}
130150

151+
// nextPrerelease go through the tags in tools repo that matches with the given
152+
// version and find the next pre-release version.
153+
func (r *ReleaseGoplsTasks) nextPrerelease(ctx *wf.TaskContext, semv semversion) (int, error) {
154+
tags, err := r.Gerrit.ListTags(ctx, "tools")
155+
if err != nil {
156+
return -1, fmt.Errorf("failed to list tags for tools repo: %w", err)
157+
}
158+
159+
max := 0
160+
for _, tag := range tags {
161+
v, ok := strings.CutPrefix(tag, "gopls/")
162+
if !ok {
163+
continue
164+
}
165+
cur, ok := parseSemver(v)
166+
if !ok {
167+
continue
168+
}
169+
if cur.Major != semv.Major || cur.Minor != semv.Minor || cur.Patch != semv.Patch {
170+
continue
171+
}
172+
pre, err := cur.prereleaseVersion()
173+
if err != nil {
174+
continue
175+
}
176+
177+
if pre > max {
178+
max = pre
179+
}
180+
}
181+
182+
return max + 1, nil
183+
}
184+
185+
// updateXToolsDependency ensures gopls sub module have the correct x/tools
186+
// version as dependency.
187+
//
188+
// It returns the change ID, or "" if the CL was not created.
189+
func (r *ReleaseGoplsTasks) updateXToolsDependency(ctx *wf.TaskContext, semv semversion, reviewers []string) (string, error) {
190+
const scriptFmt = `cp gopls/go.mod gopls/go.mod.before
191+
cp gopls/go.sum gopls/go.sum.before
192+
cd gopls
193+
go mod edit -dropreplace=golang.org/x/tools
194+
go get -u golang.org/x/tools@%s
195+
go mod tidy -compat=1.19
196+
`
197+
198+
pre, err := r.nextPrerelease(ctx, semv)
199+
if err != nil {
200+
return "", fmt.Errorf("failed to find the next prerelease version: %w", err)
201+
}
202+
203+
branch := goplsReleaseBranchName(semv)
204+
clTitle := fmt.Sprintf("gopls: update go.mod for v%v.%v.%v-pre.%v", semv.Major, semv.Minor, semv.Patch, pre)
205+
openCL, err := r.openCL(ctx, branch, clTitle)
206+
if err != nil {
207+
return "", fmt.Errorf("failed to find the open CL of title %q in branch %q: %w", clTitle, branch, err)
208+
}
209+
if openCL != "" {
210+
ctx.Printf("not creating CL: found existing CL %s", openCL)
211+
return openCL, nil
212+
}
213+
214+
outputFiles := []string{"gopls/go.mod.before", "gopls/go.mod", "gopls/go.sum.before", "gopls/go.sum"}
215+
// TODO(hxjiang): Replacing branch with the latest non-pinned commit in the
216+
// release branch. Rationale:
217+
// 1. Module proxy might return an outdated commit when using the branch name
218+
// (to be confirmed with samthanawalla@).
219+
// 2. Pinning x/tools using the latest commit from a branch isn't idempotent.
220+
// It's best to avoid pinning x/tools to a version that's effectively another
221+
// pin.
222+
build, err := r.CloudBuild.RunScript(ctx, fmt.Sprintf(scriptFmt, branch), "tools", outputFiles)
223+
if err != nil {
224+
return "", err
225+
}
226+
227+
outputs, err := buildToOutputs(ctx, r.CloudBuild, build)
228+
if err != nil {
229+
return "", err
230+
}
231+
232+
changedFiles := map[string]string{}
233+
for i := 0; i < len(outputFiles); i += 2 {
234+
before, after := outputs[outputFiles[i]], outputs[outputFiles[i+1]]
235+
if before != after {
236+
changedFiles[outputFiles[i+1]] = after
237+
}
238+
}
239+
240+
// Skip CL creation as nothing changed.
241+
if len(changedFiles) == 0 {
242+
return "", nil
243+
}
244+
245+
changeInput := gerrit.ChangeInput{
246+
Project: "tools",
247+
Branch: branch,
248+
Subject: fmt.Sprintf("%s\n\nThis is an automated CL which updates the go.mod go.sum.", clTitle),
249+
}
250+
251+
ctx.Printf("creating auto-submit change under branch %q in x/tools repo.", branch)
252+
return r.Gerrit.CreateAutoSubmitChange(ctx, changeInput, reviewers, changedFiles)
253+
}
254+
131255
// AwaitSubmission waits for the CL with the given change ID to be submitted.
132256
//
133257
// The return value is the submitted commit hash, or "" if changeID is "".
@@ -179,6 +303,24 @@ func parseSemver(v string) (_ semversion, ok bool) {
179303
return parsed, ok
180304
}
181305

306+
func (s *semversion) prereleaseVersion() (int, error) {
307+
parts := strings.Split(s.Pre, ".")
308+
if len(parts) == 1 {
309+
return -1, fmt.Errorf(`prerelease version does not contain any "."`)
310+
}
311+
312+
if len(parts) > 2 {
313+
return -1, fmt.Errorf(`prerelease version contains %v "."`, len(parts)-1)
314+
}
315+
316+
pre, err := strconv.Atoi(parts[1])
317+
if err != nil {
318+
return -1, fmt.Errorf("failed to convert prerelease version to int %q: %w", pre, err)
319+
}
320+
321+
return pre, nil
322+
}
323+
182324
// possibleGoplsVersions identifies suitable versions for the upcoming release
183325
// based on the current tags in the repo.
184326
func (r *ReleaseGoplsTasks) possibleGoplsVersions(ctx *wf.TaskContext) ([]string, error) {

internal/task/releasegopls_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,3 +312,65 @@ parent-branch: master
312312
})
313313
}
314314
}
315+
316+
func TestNextPrerelease(t *testing.T) {
317+
ctx := context.Background()
318+
testcases := []struct {
319+
name string
320+
tags []string
321+
version string
322+
want int
323+
}{
324+
{
325+
name: "next pre-release is 2",
326+
tags: []string{"gopls/v0.16.0-pre.0", "gopls/v0.16.0-pre.1"},
327+
version: "v0.16.0",
328+
want: 2,
329+
},
330+
{
331+
name: "next pre-release is 2 regardless of other minor or patch version",
332+
tags: []string{"gopls/v0.16.0-pre.0", "gopls/v0.16.0-pre.1", "gopls/v0.16.1-pre.1", "gopls/v0.2.0-pre.3"},
333+
version: "v0.16.0",
334+
want: 2,
335+
},
336+
{
337+
name: "next pre-release is 2 regardless of non-int prerelease version",
338+
tags: []string{"gopls/v0.16.0-pre.0", "gopls/v0.16.0-pre.1", "gopls/v0.16.0-pre.foo", "gopls/v0.16.0-pre.bar"},
339+
version: "v0.16.0",
340+
want: 2,
341+
},
342+
}
343+
344+
for _, tc := range testcases {
345+
t.Run(tc.name, func(t *testing.T) {
346+
tools := NewFakeRepo(t, "tools")
347+
commit := tools.Commit(map[string]string{
348+
"go.mod": "module golang.org/x/tools\n",
349+
"go.sum": "\n",
350+
})
351+
352+
for _, tag := range tc.tags {
353+
tools.Tag(tag, commit)
354+
}
355+
356+
gerrit := NewFakeGerrit(t, tools)
357+
358+
tasks := &ReleaseGoplsTasks{
359+
Gerrit: gerrit,
360+
}
361+
362+
semv, ok := parseSemver(tc.version)
363+
if !ok {
364+
t.Fatalf("parseSemver(%q) should success", tc.version)
365+
}
366+
got, err := tasks.nextPrerelease(&workflow.TaskContext{Context: ctx, Logger: &testLogger{t, ""}}, semv)
367+
if err != nil {
368+
t.Fatalf("nextPrerelease(%q) should not return error: %v", tc.version, err)
369+
}
370+
371+
if tc.want != got {
372+
t.Errorf("nextPrerelease(%q) = %v want %v", tc.version, got, tc.want)
373+
}
374+
})
375+
}
376+
}

0 commit comments

Comments
 (0)