diff --git a/gopls/go.mod b/gopls/go.mod index c3cb3af16a..3fac43f78f 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -6,6 +6,7 @@ require ( github.com/jba/templatecheck v0.5.0 github.com/sanity-io/litter v1.3.0 github.com/sergi/go-diff v1.1.0 + golang.org/x/mod v0.3.0 golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 honnef.co/go/tools v0.0.1-2020.1.6 diff --git a/gopls/release/release.go b/gopls/release/release.go new file mode 100644 index 0000000000..62455fe1bb --- /dev/null +++ b/gopls/release/release.go @@ -0,0 +1,213 @@ +// 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 release checks that the a given version of gopls is ready for +// release. It can also tag and publish the release. +// +// To run: +// +// $ cd $GOPATH/src/golang.org/x/tools/gopls +// $ go run release/release.go -version= +package main + +import ( + "flag" + "fmt" + "go/types" + "io/ioutil" + "log" + "os" + "os/exec" + "os/user" + "path/filepath" + "strconv" + "strings" + + "golang.org/x/mod/modfile" + "golang.org/x/mod/semver" + "golang.org/x/tools/go/packages" +) + +var ( + versionFlag = flag.String("version", "", "version to tag") + remoteFlag = flag.String("remote", "", "remote to which to push the tag") + releaseFlag = flag.Bool("release", false, "release is true if you intend to tag and push a release") +) + +func main() { + flag.Parse() + + if *versionFlag == "" { + log.Fatalf("must provide -version flag") + } + if !semver.IsValid(*versionFlag) { + log.Fatalf("invalid version %s", *versionFlag) + } + if semver.Major(*versionFlag) != "v0" { + log.Fatalf("expected major version v0, got %s", semver.Major(*versionFlag)) + } + if semver.Build(*versionFlag) != "" { + log.Fatalf("unexpected build suffix: %s", *versionFlag) + } + if *releaseFlag && *remoteFlag == "" { + log.Fatalf("must provide -remote flag if releasing") + } + user, err := user.Current() + if err != nil { + log.Fatal(err) + } + // Validate that the user is running the program from the gopls module. + wd, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + if filepath.Base(wd) != "gopls" { + log.Fatalf("must run from the gopls module") + } + // Confirm that they are running on a branch with a name following the + // format of "gopls-release-branch..". + if err := validateBranchName(*versionFlag); err != nil { + log.Fatal(err) + } + // Confirm that they have updated the hardcoded version. + if err := validateHardcodedVersion(wd, *versionFlag); err != nil { + log.Fatal(err) + } + // Confirm that the versions in the go.mod file are correct. + if err := validateGoModFile(wd); err != nil { + log.Fatal(err) + } + earlyExitMsg := "Validated that the release is ready. Exiting without tagging and publishing." + if !*releaseFlag { + fmt.Println(earlyExitMsg) + os.Exit(0) + } + fmt.Println(`Proceeding to tagging and publishing the release... +Please enter Y if you wish to proceed or anything else if you wish to exit.`) + // Accept and process user input. + var input string + fmt.Scanln(&input) + switch input { + case "Y": + fmt.Println("Proceeding to tagging and publishing the release.") + default: + fmt.Println(earlyExitMsg) + os.Exit(0) + } + // To tag the release: + // $ git -c user.email=username@google.com tag -a -m “” gopls/v..- + goplsVersion := fmt.Sprintf("gopls/%s", *versionFlag) + cmd := exec.Command("git", "-c", fmt.Sprintf("user.email=%s@google.com", user.Username), "tag", "-a", "-m", fmt.Sprintf("%q", goplsVersion), goplsVersion) + if err := cmd.Run(); err != nil { + log.Fatal(err) + } + // Push the tag to the remote: + // $ git push gopls/v..-pre.1 + cmd = exec.Command("git", "push", *remoteFlag, goplsVersion) + if err := cmd.Run(); err != nil { + log.Fatal(err) + } +} + +// validateBranchName reports whether the user's current branch name is of the +// form "gopls-release-branch..". It reports an error if not. +func validateBranchName(version string) error { + cmd := exec.Command("git", "branch", "--show-current") + stdout, err := cmd.Output() + if err != nil { + return err + } + branch := strings.TrimSpace(string(stdout)) + expectedBranch := fmt.Sprintf("gopls-release-branch.%s", strings.TrimPrefix(semver.MajorMinor(version), "v")) + if branch != expectedBranch { + return fmt.Errorf("expected release branch %s, got %s", expectedBranch, branch) + } + return nil +} + +// validateHardcodedVersion reports whether the version hardcoded in the gopls +// binary is equivalent to the version being published. It reports an error if +// not. +func validateHardcodedVersion(wd string, version string) error { + pkgs, err := packages.Load(&packages.Config{ + Dir: filepath.Dir(wd), + Mode: packages.NeedName | packages.NeedFiles | + packages.NeedCompiledGoFiles | packages.NeedImports | + packages.NeedTypes | packages.NeedTypesSizes, + }, "golang.org/x/tools/internal/lsp/debug") + if err != nil { + return err + } + if len(pkgs) != 1 { + return fmt.Errorf("expected 1 package, got %v", len(pkgs)) + } + pkg := pkgs[0] + obj := pkg.Types.Scope().Lookup("Version") + c, ok := obj.(*types.Const) + if !ok { + return fmt.Errorf("no constant named Version") + } + hardcodedVersion, err := strconv.Unquote(c.Val().ExactString()) + if err != nil { + return err + } + if semver.Prerelease(hardcodedVersion) != "" { + return fmt.Errorf("unexpected pre-release for hardcoded version: %s", hardcodedVersion) + } + // Don't worry about pre-release tags and expect that there is no build + // suffix. + version = strings.TrimSuffix(version, semver.Prerelease(version)) + if hardcodedVersion != version { + return fmt.Errorf("expected version to be %s, got %s", *versionFlag, hardcodedVersion) + } + return nil +} + +func validateGoModFile(wd string) error { + filename := filepath.Join(wd, "go.mod") + data, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + gomod, err := modfile.Parse(filename, data, nil) + if err != nil { + return err + } + // Confirm that there is no replace directive in the go.mod file. + if len(gomod.Replace) > 0 { + return fmt.Errorf("expected no replace directives, got %v", len(gomod.Replace)) + } + // Confirm that the version of x/tools in the gopls/go.mod file points to + // the second-to-last commit. (The last commit will be the one to update the + // go.mod file.) + cmd := exec.Command("git", "rev-parse", "@~") + stdout, err := cmd.Output() + if err != nil { + return err + } + hash := string(stdout) + // Find the golang.org/x/tools require line and compare the versions. + var version string + for _, req := range gomod.Require { + if req.Mod.Path == "golang.org/x/tools" { + version = req.Mod.Version + break + } + } + if version == "" { + return fmt.Errorf("no require for golang.org/x/tools") + } + split := strings.Split(version, "-") + if len(split) != 3 { + return fmt.Errorf("unexpected pseudoversion format %s", version) + } + last := split[len(split)-1] + if last == "" { + return fmt.Errorf("unexpected pseudoversion format %s", version) + } + if !strings.HasPrefix(hash, last) { + return fmt.Errorf("golang.org/x/tools pseudoversion should be at commit %s, instead got %s", hash, last) + } + return nil +} diff --git a/internal/lsp/debug/info.go b/internal/lsp/debug/info.go index f01c8dd905..d4580190a2 100644 --- a/internal/lsp/debug/info.go +++ b/internal/lsp/debug/info.go @@ -26,7 +26,7 @@ const ( ) // Version is a manually-updated mechanism for tracking versions. -var Version = "master" +const Version = "master" // ServerVersion is the format used by gopls to report its version to the // client. This format is structured so that the client can parse it easily.