internal/task: add new selection input to gopls pre-release flow

Coordinator have three options to run the pre-release flow:
* next minor or next patch, the flow will interpret the user intention.
* use explicit version, the user must input the version explicitly.

A local relui screenshot is at https://go.dev/issue/57643#issuecomment-2310730081

For golang/go#57643

Change-Id: I9621da87f248c79597641162682fb2d108bc19f9
Reviewed-on: https://go-review.googlesource.com/c/build/+/608415
Auto-Submit: Hongxiang Jiang <hxjiang@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Hongxiang Jiang 2024-08-26 17:45:39 +00:00 коммит произвёл Gopher Robot
Родитель 8ac64ccecd
Коммит d7fe89019c
3 изменённых файлов: 220 добавлений и 73 удалений

Просмотреть файл

@ -33,22 +33,27 @@ type ReleaseGoplsTasks struct {
func (r *ReleaseGoplsTasks) NewPrereleaseDefinition() *wf.Definition {
wd := wf.New(wf.ACL{Groups: []string{groups.ToolsTeam}})
// TODO(hxjiang): provide potential release versions in the relui where the
// coordinator can choose which version to release instead of manual input.
version := wf.Param(wd, wf.ParamDef[string]{Name: "version"})
// versionBumpStrategy specifies the desired release type: next minor or next
// patch.
// This should be the default choice for most releases.
versionBumpStrategy := wf.Param(wd, nextVersionParam)
// inputVersion allows manual override of the version, bypassing the version
// bump strategy.
// Use with caution.
inputVersion := wf.Param(wd, wf.ParamDef[string]{Name: "explicit version (optional)"})
reviewers := wf.Param(wd, reviewersParam)
semv := wf.Task1(wd, "validating input version", r.isValidReleaseVersion, version)
prerelease := wf.Task1(wd, "find the pre-release version", r.nextPrerelease, semv)
semv := wf.Task2(wd, "determine the version", r.determineVersion, inputVersion, versionBumpStrategy)
prerelease := wf.Task1(wd, "find the pre-release version", r.nextPrereleaseVersion, semv)
approved := wf.Action2(wd, "wait for release coordinator approval", r.approveVersion, semv, prerelease)
issue := wf.Task1(wd, "create release git issue", r.createReleaseIssue, semv, wf.After(approved))
branchCreated := wf.Action1(wd, "creating new branch if minor release", r.createBranchIfMinor, semv, wf.After(issue))
branchCreated := wf.Action1(wd, "create new branch if minor release", r.createBranchIfMinor, semv, wf.After(issue))
configChangeID := wf.Task3(wd, "updating branch's codereview.cfg", r.updateCodeReviewConfig, semv, reviewers, issue, wf.After(branchCreated))
configChangeID := wf.Task3(wd, "update branch's codereview.cfg", r.updateCodeReviewConfig, semv, reviewers, issue, wf.After(branchCreated))
configCommit := wf.Task1(wd, "await config CL submission", clAwaiter{r.Gerrit}.awaitSubmission, configChangeID)
dependencyChangeID := wf.Task4(wd, "updating gopls' x/tools dependency", r.updateXToolsDependency, semv, prerelease, reviewers, issue, wf.After(configCommit))
dependencyChangeID := wf.Task4(wd, "update gopls' x/tools dependency", r.updateXToolsDependency, semv, prerelease, reviewers, issue, wf.After(configCommit))
dependencyCommit := wf.Task1(wd, "await gopls' x/tools dependency CL submission", clAwaiter{r.Gerrit}.awaitSubmission, dependencyChangeID)
verified := wf.Action1(wd, "verify installing latest gopls using release branch dependency commit", r.verifyGoplsInstallation, dependencyCommit)
@ -61,6 +66,58 @@ func (r *ReleaseGoplsTasks) NewPrereleaseDefinition() *wf.Definition {
return wd
}
// determineVersion returns the release version based on coordinator inputs.
//
// Returns the specified input version if provided; otherwise, interpret a new
// version based on the version bumping strategy.
func (r *ReleaseGoplsTasks) determineVersion(ctx *wf.TaskContext, inputVersion, versionBumpStrategy string) (semversion, error) {
switch versionBumpStrategy {
case "use explicit version":
if inputVersion == "" {
return semversion{}, fmt.Errorf("the input version should not be empty when choosing explicit version release")
}
if err := r.isValidReleaseVersion(ctx, inputVersion); err != nil {
return semversion{}, err
}
semv, ok := parseSemver(inputVersion)
if !ok {
return semversion{}, fmt.Errorf("input version %q can not be parsed as semantic version", inputVersion)
}
return semv, nil
case "next minor", "next patch":
return r.interpretNextRelease(ctx, versionBumpStrategy)
default:
return semversion{}, fmt.Errorf("unknown version selection strategy: %q", versionBumpStrategy)
}
}
func (r *ReleaseGoplsTasks) interpretNextRelease(ctx *wf.TaskContext, versionBumpStrategy string) (semversion, error) {
tags, err := r.Gerrit.ListTags(ctx, "tools")
if err != nil {
return semversion{}, err
}
var versions []string
for _, tag := range tags {
if v, ok := strings.CutPrefix(tag, "gopls/"); ok {
versions = append(versions, v)
}
}
version := latestVersion(versions, isReleaseVersion)
switch versionBumpStrategy {
case "next minor":
version.Minor += 1
version.Patch = 0
case "next patch":
version.Patch += 1
default:
return semversion{}, fmt.Errorf("unknown version selection strategy: %q", versionBumpStrategy)
}
return version, nil
}
func (r *ReleaseGoplsTasks) approveVersion(ctx *wf.TaskContext, semv semversion, pre string) error {
ctx.Printf("The next release candidate will be v%v.%v.%v-%s", semv.Major, semv.Minor, semv.Patch, pre)
return r.ApproveAction(ctx)
@ -230,9 +287,9 @@ parent-branch: master
return r.Gerrit.CreateAutoSubmitChange(ctx, changeInput, reviewers, files)
}
// nextPrerelease inspects the tags in tools repo that match with the given
// version and find the next prerelease version.
func (r *ReleaseGoplsTasks) nextPrerelease(ctx *wf.TaskContext, semv semversion) (string, error) {
// nextPrereleaseVersion inspects the tags in tools repo that match with the given
// version and finds the next prerelease version.
func (r *ReleaseGoplsTasks) nextPrereleaseVersion(ctx *wf.TaskContext, semv semversion) (string, error) {
cur, err := currentGoplsPrerelease(ctx, r.Gerrit, semv)
if err != nil {
return "", err
@ -410,22 +467,21 @@ func (r *ReleaseGoplsTasks) mailAnnouncement(ctx *wf.TaskContext, semv semversio
return r.SendMail(r.AnnounceMailHeader, content)
}
func (r *ReleaseGoplsTasks) isValidReleaseVersion(ctx *wf.TaskContext, ver string) (semversion, error) {
func (r *ReleaseGoplsTasks) isValidReleaseVersion(ctx *wf.TaskContext, ver string) error {
if !semver.IsValid(ver) {
return semversion{}, fmt.Errorf("the input %q version does not follow semantic version schema", ver)
return fmt.Errorf("the input %q version does not follow semantic version schema", ver)
}
versions, err := r.possibleGoplsVersions(ctx)
if err != nil {
return semversion{}, fmt.Errorf("failed to get latest Gopls version tags from x/tool: %w", err)
return fmt.Errorf("failed to get latest Gopls version tags from x/tool: %w", err)
}
if !slices.Contains(versions, ver) {
return semversion{}, fmt.Errorf("the input %q is not next version of any existing versions", ver)
return fmt.Errorf("the input %q is not next version of any existing versions", ver)
}
semver, _ := parseSemver(ver)
return semver, nil
return nil
}
// semversion is a parsed semantic version.

Просмотреть файл

@ -17,6 +17,62 @@ import (
"golang.org/x/build/internal/workflow"
)
func TestInterpretNextRelease(t *testing.T) {
tests := []struct {
name string
tags []string
bump string
want semversion
}{
{
name: "next minor version of v0.0.0 is v0.1.0",
tags: []string{"gopls/v0.0.0"},
bump: "next minor",
want: semversion{Major: 0, Minor: 1, Patch: 0},
},
{
name: "pre-release versions should be ignored",
tags: []string{"gopls/v0.0.0", "gopls/v0.1.0-pre.1", "gopls/v0.1.0-pre.2"},
bump: "next minor",
want: semversion{Major: 0, Minor: 1, Patch: 0},
},
{
name: "next patch version of v0.2.2 is v0.2.3",
tags: []string{"gopls/0.1.1", "gopls/0.2.0", "gopls/0.2.1", "gopls/v0.2.2"},
bump: "next patch",
want: semversion{Major: 0, Minor: 2, Patch: 3},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
tools := NewFakeRepo(t, "tools")
commit := tools.Commit(map[string]string{
"go.mod": "module golang.org/x/tools\n",
"go.sum": "\n",
})
for _, tag := range tc.tags {
tools.Tag(tag, commit)
}
gerrit := NewFakeGerrit(t, tools)
tasks := &ReleaseGoplsTasks{
Gerrit: gerrit,
}
got, err := tasks.interpretNextRelease(&workflow.TaskContext{Context: context.Background(), Logger: &testLogger{t, ""}}, tc.bump)
if err != nil {
t.Fatalf("interpretNextRelease(%q) should not return error, but return %v", tc.bump, err)
}
if tc.want != got {
t.Errorf("interpretNextRelease(%q) = %v, want %v", tc.bump, tc.want, got)
}
})
}
}
func TestPossibleGoplsVersions(t *testing.T) {
tests := []struct {
name string
@ -367,7 +423,7 @@ func TestNextPrerelease(t *testing.T) {
if !ok {
t.Fatalf("parseSemver(%q) should success", tc.version)
}
got, err := tasks.nextPrerelease(&workflow.TaskContext{Context: ctx, Logger: &testLogger{t, ""}}, semv)
got, err := tasks.nextPrereleaseVersion(&workflow.TaskContext{Context: ctx, Logger: &testLogger{t, ""}}, semv)
if err != nil {
t.Fatalf("nextPrerelease(%q) should not return error: %v", tc.version, err)
}
@ -501,20 +557,22 @@ func TestGoplsPrereleaseFlow(t *testing.T) {
// For each entry, a new commit is created, and if the entry is
// non empty that commit is tagged with the entry value.
commitTags []string
config string
semv semversion
// If set, create the release branch before starting the workflow.
createBranch bool
config string
semv semversion
// fields below are the desired states.
wantVersion string
wantConfig string
wantCommits int
}{
{
name: "update all three file through two commits",
// create release branch with one commit without any tag.
commitTags: []string{""},
config: " ",
semv: semversion{Major: 0, Minor: 1, Patch: 0},
wantVersion: "v0.1.0-pre.1",
name: "update all three file through two commits",
commitTags: []string{"gopls/v0.0.0"},
createBranch: true,
config: " ",
semv: semversion{Major: 0, Minor: 1, Patch: 0},
wantVersion: "v0.1.0-pre.1",
wantConfig: `issuerepo: golang/go
branch: gopls-release-branch.0.1
parent-branch: master
@ -522,9 +580,9 @@ parent-branch: master
wantCommits: 2,
},
{
name: "codereview.cfg already have expected content, update go.mod and go.sum with one commit",
// create release branch with one commit without any tag.
commitTags: []string{""},
name: "codereview.cfg already have expected content, update go.mod and go.sum with one commit",
commitTags: []string{"gopls/v0.0.0"},
createBranch: true,
config: `issuerepo: golang/go
branch: gopls-release-branch.0.1
parent-branch: master
@ -538,11 +596,12 @@ parent-branch: master
wantCommits: 1,
},
{
name: "create the branch for minor version",
commitTags: nil, // no release branch
config: ` `,
semv: semversion{Major: 0, Minor: 12, Patch: 0},
wantVersion: "v0.12.0-pre.1",
name: "create the branch for minor version",
commitTags: []string{"gopls/v0.11.0"},
createBranch: false,
config: ` `,
semv: semversion{Major: 0, Minor: 12, Patch: 0},
wantVersion: "v0.12.0-pre.1",
wantConfig: `issuerepo: golang/go
branch: gopls-release-branch.0.12
parent-branch: master
@ -550,12 +609,12 @@ parent-branch: master
wantCommits: 2,
},
{
name: "workflow should increment the pre-release number to 4",
// create release branch with three commits with tags.
commitTags: []string{"gopls/v0.8.3-pre.1", "gopls/v0.8.3-pre.2", "gopls/v0.8.3-pre.3"},
config: " ",
semv: semversion{Major: 0, Minor: 8, Patch: 3},
wantVersion: "v0.8.3-pre.4",
name: "workflow should increment the pre-release number to 4",
commitTags: []string{"gopls/v0.8.2", "gopls/v0.8.3-pre.1", "gopls/v0.8.3-pre.2", "gopls/v0.8.3-pre.3"},
createBranch: true,
config: " ",
semv: semversion{Major: 0, Minor: 8, Patch: 3},
wantVersion: "v0.8.3-pre.4",
wantConfig: `issuerepo: golang/go
branch: gopls-release-branch.0.8
parent-branch: master
@ -565,7 +624,7 @@ parent-branch: master
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
runTestWithInput := func(input map[string]any) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@ -575,21 +634,14 @@ parent-branch: master
"gopls/go.sum": "\n",
"codereview.cfg": tc.config,
})
// These tags make sure the input version is the next version of some
// released version, in order to pass the version validation step.
tools.Tag(fmt.Sprintf("gopls/v%v.%v.%v", tc.semv.Major, tc.semv.Minor, tc.semv.Patch-1), beforeHead)
tools.Tag(fmt.Sprintf("gopls/v%v.%v.%v", tc.semv.Major, tc.semv.Minor-1, tc.semv.Patch), beforeHead)
tools.Tag(fmt.Sprintf("gopls/v%v.%v.%v", tc.semv.Major-1, tc.semv.Minor, tc.semv.Patch), beforeHead)
// Create the release branch and make a few commits to the release branch.
// Create the release branch and make a few commits to the master branch.
// Var beforeHead is used to track the commit of release branch's head
// before trigger the gopls pre-release run. If we do not need to create a
// release branch, beforeHead will point to the initial commit in the
// master branch.
if len(tc.commitTags) != 0 {
tools.Branch(goplsReleaseBranchName(tc.semv), beforeHead)
for i, tag := range tc.commitTags {
commit := tools.CommitOnBranch(goplsReleaseBranchName(tc.semv), map[string]string{
commit := tools.CommitOnBranch("master", map[string]string{
"README.md": fmt.Sprintf("THIS IS READ ME FOR %v.", i),
})
beforeHead = commit
@ -599,6 +651,10 @@ parent-branch: master
}
}
if tc.createBranch {
tools.Branch(goplsReleaseBranchName(tc.semv), beforeHead)
}
gerrit := NewFakeGerrit(t, tools)
// fakeGo handles multiple arguments in gopls pre-release flow.
@ -664,10 +720,7 @@ esac`, tc.wantVersion)
}
wd := tasks.NewPrereleaseDefinition()
w, err := workflow.Start(wd, map[string]interface{}{
reviewersParam.Name: []string(nil),
"version": fmt.Sprintf("v%v.%v.%v", tc.semv.Major, tc.semv.Minor, tc.semv.Patch),
})
w, err := workflow.Start(wd, input)
if err != nil {
t.Fatal(err)
}
@ -741,6 +794,24 @@ esac`, tc.wantVersion)
if info.Revision != afterHead {
t.Errorf("the pre-release tag points to commit %s, should point to the head commit of release branch %s", info.Revision, afterHead)
}
}
t.Run("manual input version: "+tc.name, func(t *testing.T) {
runTestWithInput(map[string]any{
reviewersParam.Name: []string(nil),
"explicit version (optional)": fmt.Sprintf("v%v.%v.%v", tc.semv.Major, tc.semv.Minor, tc.semv.Patch),
"next version": "use explicit version",
})
})
versionBump := "next patch"
if tc.semv.Patch == 0 {
versionBump = "next minor"
}
t.Run("interpret version "+versionBump+" : "+tc.name, func(t *testing.T) {
runTestWithInput(map[string]any{
reviewersParam.Name: []string(nil),
"explicit version (optional)": "",
"next version": versionBump,
})
})
}
}

Просмотреть файл

@ -56,6 +56,7 @@ var nextVersionParam = wf.ParamDef[string]{
HTMLSelectOptions: []string{
"next minor",
"next patch",
"use explicit version",
},
},
}
@ -128,7 +129,7 @@ func (r *ReleaseVSCodeGoTasks) nextPrereleaseVersion(ctx *wf.TaskContext, versio
return semversion{}, err
}
semv := lastReleasedVersion(tags, true)
semv := latestVersion(tags, isReleaseVersion, vsCodeGoStableVersion)
switch versionBumpStrategy {
case "next minor":
semv.Minor += 2
@ -168,7 +169,30 @@ func (r *ReleaseVSCodeGoTasks) nextPrereleaseVersion(ctx *wf.TaskContext, versio
return semv, err
}
func lastReleasedVersion(versions []string, onlyStable bool) semversion {
func vsCodeGoStableVersion(semv semversion) bool {
return semv.Minor%2 == 0
}
func vsCodeGoInsiderVersion(semv semversion) bool {
return semv.Minor%2 == 1
}
// isReleaseVersion reports whether semv is a release version.
// (in other words, not a prerelease).
func isReleaseVersion(semv semversion) bool {
return semv.Pre == ""
}
// isPrereleaseVersion reports whether semv is a pre-release version.
// (in other words, not a release).
func isPrereleaseVersion(semv semversion) bool {
return semv.Pre != ""
}
// latestVersion returns the latest version in the provided version list,
// considering only versions that match all the specified filters.
// Strings not following semantic versioning are ignored.
func latestVersion(versions []string, filters ...func(semversion) bool) semversion {
latest := semversion{}
for _, v := range versions {
semv, ok := parseSemver(v)
@ -176,28 +200,24 @@ func lastReleasedVersion(versions []string, onlyStable bool) semversion {
continue
}
if semv.Pre != "" {
match := true
for _, filter := range filters {
if !filter(semv) {
match = false
break
}
}
if !match {
continue
}
if semv.Minor%2 == 0 && onlyStable {
if semv.Minor > latest.Minor {
latest = semv
}
if semv.Minor == latest.Minor && semv.Patch > latest.Patch {
latest = semv
}
if semv.Minor > latest.Minor {
latest = semv
}
if semv.Minor%2 == 1 && !onlyStable {
if semv.Minor > latest.Minor {
latest = semv
}
if semv.Minor == latest.Minor && semv.Patch > latest.Patch {
latest = semv
}
if semv.Minor == latest.Minor && semv.Patch > latest.Patch {
latest = semv
}
}