golang.org/x/tools/gopls@v0.15.3/release/release.go (about)

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  //
     5  // Package release checks that the a given version of gopls is ready for
     6  // release. It can also tag and publish the release.
     7  //
     8  // To run:
     9  //
    10  // $ cd $GOPATH/src/golang.org/x/tools/gopls
    11  // $ go run release/release.go -version=<version>
    12  package main
    13  
    14  import (
    15  	"flag"
    16  	"fmt"
    17  	"go/types"
    18  	"log"
    19  	"os"
    20  	"os/exec"
    21  	"path/filepath"
    22  	"strconv"
    23  	"strings"
    24  
    25  	"golang.org/x/mod/modfile"
    26  	"golang.org/x/mod/semver"
    27  	"golang.org/x/tools/go/packages"
    28  )
    29  
    30  var versionFlag = flag.String("version", "", "version to tag")
    31  
    32  func main() {
    33  	flag.Parse()
    34  
    35  	if *versionFlag == "" {
    36  		log.Fatalf("must provide -version flag")
    37  	}
    38  	if !semver.IsValid(*versionFlag) {
    39  		log.Fatalf("invalid version %s", *versionFlag)
    40  	}
    41  	if semver.Major(*versionFlag) != "v0" {
    42  		log.Fatalf("expected major version v0, got %s", semver.Major(*versionFlag))
    43  	}
    44  	if semver.Build(*versionFlag) != "" {
    45  		log.Fatalf("unexpected build suffix: %s", *versionFlag)
    46  	}
    47  	// Validate that the user is running the program from the gopls module.
    48  	wd, err := os.Getwd()
    49  	if err != nil {
    50  		log.Fatal(err)
    51  	}
    52  	if filepath.Base(wd) != "gopls" {
    53  		log.Fatalf("must run from the gopls module")
    54  	}
    55  	// Confirm that they have updated the hardcoded version.
    56  	if err := validateHardcodedVersion(*versionFlag); err != nil {
    57  		log.Fatal(err)
    58  	}
    59  	// Confirm that the versions in the go.mod file are correct.
    60  	if err := validateGoModFile(wd); err != nil {
    61  		log.Fatal(err)
    62  	}
    63  	fmt.Println("Validated that the release is ready.")
    64  	os.Exit(0)
    65  }
    66  
    67  // validateHardcodedVersion reports whether the version hardcoded in the gopls
    68  // binary is equivalent to the version being published. It reports an error if
    69  // not.
    70  func validateHardcodedVersion(version string) error {
    71  	const debugPkg = "golang.org/x/tools/gopls/internal/debug"
    72  	pkgs, err := packages.Load(&packages.Config{
    73  		Mode: packages.NeedName | packages.NeedFiles |
    74  			packages.NeedCompiledGoFiles | packages.NeedImports |
    75  			packages.NeedTypes | packages.NeedTypesSizes,
    76  	}, debugPkg)
    77  	if err != nil {
    78  		return err
    79  	}
    80  	if len(pkgs) != 1 {
    81  		return fmt.Errorf("expected 1 package, got %v", len(pkgs))
    82  	}
    83  	pkg := pkgs[0]
    84  	if len(pkg.Errors) > 0 {
    85  		return fmt.Errorf("failed to load %q: first error: %w", debugPkg, pkg.Errors[0])
    86  	}
    87  	obj := pkg.Types.Scope().Lookup("Version")
    88  	c, ok := obj.(*types.Const)
    89  	if !ok {
    90  		return fmt.Errorf("no constant named Version")
    91  	}
    92  	hardcodedVersion, err := strconv.Unquote(c.Val().ExactString())
    93  	if err != nil {
    94  		return err
    95  	}
    96  	if semver.Prerelease(hardcodedVersion) != "" {
    97  		return fmt.Errorf("unexpected pre-release for hardcoded version: %s", hardcodedVersion)
    98  	}
    99  	// Don't worry about pre-release tags and expect that there is no build
   100  	// suffix.
   101  	version = strings.TrimSuffix(version, semver.Prerelease(version))
   102  	if hardcodedVersion != version {
   103  		return fmt.Errorf("expected version to be %s, got %s", *versionFlag, hardcodedVersion)
   104  	}
   105  	return nil
   106  }
   107  
   108  func validateGoModFile(goplsDir string) error {
   109  	filename := filepath.Join(goplsDir, "go.mod")
   110  	data, err := os.ReadFile(filename)
   111  	if err != nil {
   112  		return err
   113  	}
   114  	gomod, err := modfile.Parse(filename, data, nil)
   115  	if err != nil {
   116  		return err
   117  	}
   118  	// Confirm that there is no replace directive in the go.mod file.
   119  	if len(gomod.Replace) > 0 {
   120  		return fmt.Errorf("expected no replace directives, got %v", len(gomod.Replace))
   121  	}
   122  	// Confirm that the version of x/tools in the gopls/go.mod file points to
   123  	// the second-to-last commit. (The last commit will be the one to update the
   124  	// go.mod file.)
   125  	cmd := exec.Command("git", "rev-parse", "@~")
   126  	stdout, err := cmd.Output()
   127  	if err != nil {
   128  		return err
   129  	}
   130  	hash := string(stdout)
   131  	// Find the golang.org/x/tools require line and compare the versions.
   132  	var version string
   133  	for _, req := range gomod.Require {
   134  		if req.Mod.Path == "golang.org/x/tools" {
   135  			version = req.Mod.Version
   136  			break
   137  		}
   138  	}
   139  	if version == "" {
   140  		return fmt.Errorf("no require for golang.org/x/tools")
   141  	}
   142  	split := strings.Split(version, "-")
   143  	if len(split) != 3 {
   144  		return fmt.Errorf("unexpected pseudoversion format %s", version)
   145  	}
   146  	last := split[len(split)-1]
   147  	if last == "" {
   148  		return fmt.Errorf("unexpected pseudoversion format %s", version)
   149  	}
   150  	if !strings.HasPrefix(hash, last) {
   151  		return fmt.Errorf("golang.org/x/tools pseudoversion should be at commit %s, instead got %s", hash, last)
   152  	}
   153  	return nil
   154  }