From b655b7346a58138fda8056dee2432b252fb0b51c Mon Sep 17 00:00:00 2001 From: Hongxiang Jiang Date: Mon, 9 Sep 2024 16:12:04 +0000 Subject: [PATCH] internal/task: rename semver to releaseVersion and add string method 1. Remove prerelease field from semver and rename it to releaseVersion. 2. parseSemver rename to parseVersion returns a releaseVersion and the corresponding prerelease string. 3. Refactor vscode go release process follow the same pattern as gopls. Move some code change from CL 612115. Change-Id: Ic02f26c02db519e475118e80e09748c0c63d1521 Reviewed-on: https://go-review.googlesource.com/c/build/+/611975 Reviewed-by: Hyang-Ah Hana Kim LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- internal/task/releasegopls.go | 233 +++++++++++--------------- internal/task/releasegopls_test.go | 150 ++++++++--------- internal/task/releasevscodego.go | 140 ++++++++-------- internal/task/releasevscodego_test.go | 162 ++++++++++-------- 4 files changed, 327 insertions(+), 358 deletions(-) diff --git a/internal/task/releasegopls.go b/internal/task/releasegopls.go index ac402cfbf3..c46ea1f0ea 100644 --- a/internal/task/releasegopls.go +++ b/internal/task/releasegopls.go @@ -73,31 +73,34 @@ func (r *ReleaseGoplsTasks) NewPrereleaseDefinition() *wf.Definition { // // Returns the specified input version if provided; otherwise, interpret a new // version based on the version bumping strategy. -func (r *ReleaseGoplsTasks) determineReleaseVersion(ctx *wf.TaskContext, inputVersion, versionBumpStrategy string) (semversion, error) { +func (r *ReleaseGoplsTasks) determineReleaseVersion(ctx *wf.TaskContext, inputVersion, versionBumpStrategy string) (releaseVersion, 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") + return releaseVersion{}, 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 + return releaseVersion{}, err } - semv, ok := parseSemver(inputVersion) + release, prerelease, ok := parseVersion(inputVersion) if !ok { - return semversion{}, fmt.Errorf("input version %q can not be parsed as semantic version", inputVersion) + return releaseVersion{}, fmt.Errorf("input version %q can not be parsed as semantic version", inputVersion) } - return semv, nil + if prerelease != "" { + return releaseVersion{}, fmt.Errorf("input version %q can not be a prerelease version", inputVersion) + } + return release, nil case "next minor", "next patch": return r.interpretNextRelease(ctx, versionBumpStrategy) default: - return semversion{}, fmt.Errorf("unknown version selection strategy: %q", versionBumpStrategy) + return releaseVersion{}, fmt.Errorf("unknown version selection strategy: %q", versionBumpStrategy) } } -func (r *ReleaseGoplsTasks) interpretNextRelease(ctx *wf.TaskContext, versionBumpStrategy string) (semversion, error) { +func (r *ReleaseGoplsTasks) interpretNextRelease(ctx *wf.TaskContext, versionBumpStrategy string) (releaseVersion, error) { tags, err := r.Gerrit.ListTags(ctx, "tools") if err != nil { - return semversion{}, err + return releaseVersion{}, err } var versions []string @@ -107,30 +110,30 @@ func (r *ReleaseGoplsTasks) interpretNextRelease(ctx *wf.TaskContext, versionBum } } - version := latestVersion(versions, isReleaseVersion) + release, _ := latestVersion(versions, isReleaseVersion) switch versionBumpStrategy { case "next minor": - version.Minor += 1 - version.Patch = 0 + release.Minor += 1 + release.Patch = 0 case "next patch": - version.Patch += 1 + release.Patch += 1 default: - return semversion{}, fmt.Errorf("unknown version selection strategy: %q", versionBumpStrategy) + return releaseVersion{}, fmt.Errorf("unknown version selection strategy: %q", versionBumpStrategy) } - return version, nil + return release, nil } // approvePrerelease prompts the approval for creating a pre-release version. -func (r *ReleaseGoplsTasks) approvePrerelease(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) +func (r *ReleaseGoplsTasks) approvePrerelease(ctx *wf.TaskContext, release releaseVersion, pre string) error { + ctx.Printf("The next release candidate will be %s-%s", release, pre) return r.ApproveAction(ctx) } // approveRelease prompts the approval for releasing a pre-release version. -func (r *ReleaseGoplsTasks) approveRelease(ctx *wf.TaskContext, semv semversion, pre string) error { - ctx.Printf("The release candidate v%v.%v.%v-%s will be released", semv.Major, semv.Minor, semv.Patch, pre) +func (r *ReleaseGoplsTasks) approveRelease(ctx *wf.TaskContext, release releaseVersion, pre string) error { + ctx.Printf("The release candidate %s-%s will be released as %s", release, pre, release) return r.ApproveAction(ctx) } @@ -141,8 +144,8 @@ func (r *ReleaseGoplsTasks) approveRelease(ctx *wf.TaskContext, semv semversion, // If the release issue exists, return the issue ID. // If 'create' is true and no issue exists, a new one is created. // If 'create' is false and no issue exists, an error is returned. -func (r *ReleaseGoplsTasks) findOrCreateGitHubIssue(ctx *wf.TaskContext, semv semversion, create bool) (int64, error) { - versionString := fmt.Sprintf("v%v.%v.%v", semv.Major, semv.Minor, semv.Patch) +func (r *ReleaseGoplsTasks) findOrCreateGitHubIssue(ctx *wf.TaskContext, release releaseVersion, create bool) (int64, error) { + versionString := release.String() milestoneName := fmt.Sprintf("gopls/%s", versionString) // All milestones and issues resides under go repo. milestoneID, err := r.Github.FetchMilestone(ctx, "golang", "go", milestoneName, false) @@ -182,7 +185,7 @@ func (r *ReleaseGoplsTasks) findOrCreateGitHubIssue(ctx *wf.TaskContext, semv se - [ ] smoke test features - [ ] tag gopls/%s - [ ] (if vX.Y.0 release): update dependencies in master for the next release -`, versionString, goplsReleaseBranchName(semv), versionString, versionString) +`, versionString, goplsReleaseBranchName(release), versionString, versionString) // TODO(hxjiang): accept a new parameter release coordinator. assignee := "h9jiang" issue, _, err := r.Github.CreateIssue(ctx, "golang", "go", &github.IssueRequest{ @@ -200,18 +203,18 @@ func (r *ReleaseGoplsTasks) findOrCreateGitHubIssue(ctx *wf.TaskContext, semv se } // goplsReleaseBranchName returns the branch name for given input release version. -func goplsReleaseBranchName(semv semversion) string { - return fmt.Sprintf("gopls-release-branch.%v.%v", semv.Major, semv.Minor) +func goplsReleaseBranchName(release releaseVersion) string { + return fmt.Sprintf("gopls-release-branch.%v.%v", release.Major, release.Minor) } // createBranchIfMinor create the release branch if the input version is a minor // release. // All patch releases under the same minor version share the same release branch. -func (r *ReleaseGoplsTasks) createBranchIfMinor(ctx *wf.TaskContext, semv semversion) error { - branch := goplsReleaseBranchName(semv) +func (r *ReleaseGoplsTasks) createBranchIfMinor(ctx *wf.TaskContext, release releaseVersion) error { + branch := goplsReleaseBranchName(release) // Require gopls release branch existence if this is a non-minor release. - if semv.Patch != 0 { + if release.Patch != 0 { _, err := r.Gerrit.ReadBranchHead(ctx, "tools", branch) return err } @@ -257,10 +260,10 @@ func openCL(ctx *wf.TaskContext, gerrit GerritClient, repo, branch, title string // // It returns the change ID required to update the config if changes are needed, // otherwise it returns an empty string indicating no update is necessary. -func (r *ReleaseGoplsTasks) updateCodeReviewConfig(ctx *wf.TaskContext, semv semversion, reviewers []string, issue int64) (string, error) { +func (r *ReleaseGoplsTasks) updateCodeReviewConfig(ctx *wf.TaskContext, release releaseVersion, reviewers []string, issue int64) (string, error) { const configFile = "codereview.cfg" - branch := goplsReleaseBranchName(semv) + branch := goplsReleaseBranchName(release) clTitle := fmt.Sprintf("all: update %s for %s", configFile, branch) openCL, err := openCL(ctx, r.Gerrit, "tools", branch, clTitle) @@ -307,7 +310,7 @@ parent-branch: master // 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) { +func (r *ReleaseGoplsTasks) nextPrereleaseVersion(ctx *wf.TaskContext, release releaseVersion) (string, error) { tags, err := r.Gerrit.ListTags(ctx, "tools") if err != nil { return "", err @@ -320,62 +323,28 @@ func (r *ReleaseGoplsTasks) nextPrereleaseVersion(ctx *wf.TaskContext, semv semv } } - rc := latestVersion(versions, isSameMajorMinorPatch(semv), isPrereleaseMatchRegex(`^pre\.\d+$`)) - if rc == (semversion{}) { + _, prerelease := latestVersion(versions, isSameReleaseVersion(release), isPrereleaseMatchRegex(`^pre\.\d+$`)) + if prerelease == "" { return "pre.1", nil } - pre, err := rc.prereleaseVersion() + pre, err := prereleaseNumber(prerelease) if err != nil { return "", err } return fmt.Sprintf("pre.%v", pre+1), nil } -// currentGoplsPrerelease inspects the tags in tools repo that match with the -// given version and find the latest pre-release version. -func currentGoplsPrerelease(ctx *wf.TaskContext, client GerritClient, semv semversion) (int, error) { - tags, err := client.ListTags(ctx, "tools") - if err != nil { - return 0, fmt.Errorf("failed to list tags for tools repo: %w", err) - } - - max := 0 - for _, tag := range tags { - v, ok := strings.CutPrefix(tag, "gopls/") - if !ok { - continue - } - cur, ok := parseSemver(v) - if !ok { - continue - } - if cur.Major != semv.Major || cur.Minor != semv.Minor || cur.Patch != semv.Patch { - continue - } - pre, err := cur.prereleaseVersion() - if err != nil { - continue - } - - if pre > max { - max = pre - } - } - - return max, nil -} - // updateXToolsDependency ensures gopls sub module have the correct x/tools // version as dependency. // // It returns the change ID, or "" if the CL was not created. -func (r *ReleaseGoplsTasks) updateXToolsDependency(ctx *wf.TaskContext, semv semversion, pre string, reviewers []string, issue int64) (string, error) { +func (r *ReleaseGoplsTasks) updateXToolsDependency(ctx *wf.TaskContext, release releaseVersion, pre string, reviewers []string, issue int64) (string, error) { if pre == "" { return "", fmt.Errorf("the input pre-release version should not be empty") } - branch := goplsReleaseBranchName(semv) - clTitle := fmt.Sprintf("gopls: update go.mod for v%v.%v.%v-%s", semv.Major, semv.Minor, semv.Patch, pre) + branch := goplsReleaseBranchName(release) + clTitle := fmt.Sprintf("gopls: update go.mod for %s-%s", release, pre) openCL, err := openCL(ctx, r.Gerrit, "tools", branch, clTitle) if err != nil { return "", fmt.Errorf("failed to find the open CL of title %q in branch %q: %w", clTitle, branch, err) @@ -456,7 +425,7 @@ $(go env GOPATH)/bin/gopls references -d main.go:4:8 &> smoke.log // The input semversion provides Major, Minor, and Patch info. // The input pre-release, generated by previous steps of the workflow, provides // Pre-release info. -func (r *ReleaseGoplsTasks) tagPrerelease(ctx *wf.TaskContext, semv semversion, commit, pre string) (string, error) { +func (r *ReleaseGoplsTasks) tagPrerelease(ctx *wf.TaskContext, release releaseVersion, commit, pre string) (string, error) { if commit == "" { return "", fmt.Errorf("the input commit should not be empty") } @@ -467,7 +436,7 @@ func (r *ReleaseGoplsTasks) tagPrerelease(ctx *wf.TaskContext, semv semversion, // Defensively guard against re-creating tags. ctx.DisableRetries() - version := fmt.Sprintf("v%v.%v.%v-%s", semv.Major, semv.Minor, semv.Patch, pre) + version := fmt.Sprintf("%s-%s", release, pre) tag := fmt.Sprintf("gopls/%s", version) if err := r.Gerrit.Tag(ctx, "tools", tag, commit); err != nil { return "", err @@ -484,7 +453,7 @@ type goplsPrereleaseAnnouncement struct { Issue int64 } -func (r *ReleaseGoplsTasks) mailPrereleaseAnnouncement(ctx *wf.TaskContext, release semversion, rc, commit string, issue int64) error { +func (r *ReleaseGoplsTasks) mailPrereleaseAnnouncement(ctx *wf.TaskContext, release releaseVersion, rc, commit string, issue int64) error { announce := goplsPrereleaseAnnouncement{ Version: rc, Branch: goplsReleaseBranchName(release), @@ -507,7 +476,7 @@ type goplsReleaseAnnouncement struct { Commit string } -func (r *ReleaseGoplsTasks) mailReleaseAnnouncement(ctx *wf.TaskContext, release semversion) error { +func (r *ReleaseGoplsTasks) mailReleaseAnnouncement(ctx *wf.TaskContext, release releaseVersion) error { version := fmt.Sprintf("v%v.%v.%v", release.Major, release.Minor, release.Patch) info, err := r.Gerrit.GetTag(ctx, "tools", fmt.Sprintf("gopls/%s", version)) @@ -547,27 +516,36 @@ func (r *ReleaseGoplsTasks) isValidReleaseVersion(ctx *wf.TaskContext, ver strin return nil } -// semversion is a parsed semantic version. -type semversion struct { +// releaseVersion is a parsed semantic release version containing only major, +// minor and patch version. +type releaseVersion struct { Major, Minor, Patch int - Pre string } -// parseSemver attempts to parse semver components out of the provided semver -// v. If v is not valid semver in canonical form, parseSemver returns false. -func parseSemver(v string) (_ semversion, ok bool) { - var parsed semversion - v, parsed.Pre, _ = strings.Cut(v, "-") - if _, err := fmt.Sscanf(v, "v%d.%d.%d", &parsed.Major, &parsed.Minor, &parsed.Patch); err == nil { - ok = true +// String returns the version string representation of the release version. +func (s releaseVersion) String() string { + return fmt.Sprintf("v%v.%v.%v", s.Major, s.Minor, s.Patch) +} + +// parseVersion parses the input version string into a release version and a +// prerelease string. +// It returns false if the input is not a valid semantic version in canonical form. +func parseVersion(v string) (_ releaseVersion, prerelease string, ok bool) { + var parsed releaseVersion + if !semver.IsValid(v) { + return releaseVersion{}, "", false + } + v, pre, _ := strings.Cut(v, "-") + if _, err := fmt.Sscanf(v, "v%d.%d.%d", &parsed.Major, &parsed.Minor, &parsed.Patch); err != nil { + return releaseVersion{}, "", false } - return parsed, ok + return parsed, pre, true } -// prereleaseVersion extracts the integer component from a pre-release version +// prereleaseNumber extracts the integer component from a pre-release version // string in the format "${STRING}.${INT}". -func (s *semversion) prereleaseVersion() (int, error) { - parts := strings.Split(s.Pre, ".") +func prereleaseNumber(prerelease string) (int, error) { + parts := strings.Split(prerelease, ".") if len(parts) == 1 { return 0, fmt.Errorf(`pre-release version does not contain any "."`) } @@ -596,54 +574,33 @@ func (r *ReleaseGoplsTasks) possibleGoplsVersions(ctx *wf.TaskContext) ([]string return nil, err } - var semVersions []semversion - majorMinorPatch := map[int]map[int]map[int]bool{} + var releaseVersions []releaseVersion + seen := make(map[releaseVersion]bool) for _, tag := range tags { v, ok := strings.CutPrefix(tag, "gopls/") if !ok { continue } - if !semver.IsValid(v) { - continue - } - - // Skip for pre-release versions. - if semver.Prerelease(v) != "" { + release, prerelease, ok := parseVersion(v) + if !ok || prerelease != "" { continue } - - semv, ok := parseSemver(v) - semVersions = append(semVersions, semv) - - if majorMinorPatch[semv.Major] == nil { - majorMinorPatch[semv.Major] = map[int]map[int]bool{} - } - if majorMinorPatch[semv.Major][semv.Minor] == nil { - majorMinorPatch[semv.Major][semv.Minor] = map[int]bool{} - } - majorMinorPatch[semv.Major][semv.Minor][semv.Patch] = true + releaseVersions = append(releaseVersions, release) + seen[release] = true } var possible []string - seen := map[string]bool{} - for _, v := range semVersions { - nextMajor := fmt.Sprintf("v%d.%d.%d", v.Major+1, 0, 0) - if _, ok := majorMinorPatch[v.Major+1]; !ok && !seen[nextMajor] { - seen[nextMajor] = true - possible = append(possible, nextMajor) - } - - nextMinor := fmt.Sprintf("v%d.%d.%d", v.Major, v.Minor+1, 0) - if _, ok := majorMinorPatch[v.Major][v.Minor+1]; !ok && !seen[nextMinor] { - seen[nextMinor] = true - possible = append(possible, nextMinor) - } - - nextPatch := fmt.Sprintf("v%d.%d.%d", v.Major, v.Minor, v.Patch+1) - if _, ok := majorMinorPatch[v.Major][v.Minor][v.Patch+1]; !ok && !seen[nextPatch] { - seen[nextPatch] = true - possible = append(possible, nextPatch) + for _, v := range releaseVersions { + for _, next := range []releaseVersion{ + {v.Major+1, 0, 0}, // next major + {v.Major, v.Minor+1, 0}, // next minor + {v.Major, v.Minor, v.Patch+1}, // next patch + } { + if _, ok := seen[next]; !ok { + possible = append(possible, next.String()) + seen[next] = true + } } } @@ -683,7 +640,7 @@ func (r *ReleaseGoplsTasks) NewReleaseDefinition() *wf.Definition { return wd } -func (r *ReleaseGoplsTasks) latestPrerelease(ctx *wf.TaskContext, semv semversion) (string, error) { +func (r *ReleaseGoplsTasks) latestPrerelease(ctx *wf.TaskContext, release releaseVersion) (string, error) { tags, err := r.Gerrit.ListTags(ctx, "tools") if err != nil { return "", err @@ -696,12 +653,12 @@ func (r *ReleaseGoplsTasks) latestPrerelease(ctx *wf.TaskContext, semv semversio } } - rc := latestVersion(versions, isSameMajorMinorPatch(semv), isPrereleaseMatchRegex(`^pre\.\d+$`)) - if rc == (semversion{}) { - return "", fmt.Errorf("could not find any release candidate for v%v.%v.%v", semv.Major, semv.Minor, semv.Patch) + _, prerelease := latestVersion(versions, isSameReleaseVersion(release), isPrereleaseMatchRegex(`^pre\.\d+$`)) + if prerelease == "" { + return "", fmt.Errorf("could not find any release candidate for %s", release) } - return rc.Pre, nil + return prerelease, nil } // updateVSCodeGoGoplsVersion updates the gopls version in the vscode-go project. @@ -709,8 +666,8 @@ func (r *ReleaseGoplsTasks) latestPrerelease(ctx *wf.TaskContext, semv semversio // and release branches. // For pre-releases (input param prerelease is not empty), it updates only the // master branch. -func (r *ReleaseGoplsTasks) updateVSCodeGoGoplsVersion(ctx *wf.TaskContext, reviewers []string, issue int64, release semversion, prerelease string) ([]string, error) { - version := fmt.Sprintf("v%v.%v.%v", release.Major, release.Minor, release.Patch) +func (r *ReleaseGoplsTasks) updateVSCodeGoGoplsVersion(ctx *wf.TaskContext, reviewers []string, issue int64, release releaseVersion, prerelease string) ([]string, error) { + version := release.String() if prerelease != "" { version = version + "-" + prerelease } @@ -767,8 +724,8 @@ func (r *ReleaseGoplsTasks) updateVSCodeGoGoplsVersion(ctx *wf.TaskContext, revi // tagRelease locates the commit associated with the pre-release version and // applies the official release tag in form of "gopls/vX.Y.Z" to the same commit. -func (r *ReleaseGoplsTasks) tagRelease(ctx *wf.TaskContext, semv semversion, prerelease string) error { - info, err := r.Gerrit.GetTag(ctx, "tools", fmt.Sprintf("gopls/v%v.%v.%v-%s", semv.Major, semv.Minor, semv.Patch, prerelease)) +func (r *ReleaseGoplsTasks) tagRelease(ctx *wf.TaskContext, release releaseVersion, prerelease string) error { + info, err := r.Gerrit.GetTag(ctx, "tools", fmt.Sprintf("gopls/%s-%s", release, prerelease)) if err != nil { return err } @@ -776,7 +733,7 @@ func (r *ReleaseGoplsTasks) tagRelease(ctx *wf.TaskContext, semv semversion, pre // Defensively guard against re-creating tags. ctx.DisableRetries() - releaseTag := fmt.Sprintf("gopls/v%v.%v.%v", semv.Major, semv.Minor, semv.Patch) + releaseTag := fmt.Sprintf("gopls/%s", release) if err := r.Gerrit.Tag(ctx, "tools", releaseTag, info.Revision); err != nil { return err } @@ -789,12 +746,12 @@ func (r *ReleaseGoplsTasks) tagRelease(ctx *wf.TaskContext, semv semversion, pre // branch. // // Returns the change ID. -func (r *ReleaseGoplsTasks) updateDependencyIfMinor(ctx *wf.TaskContext, reviewers []string, semv semversion, issue int64) (string, error) { - if semv.Patch != 0 { +func (r *ReleaseGoplsTasks) updateDependencyIfMinor(ctx *wf.TaskContext, reviewers []string, release releaseVersion, issue int64) (string, error) { + if release.Patch != 0 { return "", nil } - clTitle := fmt.Sprintf("gopls/go.mod: update dependencies following the v%v.%v.%v release", semv.Major, semv.Minor, semv.Patch) + clTitle := fmt.Sprintf("gopls/go.mod: update dependencies following the %s release", release) openCL, err := openCL(ctx, r.Gerrit, "tools", "master", clTitle) if err != nil { return "", fmt.Errorf("failed to find the open CL of title %q in master branch: %w", clTitle, err) diff --git a/internal/task/releasegopls_test.go b/internal/task/releasegopls_test.go index 2f96bce0c8..86a1586b7a 100644 --- a/internal/task/releasegopls_test.go +++ b/internal/task/releasegopls_test.go @@ -21,25 +21,25 @@ func TestInterpretNextRelease(t *testing.T) { name string tags []string bump string - want semversion + want releaseVersion }{ { 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}, + want: releaseVersion{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}, + want: releaseVersion{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}, + want: releaseVersion{Major: 0, Minor: 2, Patch: 3}, }, } @@ -84,24 +84,24 @@ func TestPossibleGoplsVersions(t *testing.T) { want: []string{"v1.2.4", "v1.3.0", "v2.0.0"}, }, { - name: "1.2.0 should be skipped because 1.2.3 already exist", + name: "v1.2.3 and v1.1.0 share the same next major version", tags: []string{"gopls/v1.2.3", "gopls/v1.1.0"}, - want: []string{"v1.1.1", "v1.2.4", "v1.3.0", "v2.0.0"}, + want: []string{"v1.1.1", "v1.2.0", "v1.2.4", "v1.3.0", "v2.0.0"}, }, { - name: "2.0.0 should be skipped because 2.1.3 already exist", - tags: []string{"gopls/v1.2.3", "gopls/v2.1.3"}, - want: []string{"v1.2.4", "v1.3.0", "v2.1.4", "v2.2.0", "v3.0.0"}, + name: "two versions without any duplicate next version should have 6 possible versions", + tags: []string{"gopls/v1.1.3", "gopls/v2.1.2"}, + want: []string{"v1.1.4", "v1.2.0", "v2.0.0", "v2.1.3", "v2.2.0", "v3.0.0"}, }, { - name: "1.2.0 is still consider valid version because there is no 1.2.X", - tags: []string{"gopls/v1.1.3", "gopls/v1.3.2", "gopls/v2.1.2"}, - want: []string{"v1.1.4", "v1.2.0", "v1.3.3", "v1.4.0", "v2.1.3", "v2.2.0", "v3.0.0"}, + name: "v1.1.0 and v1.1.0 share the same next major and next minor", + tags: []string{"gopls/v1.1.2", "gopls/v1.1.0"}, + want: []string{"v1.1.1", "v1.1.3", "v1.2.0", "v2.0.0"}, }, { - name: "2.0.0 is still consider valid version because there is no 2.X.X", - tags: []string{"gopls/v1.2.3", "gopls/v3.1.2"}, - want: []string{"v1.2.4", "v1.3.0", "v2.0.0", "v3.1.3", "v3.2.0", "v4.0.0"}, + name: "v1.1.0 next patch v1.1.1 already exist", + tags: []string{"gopls/v1.1.1", "gopls/v1.1.0"}, + want: []string{"v1.1.2", "v1.2.0", "v2.0.0"}, }, { name: "pre-release version tag should not have any effect on the next version", @@ -208,8 +208,8 @@ func TestCreateBranchIfMinor(t *testing.T) { Gerrit: gerritClient, } - semv, _ := parseSemver(tc.version) - err = tasks.createBranchIfMinor(&workflow.TaskContext{Context: ctx, Logger: &testLogger{t, ""}}, semv) + release, _, _ := parseVersion(tc.version) + err = tasks.createBranchIfMinor(&workflow.TaskContext{Context: ctx, Logger: &testLogger{t, ""}}, release) if tc.wantErr && err == nil { t.Errorf("createBranchIfMinor() should return error but return nil") @@ -310,8 +310,8 @@ parent-branch: master t.Fatalf("ReadFile should be able to read the codereview.cfg file from master branch head: %v", err) } - semv, _ := parseSemver(tc.version) - releaseBranch := goplsReleaseBranchName(semv) + release, _, _ := parseVersion(tc.version) + releaseBranch := goplsReleaseBranchName(release) if _, err := gerritClient.CreateBranch(ctx, "tools", releaseBranch, gerrit.BranchInput{Revision: headMaster}); err != nil { t.Fatalf("failed to create the branch %q: %v", releaseBranch, err) } @@ -326,7 +326,7 @@ parent-branch: master CloudBuild: NewFakeCloudBuild(t, gerritClient, "", nil, fakeGo), } - _, err = tasks.updateCodeReviewConfig(&workflow.TaskContext{Context: ctx, Logger: &testLogger{t, ""}}, semv, nil, 0) + _, err = tasks.updateCodeReviewConfig(&workflow.TaskContext{Context: ctx, Logger: &testLogger{t, ""}}, release, nil, 0) if err != nil { t.Fatalf("updateCodeReviewConfig() returns error: %v", err) } @@ -418,17 +418,13 @@ func TestNextPrerelease(t *testing.T) { Gerrit: gerrit, } - semv, ok := parseSemver(tc.version) + release, _, ok := parseVersion(tc.version) if !ok { - t.Fatalf("parseSemver(%q) should success", tc.version) + t.Fatalf("parseVersion(%q) failed", tc.version) } - 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) - } - - if tc.want != got { - t.Errorf("nextPrerelease(%q) = %v want %v", tc.version, got, tc.want) + got, err := tasks.nextPrereleaseVersion(&workflow.TaskContext{Context: ctx, Logger: &testLogger{t, ""}}, release) + if err != nil || tc.want != got { + t.Errorf("nextPrereleaseVersion(%q) = (%v, %v) but want (%v, nil)", tc.version, got, err, tc.want) } }) } @@ -511,11 +507,11 @@ func TestFindOrCreateReleaseIssue(t *testing.T) { Github: &tc.fakeGithub, } - semv, ok := parseSemver(tc.version) + release, _, ok := parseVersion(tc.version) if !ok { - t.Fatalf("parseSemver(%q) should success", tc.version) + t.Fatalf("parseVersion(%q) failed", tc.version) } - gotIssue, err := tasks.findOrCreateGitHubIssue(&workflow.TaskContext{Context: ctx, Logger: &testLogger{t, ""}}, semv, tc.create) + gotIssue, err := tasks.findOrCreateGitHubIssue(&workflow.TaskContext{Context: ctx, Logger: &testLogger{t, ""}}, release, tc.create) if tc.wantErr && err == nil { t.Errorf("createReleaseIssue(%s) should return error but return nil", tc.version) @@ -547,7 +543,7 @@ func TestGoplsPrereleaseFlow(t *testing.T) { // If set, create the release branch before starting the workflow. createBranch bool config string - semv semversion + release releaseVersion // fields below are the desired states. wantVersion string wantConfig string @@ -558,7 +554,7 @@ func TestGoplsPrereleaseFlow(t *testing.T) { commitTags: []string{"gopls/v0.0.0"}, createBranch: true, config: " ", - semv: semversion{Major: 0, Minor: 1, Patch: 0}, + release: releaseVersion{Major: 0, Minor: 1, Patch: 0}, wantVersion: "v0.1.0-pre.1", wantConfig: `issuerepo: golang/go branch: gopls-release-branch.0.1 @@ -574,7 +570,7 @@ parent-branch: master branch: gopls-release-branch.0.1 parent-branch: master `, - semv: semversion{Major: 0, Minor: 1, Patch: 0}, + release: releaseVersion{Major: 0, Minor: 1, Patch: 0}, wantVersion: "v0.1.0-pre.1", wantConfig: `issuerepo: golang/go branch: gopls-release-branch.0.1 @@ -587,7 +583,7 @@ parent-branch: master commitTags: []string{"gopls/v0.11.0"}, createBranch: false, config: ` `, - semv: semversion{Major: 0, Minor: 12, Patch: 0}, + release: releaseVersion{Major: 0, Minor: 12, Patch: 0}, wantVersion: "v0.12.0-pre.1", wantConfig: `issuerepo: golang/go branch: gopls-release-branch.0.12 @@ -600,7 +596,7 @@ parent-branch: master 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}, + release: releaseVersion{Major: 0, Minor: 8, Patch: 3}, wantVersion: "v0.8.3-pre.4", wantConfig: `issuerepo: golang/go branch: gopls-release-branch.0.8 @@ -643,7 +639,7 @@ parent-branch: master } if tc.createBranch { - tools.Branch(goplsReleaseBranchName(tc.semv), beforeHead) + tools.Branch(goplsReleaseBranchName(tc.release), beforeHead) } gerrit := NewFakeGerrit(t, tools, vscodego) @@ -712,7 +708,7 @@ esac`, tc.wantVersion) CloudBuild: NewFakeCloudBuild(t, gerrit, "", nil, fakeGo), Github: &FakeGitHub{ Milestones: map[int]string{ - 1: fmt.Sprintf("gopls/v%v.%v.%v", tc.semv.Major, tc.semv.Minor, tc.semv.Patch), + 1: fmt.Sprintf("gopls/%s", tc.release.String()), }, }, SendMail: func(h MailHeader, c MailContent) error { @@ -735,7 +731,7 @@ esac`, tc.wantVersion) // Verify that workflow will create the release branch for minor releases. // The release branch is created before the flow run for patch releases. - afterHead, err := gerrit.ReadBranchHead(ctx, "tools", goplsReleaseBranchName(tc.semv)) + afterHead, err := gerrit.ReadBranchHead(ctx, "tools", goplsReleaseBranchName(tc.release)) if err != nil { t.Error(err) } @@ -754,19 +750,19 @@ esac`, tc.wantVersion) }{ { repo: "tools", - branch: goplsReleaseBranchName(tc.semv), + branch: goplsReleaseBranchName(tc.release), path: "codereview.cfg", want: tc.wantConfig, }, { repo: "tools", - branch: goplsReleaseBranchName(tc.semv), + branch: goplsReleaseBranchName(tc.release), path: "gopls/go.sum", want: "test go sum", }, { repo: "tools", - branch: goplsReleaseBranchName(tc.semv), + branch: goplsReleaseBranchName(tc.release), path: "gopls/go.mod", want: "test go mod", }, @@ -830,12 +826,12 @@ esac`, tc.wantVersion) 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), + "explicit version (optional)": tc.release.String(), "next version": "use explicit version", }) }) versionBump := "next patch" - if tc.semv.Patch == 0 { + if tc.release.Patch == 0 { versionBump = "next minor" } t.Run("interpret version "+versionBump+" : "+tc.name, func(t *testing.T) { @@ -851,10 +847,11 @@ esac`, tc.wantVersion) func TestTagRelease(t *testing.T) { ctx := context.Background() testcases := []struct { - name string - tags []string - version string - wantErr bool + name string + tags []string + release releaseVersion + prerelease string + wantErr bool }{ { name: "should add the release tag v0.1.0 to the commit with tag v0.1.0-pre.2", @@ -862,8 +859,9 @@ func TestTagRelease(t *testing.T) { "gopls/v0.1.0-pre.1", "gopls/v0.1.0-pre.2", }, - version: "v0.1.0-pre.2", - wantErr: false, + release: releaseVersion{Major: 0, Minor: 1, Patch: 0}, + prerelease: "pre.2", + wantErr: false, }, { name: "should add the release tag v0.12.0 to the commit with tag v0.12.0-pre.1", @@ -871,8 +869,9 @@ func TestTagRelease(t *testing.T) { "gopls/v0.12.0-pre.1", "gopls/v0.12.0-pre.2", }, - version: "v0.12.0-pre.1", - wantErr: false, + release: releaseVersion{Major: 0, Minor: 12, Patch: 0}, + prerelease: "pre.1", + wantErr: false, }, { name: "should error if the pre-release tag does not exist", @@ -880,8 +879,9 @@ func TestTagRelease(t *testing.T) { "gopls/v0.12.0-pre.1", "gopls/v0.12.0-pre.2", }, - version: "v0.12.0-pre.3", - wantErr: true, + release: releaseVersion{Major: 0, Minor: 12, Patch: 0}, + prerelease: "pre.3", + wantErr: true, }, } @@ -904,24 +904,22 @@ func TestTagRelease(t *testing.T) { Gerrit: NewFakeGerrit(t, tools), } - semv, _ := parseSemver(tc.version) - - err := tasks.tagRelease(&workflow.TaskContext{Context: context.Background(), Logger: &testLogger{t, ""}}, semv, semv.Pre) + err := tasks.tagRelease(&workflow.TaskContext{Context: context.Background(), Logger: &testLogger{t, ""}}, tc.release, tc.prerelease) if tc.wantErr && err == nil { - t.Errorf("tagRelease(%q) should return error but return nil", tc.version) + t.Errorf("tagRelease(%q) should return error but return nil", tc.release) } else if !tc.wantErr && err != nil { - t.Errorf("tagRelease(%q) should return nil but return err: %v", tc.version, err) + t.Errorf("tagRelease(%q) should return nil but return err: %v", tc.release, err) } if !tc.wantErr { - releaseTag := fmt.Sprintf("gopls/v%v.%v.%v", semv.Major, semv.Minor, semv.Patch) + releaseTag := fmt.Sprintf("gopls/%s", tc.release) release, err := tasks.Gerrit.GetTag(ctx, "tools", releaseTag) if err != nil { - t.Errorf("release tag %q should be added after tagRelease(%q): %v", releaseTag, tc.version, err) + t.Errorf("release tag %q should be added after tagRelease(%q): %v", releaseTag, tc.release, err) } - prereleaseTag := fmt.Sprintf("gopls/%s", tc.version) + prereleaseTag := fmt.Sprintf("gopls/%s", tc.release) prerelease, err := tasks.Gerrit.GetTag(ctx, "tools", prereleaseTag) if err != nil { t.Fatalf("failed to get tag %q: %v", prereleaseTag, err) @@ -930,7 +928,7 @@ func TestTagRelease(t *testing.T) { // verify the release tag and the input pre-release tag point to the same // commit. if release.Revision != prerelease.Revision { - t.Errorf("tagRelease(%s) add the release tag to commit %s, but should add to commit %s", tc.version, prerelease.Revision, release.Revision) + t.Errorf("tagRelease(%s) add the release tag to commit %s, but should add to commit %s", tc.release, prerelease.Revision, release.Revision) } } }) @@ -1040,7 +1038,7 @@ func TestGoplsReleaseFlow(t *testing.T) { // goplsGoMod specifies the content of gopls/go.mod in x/tools' master // branch before the flow execution. goplsGoMod string - semv semversion + release releaseVersion // The fields below are the desired states. // wantPrereleaseTag is the expected prerelease tag in x/tools repo @@ -1057,7 +1055,7 @@ func TestGoplsReleaseFlow(t *testing.T) { name: "release patch v0.16.3-pre.3, update vscode-go release and master branch", commitTags: []string{"gopls/v0.16.2", "gopls/v0.16.3-pre.1", "gopls/v0.16.3-pre.2", "gopls/v0.16.3-pre.3"}, goplsGoMod: "foo", - semv: semversion{Major: 0, Minor: 16, Patch: 3}, + release: releaseVersion{Major: 0, Minor: 16, Patch: 3}, wantPrereleaseTag: "gopls/v0.16.3-pre.3", wantCommit: map[string]map[string]bool{ "tools": { @@ -1073,7 +1071,7 @@ func TestGoplsReleaseFlow(t *testing.T) { name: "release minor v0.17.0-pre.2, update vscode-go release and master branch, update tools gopls go.mod", commitTags: []string{"gopls/v0.16.0", "gopls/v0.17.0-pre.1", "gopls/v0.17.0-pre.2"}, goplsGoMod: "foo", - semv: semversion{Major: 0, Minor: 17, Patch: 0}, + release: releaseVersion{Major: 0, Minor: 17, Patch: 0}, wantPrereleaseTag: "gopls/v0.17.0-pre.2", wantCommit: map[string]map[string]bool{ "tools": { @@ -1089,7 +1087,7 @@ func TestGoplsReleaseFlow(t *testing.T) { name: "release minor v0.17.0-pre.2, update vscode-go release and master branch, skip update tools gopls go.mod", commitTags: []string{"gopls/v0.16.0", "gopls/v0.17.0-pre.1", "gopls/v0.17.0-pre.2"}, goplsGoMod: "bar", - semv: semversion{Major: 0, Minor: 17, Patch: 0}, + release: releaseVersion{Major: 0, Minor: 17, Patch: 0}, wantPrereleaseTag: "gopls/v0.17.0-pre.2", wantCommit: map[string]map[string]bool{ "tools": { @@ -1107,7 +1105,7 @@ func TestGoplsReleaseFlow(t *testing.T) { runTestWithInput := func(input map[string]any) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - releaseBranch := goplsReleaseBranchName(tc.semv) + releaseBranch := goplsReleaseBranchName(tc.release) vscodego := NewFakeRepo(t, "vscode-go") initial := vscodego.Commit(map[string]string{ @@ -1187,12 +1185,12 @@ esac CloudBuild: NewFakeCloudBuild(t, gerrit, "", nil, fakeGo), Github: &FakeGitHub{ Milestones: map[int]string{ - 1: fmt.Sprintf("gopls/v%v.%v.%v", tc.semv.Major, tc.semv.Minor, tc.semv.Patch), + 1: fmt.Sprintf("gopls/%s", tc.release), }, Issues: map[int]*github.Issue{ 1: { Number: github.Int(1), - Title: github.String(fmt.Sprintf("x/tools/gopls: release version v%v.%v.%v", tc.semv.Major, tc.semv.Minor, tc.semv.Patch)), + Title: github.String(fmt.Sprintf("x/tools/gopls: release version %s", tc.release)), Milestone: &github.Milestone{ID: github.Int64(1)}, }, }, @@ -1264,9 +1262,9 @@ esac if err != nil { t.Fatalf("can not get the commit for tag %s", tc.wantPrereleaseTag) } - gotCommit, err := gerrit.GetTag(ctx, "tools", fmt.Sprintf("gopls/v%v.%v.%v", tc.semv.Major, tc.semv.Minor, tc.semv.Patch)) + gotCommit, err := gerrit.GetTag(ctx, "tools", fmt.Sprintf("gopls/%s", tc.release)) if err != nil { - t.Errorf("can not get the commit for tag %s", fmt.Sprintf("gopls/v%v.%v.%v", tc.semv.Major, tc.semv.Minor, tc.semv.Patch)) + t.Errorf("can not get the commit for tag %s", fmt.Sprintf("gopls/%s", tc.release)) } if wantCommit.Revision != gotCommit.Revision { t.Errorf("the flow create release tag upon commit %s, but should tag on commit %s which have tag %s", gotCommit.Revision, wantCommit.Revision, tc.wantPrereleaseTag) @@ -1312,7 +1310,7 @@ esac } } - if wantSubject := fmt.Sprintf("Gopls v%v.%v.%v is released", tc.semv.Major, tc.semv.Minor, tc.semv.Patch); gotSubject != wantSubject { + if wantSubject := fmt.Sprintf("Gopls %s is released", tc.release); gotSubject != wantSubject { // The full email content is checked by TestAnnouncementMail. t.Errorf("NewReleaseDefinition().Run(): got email subject %q, want %q", gotSubject, wantSubject) } @@ -1320,12 +1318,12 @@ esac 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), + "explicit version (optional)": tc.release.String(), "next version": "use explicit version", }) }) versionBump := "next patch" - if tc.semv.Patch == 0 { + if tc.release.Patch == 0 { versionBump = "next minor" } t.Run("interpret version "+versionBump+": "+tc.name, func(t *testing.T) { diff --git a/internal/task/releasevscodego.go b/internal/task/releasevscodego.go index c24fe7f970..1ea60ac275 100644 --- a/internal/task/releasevscodego.go +++ b/internal/task/releasevscodego.go @@ -130,16 +130,18 @@ func (r *ReleaseVSCodeGoTasks) NewPrereleaseDefinition() *wf.Definition { versionBumpStrategy := wf.Param(wd, nextVersionParam) - semv := wf.Task1(wd, "find the next pre-release version", r.nextPrereleaseVersion, versionBumpStrategy) - approved := wf.Action1(wd, "await release coordinator's approval", r.approveVersion, semv) + release := wf.Task1(wd, "determine the release version", r.determineReleaseVersion, versionBumpStrategy) + prerelease := wf.Task1(wd, "find the next pre-release version", r.nextPrereleaseVersion, release) + approved := wf.Action2(wd, "await release coordinator's approval", r.approveVersion, release, prerelease) - _ = wf.Task1(wd, "create release milestone and issue", r.createReleaseMilestoneAndIssue, semv, wf.After(approved)) - _ = wf.Action1(wd, "create release branch", r.createReleaseBranch, semv, wf.After(approved)) + _ = wf.Task1(wd, "create release milestone and issue", r.createReleaseMilestoneAndIssue, release, wf.After(approved)) + + _ = wf.Action2(wd, "create release branch", r.createReleaseBranch, release, prerelease, wf.After(approved)) return wd } -func (r *ReleaseVSCodeGoTasks) createReleaseMilestoneAndIssue(ctx *wf.TaskContext, semv semversion) (int, error) { +func (r *ReleaseVSCodeGoTasks) createReleaseMilestoneAndIssue(ctx *wf.TaskContext, semv releaseVersion) (int, error) { version := fmt.Sprintf("v%v.%v.%v", semv.Major, semv.Minor, semv.Patch) // The vscode-go release milestone name matches the release version. @@ -180,8 +182,8 @@ func (r *ReleaseVSCodeGoTasks) createReleaseMilestoneAndIssue(ctx *wf.TaskContex // createReleaseBranch creates corresponding release branch only for the initial // release candidate of a minor version. -func (r *ReleaseVSCodeGoTasks) createReleaseBranch(ctx *wf.TaskContext, semv semversion) error { - branch := fmt.Sprintf("release-v%v.%v", semv.Major, semv.Minor) +func (r *ReleaseVSCodeGoTasks) createReleaseBranch(ctx *wf.TaskContext, release releaseVersion, prerelease string) error { + branch := fmt.Sprintf("release-v%v.%v", release.Major, release.Minor) releaseHead, err := r.Gerrit.ReadBranchHead(ctx, "vscode-go", branch) if err == nil { @@ -194,11 +196,11 @@ func (r *ReleaseVSCodeGoTasks) createReleaseBranch(ctx *wf.TaskContext, semv sem } // Require vscode release branch existence if this is a non-minor release. - if semv.Patch != 0 { + if release.Patch != 0 { return fmt.Errorf("release branch is required for patch releases: %w", err) } - rc, err := semv.prereleaseVersion() + rc, err := prereleaseNumber(prerelease) if err != nil { return err } @@ -224,86 +226,78 @@ func (r *ReleaseVSCodeGoTasks) createReleaseBranch(ctx *wf.TaskContext, semv sem return nil } -// nextPrereleaseVersion determines the next pre-release version for the -// upcoming stable release of vscode-go by examining all existing tags in the -// repository. +// determineReleaseVersion determines the release version for the upcoming +// stable release of vscode-go by examining all existing tags in the repository. // // The versionBumpStrategy input indicates whether the pre-release should target // the next minor or next patch version. -func (r *ReleaseVSCodeGoTasks) nextPrereleaseVersion(ctx *wf.TaskContext, versionBumpStrategy string) (semversion, error) { +func (r *ReleaseVSCodeGoTasks) determineReleaseVersion(ctx *wf.TaskContext, versionBumpStrategy string) (releaseVersion, error) { tags, err := r.Gerrit.ListTags(ctx, "vscode-go") if err != nil { - return semversion{}, err + return releaseVersion{}, err } - semv := latestVersion(tags, isReleaseVersion, vsCodeGoStableVersion) + release, _ := latestVersion(tags, isReleaseVersion, isVSCodeGoStableVersion) switch versionBumpStrategy { case "next minor": - semv.Minor += 2 - semv.Patch = 0 + release.Minor += 2 + release.Patch = 0 case "next patch": - semv.Patch += 1 + release.Patch += 1 default: - return semversion{}, fmt.Errorf("unknown version selection strategy: %q", versionBumpStrategy) + return releaseVersion{}, fmt.Errorf("unknown version selection strategy: %q", versionBumpStrategy) } + return release, err +} - // latest to track the latest pre-release for the given semantic version. - latest := 0 - for _, v := range tags { - cur, ok := parseSemver(v) - if !ok { - continue - } - - if cur.Pre == "" { - continue - } - - if cur.Major != semv.Major || cur.Minor != semv.Minor || cur.Patch != semv.Patch { - continue - } +// nextPrereleaseVersion inspects the tags in vscode-go repo that match with the +// given version and finds the next pre-release version. +func (r *ReleaseVSCodeGoTasks) nextPrereleaseVersion(ctx *wf.TaskContext, release releaseVersion) (string, error) { + tags, err := r.Gerrit.ListTags(ctx, "vscode-go") + if err != nil { + return "", err + } - pre, err := cur.prereleaseVersion() - if err != nil { - continue - } - if pre > latest { - latest = pre - } + _, prerelease := latestVersion(tags, isSameReleaseVersion(release), isPrereleaseMatchRegex(`^rc\.\d+$`)) + if prerelease == "" { + return "rc.1", nil } - semv.Pre = fmt.Sprintf("rc.%v", latest+1) - return semv, err + pre, err := prereleaseNumber(prerelease) + if err != nil { + return "", err + } + return fmt.Sprintf("rc.%v", pre+1), nil } -func vsCodeGoStableVersion(semv semversion) bool { - return semv.Minor%2 == 0 +func isVSCodeGoStableVersion(release releaseVersion, _ string) bool { + return release.Minor%2 == 0 } -func vsCodeGoInsiderVersion(semv semversion) bool { - return semv.Minor%2 == 1 +func isVSCodeGoInsiderVersion(release releaseVersion, _ string) bool { + return release.Minor%2 == 1 } -// isReleaseVersion reports whether semv is a release version. +// isReleaseVersion reports whether input version is a release version. // (in other words, not a prerelease). -func isReleaseVersion(semv semversion) bool { - return semv.Pre == "" +func isReleaseVersion(_ releaseVersion, prerelease string) bool { + return prerelease == "" } -// isPrereleaseVersion reports whether semv is a pre-release version. +// isPrereleaseVersion reports whether input version is a pre-release version. // (in other words, not a release). -func isPrereleaseVersion(semv semversion) bool { - return semv.Pre != "" +func isPrereleaseVersion(_ releaseVersion, prerelease string) bool { + return prerelease != "" } // isPrereleaseMatchRegex reports whether the pre-release string of the input // version matches the regex expression. -func isPrereleaseMatchRegex(regex string) func(semversion) bool { - return func(semv semversion) bool { - if semv.Pre == "" { +func isPrereleaseMatchRegex(regex string) func(releaseVersion, string)bool { + return func(_ releaseVersion, prerelease string) bool { + if prerelease == "" { return false } - matched, err := regexp.MatchString(regex, semv.Pre) + matched, err := regexp.MatchString(regex, prerelease) if err != nil { return false } @@ -311,27 +305,30 @@ func isPrereleaseMatchRegex(regex string) func(semversion) bool { } } -func isSameMajorMinorPatch(want semversion) func(semversion) bool { - return func(got semversion) bool { - return got.Major == want.Major && got.Minor == want.Minor && got.Patch == want.Patch +// isSameReleaseVersion reports whether the version string have the same release +// version(same major minor and patch) as input. +func isSameReleaseVersion(want releaseVersion) func(releaseVersion, string) bool { + return func(got releaseVersion, _ string) bool { + return got == want } } -// 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 { +// latestVersion returns the releaseVersion and the prerelease tag of the latest +// version from the provided version strings. +// It considers only versions that are valid and match all the filters. +func latestVersion(versions []string, filters ...func(releaseVersion, string) bool) (releaseVersion, string) { latest := "" - latestSemv := semversion{} + latestRelease := releaseVersion{} + latestPre := "" for _, v := range versions { - semv, ok := parseSemver(v) + release, prerelease, ok := parseVersion(v); if !ok { continue } match := true for _, filter := range filters { - if !filter(semv) { + if !filter(release, prerelease) { match = false break } @@ -343,14 +340,15 @@ func latestVersion(versions []string, filters ...func(semversion) bool) semversi if semver.Compare(v, latest) == 1 { latest = v - latestSemv = semv + latestRelease = release + latestPre = prerelease } } - return latestSemv + return latestRelease, latestPre } -func (r *ReleaseVSCodeGoTasks) approveVersion(ctx *wf.TaskContext, semv semversion) error { - ctx.Printf("The next release candidate will be v%v.%v.%v-%s", semv.Major, semv.Minor, semv.Patch, semv.Pre) +func (r *ReleaseVSCodeGoTasks) approveVersion(ctx *wf.TaskContext, release releaseVersion, prerelease string) error { + ctx.Printf("The next release candidate will be %s-%s", release, prerelease) return r.ApproveAction(ctx) } diff --git a/internal/task/releasevscodego_test.go b/internal/task/releasevscodego_test.go index 5f79e1bc7a..d52996f786 100644 --- a/internal/task/releasevscodego_test.go +++ b/internal/task/releasevscodego_test.go @@ -15,58 +15,74 @@ import ( func TestLatestVersion(t *testing.T) { testcases := []struct { - name string - input []string - filters []func(semversion) bool - want semversion + name string + input []string + filters []func(releaseVersion, string) bool + wantRelease releaseVersion + wantPrerelease string }{ { - name: "choose the latest version v2.1.0", - input: []string{"v1.0.0", "v2.0.0", "v2.1.0"}, - want: semversion{Major: 2, Minor: 1, Patch: 0}, + name: "choose the latest version v2.1.0", + input: []string{"v1.0.0", "v2.0.0", "v2.1.0"}, + wantRelease: releaseVersion{Major: 2, Minor: 1, Patch: 0}, + wantPrerelease: "", + }, + { + name: "choose the latest version v2.2.0-pre.1", + input: []string{"v1.0.0", "v2.0.0", "v2.1.0", "v2.2.0-pre.1"}, + wantRelease: releaseVersion{Major: 2, Minor: 2, Patch: 0}, + wantPrerelease: "pre.1", }, { - name: "choose the latest version v2.2.0-pre.1", - input: []string{"v1.0.0", "v2.0.0", "v2.1.0", "v2.2.0-pre.1"}, - want: semversion{Major: 2, Minor: 2, Patch: 0, Pre: "pre.1"}, + name: "choose the latest pre-release version v2.2.0-pre.1", + input: []string{"v1.0.0", "v2.0.0", "v2.1.0", "v2.2.0-pre.1", "v2.3.0"}, + filters: []func(releaseVersion, string) bool{isPrereleaseVersion}, + wantRelease: releaseVersion{Major: 2, Minor: 2, Patch: 0}, + wantPrerelease: "pre.1", }, { - name: "choose the latest pre-release version v2.2.0-pre.1", - input: []string{"v1.0.0", "v2.0.0", "v2.1.0", "v2.2.0-pre.1", "v2.3.0"}, - filters: []func(semversion) bool{isPrereleaseVersion}, - want: semversion{Major: 2, Minor: 2, Patch: 0, Pre: "pre.1"}, + name: "choose the latest release version v2.1.0", + input: []string{"v1.0.0", "v2.0.0", "v2.1.0", "v2.2.0-pre.1"}, + filters: []func(releaseVersion, string) bool{isReleaseVersion}, + wantRelease: releaseVersion{Major: 2, Minor: 1, Patch: 0}, }, { - name: "choose the latest release version v2.1.0", - input: []string{"v1.0.0", "v2.0.0", "v2.1.0", "v2.2.0-pre.1"}, - filters: []func(semversion) bool{isReleaseVersion}, - want: semversion{Major: 2, Minor: 1, Patch: 0}, + name: "choose the latest version among v2.2.0", + input: []string{"v1.0.0", "v2.0.0", "v2.1.0", "v2.2.0-pre.3", "v2.2.0-pre.2", "v2.2.0-pre.1", "v2.3.0"}, + filters: []func(releaseVersion, string) bool{isSameReleaseVersion(releaseVersion{Major: 2, Minor: 2, Patch: 0})}, + wantRelease: releaseVersion{Major: 2, Minor: 2, Patch: 0}, + wantPrerelease: "pre.3", }, { - name: "choose the latest version among v2.2.0", - input: []string{"v1.0.0", "v2.0.0", "v2.1.0", "v2.2.0-pre.3", "v2.2.0-pre.2", "v2.2.0-pre.1", "v2.3.0"}, - filters: []func(semversion) bool{isSameMajorMinorPatch(semversion{Major: 2, Minor: 2, Patch: 0})}, - want: semversion{Major: 2, Minor: 2, Patch: 0, Pre: "pre.3"}, + name: "release version is consider newer than prerelease version", + input: []string{"v1.0.0", "v2.0.0", "v2.1.0", "v2.2.0", "v2.2.0-pre.2", "v2.2.0-pre.3", "v2.2.0-pre.1", "v2.3.0"}, + filters: []func(releaseVersion, string) bool{isSameReleaseVersion(releaseVersion{Major: 2, Minor: 2, Patch: 0})}, + wantRelease: releaseVersion{Major: 2, Minor: 2, Patch: 0}, }, { - name: "release version is consider newer than prerelease version", - input: []string{"v1.0.0", "v2.0.0", "v2.1.0", "v2.2.0", "v2.2.0-pre.2", "v2.2.0-pre.3", "v2.2.0-pre.1", "v2.3.0"}, - filters: []func(semversion) bool{isSameMajorMinorPatch(semversion{Major: 2, Minor: 2, Patch: 0})}, - want: semversion{Major: 2, Minor: 2, Patch: 0}, + name: "choose the latest pre-release version among v2.2.0", + input: []string{"v1.0.0", "v2.0.0", "v2.1.0", "v2.2.0", "v2.2.0-pre.2", "v2.2.0-pre.3", "v2.2.0-pre.1", "v2.3.0"}, + filters: []func(releaseVersion, string) bool{isPrereleaseVersion, isSameReleaseVersion(releaseVersion{Major: 2, Minor: 2, Patch: 0})}, + wantRelease: releaseVersion{Major: 2, Minor: 2, Patch: 0}, + wantPrerelease: "pre.3", }, { - name: "choose the latest pre-release version among v2.2.0", - input: []string{"v1.0.0", "v2.0.0", "v2.1.0", "v2.2.0", "v2.2.0-pre.2", "v2.2.0-pre.3", "v2.2.0-pre.1", "v2.3.0"}, - filters: []func(semversion) bool{isPrereleaseVersion, isSameMajorMinorPatch(semversion{Major: 2, Minor: 2, Patch: 0})}, - want: semversion{Major: 2, Minor: 2, Patch: 0, Pre: "pre.3"}, + name: "choose the latest pre-release version matching pattern among v2.2.0", + input: []string{"v2.2.0-pre.2", "v2.2.0-pre.3"}, + filters: []func(releaseVersion, string) bool{isPrereleaseMatchRegex(`^pre\.\d+$`), isSameReleaseVersion(releaseVersion{Major: 2, Minor: 2, Patch: 0})}, + wantRelease: releaseVersion{Major: 2, Minor: 2, Patch: 0}, + wantPrerelease: "pre.3", }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - got := latestVersion(tc.input, tc.filters...) - if got != tc.want { - t.Errorf("latestVersion() = %v, want %v", got, tc.want) + gotRelease, gotPrerelease := latestVersion(tc.input, tc.filters...) + if gotRelease != tc.wantRelease { + t.Errorf("latestVersion() = %v, want %v", gotRelease, tc.wantRelease) + } + if gotPrerelease != tc.wantPrerelease { + t.Errorf("latestVersion() = %v, want %v", gotPrerelease, tc.wantPrerelease) } }) } @@ -114,11 +130,11 @@ func TestCreateReleaseMilestoneAndIssue(t *testing.T) { GitHub: &tc.fakeGithub, } - semv, ok := parseSemver(tc.version) + release, _, ok := parseVersion(tc.version) if !ok { - t.Fatalf("parseSemver(%q) should success", tc.version) + t.Fatalf("parseVersion(%q) failed", tc.version) } - issueNumber, err := tasks.createReleaseMilestoneAndIssue(&workflow.TaskContext{Context: context.Background(), Logger: &testLogger{t, ""}}, semv) + issueNumber, err := tasks.createReleaseMilestoneAndIssue(&workflow.TaskContext{Context: context.Background(), Logger: &testLogger{t, ""}}, release) if err != nil { t.Fatal(err) } @@ -181,7 +197,7 @@ func TestCreateReleaseBranch(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - semv, ok := parseSemver(tc.version) + release, prerelease, ok := parseVersion(tc.version) if !ok { t.Fatalf("failed to parse the want version: %q", tc.version) } @@ -192,7 +208,7 @@ func TestCreateReleaseBranch(t *testing.T) { "go.sum": "\n", }) if tc.existingBranch { - vscodego.Branch(fmt.Sprintf("release-v%v.%v", semv.Major, semv.Minor), commit) + vscodego.Branch(fmt.Sprintf("release-v%v.%v", release.Major, release.Minor), commit) } gerrit := NewFakeGerrit(t, vscodego) @@ -200,7 +216,7 @@ func TestCreateReleaseBranch(t *testing.T) { Gerrit: gerrit, } - err := tasks.createReleaseBranch(&workflow.TaskContext{Context: ctx, Logger: &testLogger{t, ""}}, semv) + err := tasks.createReleaseBranch(&workflow.TaskContext{Context: ctx, Logger: &testLogger{t, ""}}, release, prerelease) if tc.wantErr && err == nil { t.Errorf("createReleaseBranch(%q) should return error but return nil", tc.version) } else if !tc.wantErr && err != nil { @@ -208,7 +224,7 @@ func TestCreateReleaseBranch(t *testing.T) { } if !tc.wantErr { - if _, err := gerrit.ReadBranchHead(ctx, "vscode-go", fmt.Sprintf("release-v%v.%v", semv.Major, semv.Minor)); err != nil { + if _, err := gerrit.ReadBranchHead(ctx, "vscode-go", fmt.Sprintf("release-v%v.%v", release.Major, release.Minor)); err != nil { t.Errorf("createReleaseBranch(%q) should ensure the release branch creation: %v", tc.version, err) } } @@ -216,36 +232,42 @@ func TestCreateReleaseBranch(t *testing.T) { } } -func TestNextPrereleaseVersion(t *testing.T) { +func TestDetermineReleaseAndNextPrereleaseVersion(t *testing.T) { + ctx := workflow.TaskContext{Context: context.Background(), Logger: &testLogger{t, ""}} tests := []struct { - name string - existingTags []string - versionRule string - wantVersion string + name string + existingTags []string + versionRule string + wantRelease releaseVersion + wantPrerelease string }{ { - name: "v0.44.0 have not released, have no release candidate", - existingTags: []string{"v0.44.0", "v0.43.0", "v0.42.0"}, - versionRule: "next minor", - wantVersion: "v0.46.0-rc.1", + name: "v0.44.0 have not released, have no release candidate", + existingTags: []string{"v0.44.0", "v0.43.0", "v0.42.0"}, + versionRule: "next minor", + wantRelease: releaseVersion{Major: 0, Minor: 46, Patch: 0}, + wantPrerelease: "rc.1", }, { - name: "v0.44.0 have not released but already have two release candidate", - existingTags: []string{"v0.44.0-rc.1", "v0.44.0-rc.2", "v0.43.0", "v0.42.0"}, - versionRule: "next minor", - wantVersion: "v0.44.0-rc.3", + name: "v0.44.0 have not released but already have two release candidate", + existingTags: []string{"v0.44.0-rc.1", "v0.44.0-rc.2", "v0.43.0", "v0.42.0"}, + versionRule: "next minor", + wantRelease: releaseVersion{Major: 0, Minor: 44, Patch: 0}, + wantPrerelease: "rc.3", }, { - name: "v0.44.3 have not released, have no release candidate", - existingTags: []string{"v0.44.2-rc.1", "v0.44.2", "v0.44.1", "v0.44.1-rc.1"}, - versionRule: "next patch", - wantVersion: "v0.44.3-rc.1", + name: "v0.44.3 have not released, have no release candidate", + existingTags: []string{"v0.44.2-rc.1", "v0.44.2", "v0.44.1", "v0.44.1-rc.1"}, + versionRule: "next patch", + wantRelease: releaseVersion{Major: 0, Minor: 44, Patch: 3}, + wantPrerelease: "rc.1", }, { - name: "v0.44.3 have not released but already have one release candidate", - existingTags: []string{"v0.44.3-rc.1", "v0.44.2", "v0.44.2-rc.1", "v0.44.1", "v0.44.1-rc.1"}, - versionRule: "next patch", - wantVersion: "v0.44.3-rc.2", + name: "v0.44.3 have not released but already have one release candidate", + existingTags: []string{"v0.44.3-rc.1", "v0.44.2", "v0.44.2-rc.1", "v0.44.1", "v0.44.1-rc.1"}, + versionRule: "next patch", + wantRelease: releaseVersion{Major: 0, Minor: 44, Patch: 3}, + wantPrerelease: "rc.2", }, } @@ -267,23 +289,18 @@ func TestNextPrereleaseVersion(t *testing.T) { Gerrit: gerrit, } - got, err := tasks.nextPrereleaseVersion(&workflow.TaskContext{Context: context.Background(), Logger: &testLogger{t, ""}}, tc.versionRule) - if err != nil { - t.Fatal(err) - } - - want, ok := parseSemver(tc.wantVersion) - if !ok { - t.Fatalf("failed to parse the want version: %q", tc.wantVersion) + gotRelease, err := tasks.determineReleaseVersion(&ctx, tc.versionRule) + if err != nil || gotRelease != tc.wantRelease { + t.Errorf("determineReleaseVersion(%q) = (%v, %v), want (%v, nil)", tc.versionRule, gotRelease, err, tc.wantRelease) } - if want != got { - t.Errorf("nextPrereleaseVersion(%q) = %v but want %v", tc.versionRule, got, want) + gotPrerelease, err := tasks.nextPrereleaseVersion(&ctx, gotRelease) + if err != nil || tc.wantPrerelease != gotPrerelease { + t.Errorf("nextPrerelease(%v) = (%s, %v) but want (%s, nil)", gotRelease, gotPrerelease, err, tc.wantPrerelease) } }) } } - func TestVSCodeGoActiveReleaseBranch(t *testing.T) { testcases := []struct { name string @@ -337,7 +354,6 @@ func TestVSCodeGoActiveReleaseBranch(t *testing.T) { if tc.want != got { t.Errorf("vsCodeGoActiveReleaseBranch() = %q, want %q", got, tc.want) } - }) } }