| // Copyright 2020 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package task |
| |
| import ( |
| "fmt" |
| "slices" |
| "strings" |
| |
| "golang.org/x/build/gerrit" |
| wf "golang.org/x/build/internal/workflow" |
| "golang.org/x/mod/semver" |
| ) |
| |
| // ReleaseGoplsTasks implements a new workflow definition include all the tasks |
| // to release a gopls. |
| type ReleaseGoplsTasks struct { |
| Gerrit GerritClient |
| } |
| |
| // NewDefinition create a new workflow definition for releasing gopls. |
| func (r *ReleaseGoplsTasks) NewDefinition() *wf.Definition { |
| wd := wf.New() |
| |
| // 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"}) |
| semversion := wf.Task1(wd, "validating input version", r.isValidVersion, version) |
| _ = wf.Action1(wd, "creating new branch if minor release", r.createBranchIfMinor, semversion) |
| return wd |
| } |
| |
| // 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 := fmt.Sprintf("gopls-release-branch.%v.%v", semv.Major, semv.Minor) |
| |
| // Require gopls release branch existence if this is a non-minor release. |
| if semv.Patch != 0 { |
| _, err := r.Gerrit.ReadBranchHead(ctx, "tools", branch) |
| return err |
| } |
| |
| // Return early if the branch already exist. |
| // This scenario should only occur if the initial minor release flow failed |
| // or was interrupted and subsequently re-triggered. |
| if _, err := r.Gerrit.ReadBranchHead(ctx, "tools", branch); err == nil { |
| return nil |
| } |
| |
| // Create the release branch using the revision from the head of master branch. |
| head, err := r.Gerrit.ReadBranchHead(ctx, "tools", "master") |
| if err != nil { |
| return err |
| } |
| |
| ctx.Printf("Creating branch %s at revision %s.\n", branch, head) |
| _, err = r.Gerrit.CreateBranch(ctx, "tools", branch, gerrit.BranchInput{Revision: head}) |
| return err |
| } |
| |
| func (r *ReleaseGoplsTasks) isValidVersion(ctx *wf.TaskContext, ver string) (semversion, error) { |
| if !semver.IsValid(ver) { |
| return semversion{}, 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) |
| } |
| |
| if !slices.Contains(versions, ver) { |
| return semversion{}, fmt.Errorf("the input %q is not next version of any existing versions", ver) |
| } |
| |
| semver, _ := parseSemver(ver) |
| return semver, nil |
| } |
| |
| // semversion is a parsed semantic version. |
| type semversion 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 |
| } |
| return parsed, ok |
| } |
| |
| // possibleGoplsVersions identifies suitable versions for the upcoming release |
| // based on the current tags in the repo. |
| func (r *ReleaseGoplsTasks) possibleGoplsVersions(ctx *wf.TaskContext) ([]string, error) { |
| tags, err := r.Gerrit.ListTags(ctx, "tools") |
| if err != nil { |
| return nil, err |
| } |
| |
| var semVersions []semversion |
| majorMinorPatch := map[int]map[int]map[int]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) != "" { |
| 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 |
| } |
| |
| 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) |
| } |
| } |
| |
| semver.Sort(possible) |
| return possible, nil |
| } |