Skip to content

Commit 2b0845d

Browse files
committed
gopls/release: add a command to validate the gopls release process
The gopls release process has a number of steps which can be easily forgotten, but they are also easy enough to check. In the future, we can certainly automate this process further, but this basic program will validate that mistakes like golang/go#43256 are not made again in the future. Change-Id: I04641ae202bc6615f2e4c8810b5dab4885d37fd4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/279715 Trust: Rebecca Stambler <[email protected]> Run-TryBot: Rebecca Stambler <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent 57089f8 commit 2b0845d

File tree

3 files changed

+215
-1
lines changed

3 files changed

+215
-1
lines changed

gopls/go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/jba/templatecheck v0.5.0
77
github.com/sanity-io/litter v1.3.0
88
github.com/sergi/go-diff v1.1.0
9+
golang.org/x/mod v0.3.0
910
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58
1011
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
1112
honnef.co/go/tools v0.0.1-2020.1.6

gopls/release/release.go

+213
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
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+
}

internal/lsp/debug/info.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const (
2626
)
2727

2828
// Version is a manually-updated mechanism for tracking versions.
29-
var Version = "master"
29+
const Version = "master"
3030

3131
// ServerVersion is the format used by gopls to report its version to the
3232
// client. This format is structured so that the client can parse it easily.

0 commit comments

Comments
 (0)