| // 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=<version> |
| package main |
| |
| import ( |
| "flag" |
| "fmt" |
| "go/types" |
| exec "golang.org/x/sys/execabs" |
| "io/ioutil" |
| "log" |
| "os" |
| "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.<major>.<minor>". |
| 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 “<message>” gopls/v<major>.<minor>.<patch>-<pre-release> |
| 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 <remote> gopls/v<major>.<minor>.<patch>-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.<major>.<minor>". 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 |
| } |