golang.org/x/tools/gopls@v0.15.3/release/release.go (about) 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 "log" 19 "os" 20 "os/exec" 21 "path/filepath" 22 "strconv" 23 "strings" 24 25 "golang.org/x/mod/modfile" 26 "golang.org/x/mod/semver" 27 "golang.org/x/tools/go/packages" 28 ) 29 30 var versionFlag = flag.String("version", "", "version to tag") 31 32 func main() { 33 flag.Parse() 34 35 if *versionFlag == "" { 36 log.Fatalf("must provide -version flag") 37 } 38 if !semver.IsValid(*versionFlag) { 39 log.Fatalf("invalid version %s", *versionFlag) 40 } 41 if semver.Major(*versionFlag) != "v0" { 42 log.Fatalf("expected major version v0, got %s", semver.Major(*versionFlag)) 43 } 44 if semver.Build(*versionFlag) != "" { 45 log.Fatalf("unexpected build suffix: %s", *versionFlag) 46 } 47 // Validate that the user is running the program from the gopls module. 48 wd, err := os.Getwd() 49 if err != nil { 50 log.Fatal(err) 51 } 52 if filepath.Base(wd) != "gopls" { 53 log.Fatalf("must run from the gopls module") 54 } 55 // Confirm that they have updated the hardcoded version. 56 if err := validateHardcodedVersion(*versionFlag); err != nil { 57 log.Fatal(err) 58 } 59 // Confirm that the versions in the go.mod file are correct. 60 if err := validateGoModFile(wd); err != nil { 61 log.Fatal(err) 62 } 63 fmt.Println("Validated that the release is ready.") 64 os.Exit(0) 65 } 66 67 // validateHardcodedVersion reports whether the version hardcoded in the gopls 68 // binary is equivalent to the version being published. It reports an error if 69 // not. 70 func validateHardcodedVersion(version string) error { 71 const debugPkg = "golang.org/x/tools/gopls/internal/debug" 72 pkgs, err := packages.Load(&packages.Config{ 73 Mode: packages.NeedName | packages.NeedFiles | 74 packages.NeedCompiledGoFiles | packages.NeedImports | 75 packages.NeedTypes | packages.NeedTypesSizes, 76 }, debugPkg) 77 if err != nil { 78 return err 79 } 80 if len(pkgs) != 1 { 81 return fmt.Errorf("expected 1 package, got %v", len(pkgs)) 82 } 83 pkg := pkgs[0] 84 if len(pkg.Errors) > 0 { 85 return fmt.Errorf("failed to load %q: first error: %w", debugPkg, pkg.Errors[0]) 86 } 87 obj := pkg.Types.Scope().Lookup("Version") 88 c, ok := obj.(*types.Const) 89 if !ok { 90 return fmt.Errorf("no constant named Version") 91 } 92 hardcodedVersion, err := strconv.Unquote(c.Val().ExactString()) 93 if err != nil { 94 return err 95 } 96 if semver.Prerelease(hardcodedVersion) != "" { 97 return fmt.Errorf("unexpected pre-release for hardcoded version: %s", hardcodedVersion) 98 } 99 // Don't worry about pre-release tags and expect that there is no build 100 // suffix. 101 version = strings.TrimSuffix(version, semver.Prerelease(version)) 102 if hardcodedVersion != version { 103 return fmt.Errorf("expected version to be %s, got %s", *versionFlag, hardcodedVersion) 104 } 105 return nil 106 } 107 108 func validateGoModFile(goplsDir string) error { 109 filename := filepath.Join(goplsDir, "go.mod") 110 data, err := os.ReadFile(filename) 111 if err != nil { 112 return err 113 } 114 gomod, err := modfile.Parse(filename, data, nil) 115 if err != nil { 116 return err 117 } 118 // Confirm that there is no replace directive in the go.mod file. 119 if len(gomod.Replace) > 0 { 120 return fmt.Errorf("expected no replace directives, got %v", len(gomod.Replace)) 121 } 122 // Confirm that the version of x/tools in the gopls/go.mod file points to 123 // the second-to-last commit. (The last commit will be the one to update the 124 // go.mod file.) 125 cmd := exec.Command("git", "rev-parse", "@~") 126 stdout, err := cmd.Output() 127 if err != nil { 128 return err 129 } 130 hash := string(stdout) 131 // Find the golang.org/x/tools require line and compare the versions. 132 var version string 133 for _, req := range gomod.Require { 134 if req.Mod.Path == "golang.org/x/tools" { 135 version = req.Mod.Version 136 break 137 } 138 } 139 if version == "" { 140 return fmt.Errorf("no require for golang.org/x/tools") 141 } 142 split := strings.Split(version, "-") 143 if len(split) != 3 { 144 return fmt.Errorf("unexpected pseudoversion format %s", version) 145 } 146 last := split[len(split)-1] 147 if last == "" { 148 return fmt.Errorf("unexpected pseudoversion format %s", version) 149 } 150 if !strings.HasPrefix(hash, last) { 151 return fmt.Errorf("golang.org/x/tools pseudoversion should be at commit %s, instead got %s", hash, last) 152 } 153 return nil 154 }