blob: 8f722511dd051c5c45990846a1d9328c6e625ed9 [file] [log] [blame]
// 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
}