|
| 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 | + "io/ioutil" |
| 19 | + "log" |
| 20 | + "os" |
| 21 | + "os/exec" |
| 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 | + |
| 32 | +var ( |
| 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 | + |
| 38 | +func 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... |
| 87 | +Please 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 [email protected] 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=%[email protected]", 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. |
| 115 | +func 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. |
| 132 | +func 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 | + |
| 167 | +func 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 | +} |
0 commit comments