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> 12package main 13 14import ( 15 "flag" 16 "fmt" 17 "go/types" 18 exec "golang.org/x/sys/execabs" 19 "io/ioutil" 20 "log" 21 "os" 22 "os/user" 23 "path/filepath" 24 "strconv" 25 "strings" 26 27 "golang.org/x/mod/modfile" 28 "golang.org/x/mod/semver" 29 "golang.org/x/tools/go/packages" 30) 31 32var ( 33 versionFlag = flag.String("version", "", "version to tag") 34 remoteFlag = flag.String("remote", "", "remote to which to push the tag") 35 releaseFlag = flag.Bool("release", false, "release is true if you intend to tag and push a release") 36) 37 38func main() { 39 flag.Parse() 40 41 if *versionFlag == "" { 42 log.Fatalf("must provide -version flag") 43 } 44 if !semver.IsValid(*versionFlag) { 45 log.Fatalf("invalid version %s", *versionFlag) 46 } 47 if semver.Major(*versionFlag) != "v0" { 48 log.Fatalf("expected major version v0, got %s", semver.Major(*versionFlag)) 49 } 50 if semver.Build(*versionFlag) != "" { 51 log.Fatalf("unexpected build suffix: %s", *versionFlag) 52 } 53 if *releaseFlag && *remoteFlag == "" { 54 log.Fatalf("must provide -remote flag if releasing") 55 } 56 user, err := user.Current() 57 if err != nil { 58 log.Fatal(err) 59 } 60 // Validate that the user is running the program from the gopls module. 61 wd, err := os.Getwd() 62 if err != nil { 63 log.Fatal(err) 64 } 65 if filepath.Base(wd) != "gopls" { 66 log.Fatalf("must run from the gopls module") 67 } 68 // Confirm that they are running on a branch with a name following the 69 // format of "gopls-release-branch.<major>.<minor>". 70 if err := validateBranchName(*versionFlag); err != nil { 71 log.Fatal(err) 72 } 73 // Confirm that they have updated the hardcoded version. 74 if err := validateHardcodedVersion(wd, *versionFlag); err != nil { 75 log.Fatal(err) 76 } 77 // Confirm that the versions in the go.mod file are correct. 78 if err := validateGoModFile(wd); err != nil { 79 log.Fatal(err) 80 } 81 earlyExitMsg := "Validated that the release is ready. Exiting without tagging and publishing." 82 if !*releaseFlag { 83 fmt.Println(earlyExitMsg) 84 os.Exit(0) 85 } 86 fmt.Println(`Proceeding to tagging and publishing the release... 87Please enter Y if you wish to proceed or anything else if you wish to exit.`) 88 // Accept and process user input. 89 var input string 90 fmt.Scanln(&input) 91 switch input { 92 case "Y": 93 fmt.Println("Proceeding to tagging and publishing the release.") 94 default: 95 fmt.Println(earlyExitMsg) 96 os.Exit(0) 97 } 98 // To tag the release: 99 // $ git -c user.email=username@google.com tag -a -m “<message>” gopls/v<major>.<minor>.<patch>-<pre-release> 100 goplsVersion := fmt.Sprintf("gopls/%s", *versionFlag) 101 cmd := exec.Command("git", "-c", fmt.Sprintf("user.email=%s@google.com", user.Username), "tag", "-a", "-m", fmt.Sprintf("%q", goplsVersion), goplsVersion) 102 if err := cmd.Run(); err != nil { 103 log.Fatal(err) 104 } 105 // Push the tag to the remote: 106 // $ git push <remote> gopls/v<major>.<minor>.<patch>-pre.1 107 cmd = exec.Command("git", "push", *remoteFlag, goplsVersion) 108 if err := cmd.Run(); err != nil { 109 log.Fatal(err) 110 } 111} 112 113// validateBranchName reports whether the user's current branch name is of the 114// form "gopls-release-branch.<major>.<minor>". It reports an error if not. 115func validateBranchName(version string) error { 116 cmd := exec.Command("git", "branch", "--show-current") 117 stdout, err := cmd.Output() 118 if err != nil { 119 return err 120 } 121 branch := strings.TrimSpace(string(stdout)) 122 expectedBranch := fmt.Sprintf("gopls-release-branch.%s", strings.TrimPrefix(semver.MajorMinor(version), "v")) 123 if branch != expectedBranch { 124 return fmt.Errorf("expected release branch %s, got %s", expectedBranch, branch) 125 } 126 return nil 127} 128 129// validateHardcodedVersion reports whether the version hardcoded in the gopls 130// binary is equivalent to the version being published. It reports an error if 131// not. 132func validateHardcodedVersion(wd string, version string) error { 133 pkgs, err := packages.Load(&packages.Config{ 134 Dir: filepath.Dir(wd), 135 Mode: packages.NeedName | packages.NeedFiles | 136 packages.NeedCompiledGoFiles | packages.NeedImports | 137 packages.NeedTypes | packages.NeedTypesSizes, 138 }, "golang.org/x/tools/internal/lsp/debug") 139 if err != nil { 140 return err 141 } 142 if len(pkgs) != 1 { 143 return fmt.Errorf("expected 1 package, got %v", len(pkgs)) 144 } 145 pkg := pkgs[0] 146 obj := pkg.Types.Scope().Lookup("Version") 147 c, ok := obj.(*types.Const) 148 if !ok { 149 return fmt.Errorf("no constant named Version") 150 } 151 hardcodedVersion, err := strconv.Unquote(c.Val().ExactString()) 152 if err != nil { 153 return err 154 } 155 if semver.Prerelease(hardcodedVersion) != "" { 156 return fmt.Errorf("unexpected pre-release for hardcoded version: %s", hardcodedVersion) 157 } 158 // Don't worry about pre-release tags and expect that there is no build 159 // suffix. 160 version = strings.TrimSuffix(version, semver.Prerelease(version)) 161 if hardcodedVersion != version { 162 return fmt.Errorf("expected version to be %s, got %s", *versionFlag, hardcodedVersion) 163 } 164 return nil 165} 166 167func validateGoModFile(wd string) error { 168 filename := filepath.Join(wd, "go.mod") 169 data, err := ioutil.ReadFile(filename) 170 if err != nil { 171 return err 172 } 173 gomod, err := modfile.Parse(filename, data, nil) 174 if err != nil { 175 return err 176 } 177 // Confirm that there is no replace directive in the go.mod file. 178 if len(gomod.Replace) > 0 { 179 return fmt.Errorf("expected no replace directives, got %v", len(gomod.Replace)) 180 } 181 // Confirm that the version of x/tools in the gopls/go.mod file points to 182 // the second-to-last commit. (The last commit will be the one to update the 183 // go.mod file.) 184 cmd := exec.Command("git", "rev-parse", "@~") 185 stdout, err := cmd.Output() 186 if err != nil { 187 return err 188 } 189 hash := string(stdout) 190 // Find the golang.org/x/tools require line and compare the versions. 191 var version string 192 for _, req := range gomod.Require { 193 if req.Mod.Path == "golang.org/x/tools" { 194 version = req.Mod.Version 195 break 196 } 197 } 198 if version == "" { 199 return fmt.Errorf("no require for golang.org/x/tools") 200 } 201 split := strings.Split(version, "-") 202 if len(split) != 3 { 203 return fmt.Errorf("unexpected pseudoversion format %s", version) 204 } 205 last := split[len(split)-1] 206 if last == "" { 207 return fmt.Errorf("unexpected pseudoversion format %s", version) 208 } 209 if !strings.HasPrefix(hash, last) { 210 return fmt.Errorf("golang.org/x/tools pseudoversion should be at commit %s, instead got %s", hash, last) 211 } 212 return nil 213} 214