github.com/cornelk/go-cloud@v0.17.1/internal/releasehelper/releasehelper.go (about) 1 // Copyright 2019 The Go Cloud Development Kit Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Helper tool for creating new releases of the Go CDK. Run without arguments 16 // or with 'help' for details. 17 package main 18 19 import ( 20 "bufio" 21 "encoding/json" 22 "fmt" 23 "log" 24 "os" 25 "os/exec" 26 "path/filepath" 27 "regexp" 28 "strings" 29 ) 30 31 var helpText string = ` 32 Helper tool for creating new releases of the Go CDK. 33 34 Automates the modifications required in the project's 35 go.mod files to create an test new releases. 36 37 The tool processes all modules listed in the 'allmodules' 38 file. For each module it handles all dependencies on 39 other github.com/cornelk/go-cloud modules. 40 41 Run it from the root directory of the repository, as follows: 42 43 $ %s <command> 44 45 Where command is one of the following: 46 47 addreplace adds 'replace' directives to point to local versions 48 for testing. 49 50 dropreplace removes these directives. 51 52 setversion <version> 53 sets 'required' version of modules to a given version formatted 54 as vX.Y.Z 55 56 tag <version> 57 runs 'git tag <module>/<version>' for all CDK modules 58 59 help prints this usage message 60 ` 61 62 func printHelp() { 63 _, binName := filepath.Split(os.Args[0]) 64 fmt.Fprintf(os.Stderr, helpText, binName) 65 fmt.Fprintln(os.Stderr) 66 } 67 68 // cmdCheck invokes the command given in s, echoing the invocation to stdout. 69 // It checks that the command was successful and returns its standard output. 70 // If the command returned a non-0 status, log.Fatal is invoked. 71 func cmdCheck(s string) []byte { 72 fmt.Printf(" -> %s\n", s) 73 fields := strings.Fields(s) 74 if len(fields) < 1 { 75 log.Fatal(`Expected "command <arguments>"`) 76 } 77 b, err := exec.Command(fields[0], fields[1:]...).Output() 78 if exiterr, ok := err.(*exec.ExitError); ok { 79 log.Fatalf("%s; stderr: %s\n", err, string(exiterr.Stderr)) 80 } else if err != nil { 81 log.Fatal("exec.Command", err) 82 } 83 return b 84 } 85 86 // These types are taken from "go mod help edit", in order to parse the JSON 87 // output of `go mod edit -json`. 88 type GoMod struct { 89 Module Module 90 Go string 91 Require []Require 92 Exclude []Module 93 Replace []Replace 94 } 95 96 type Module struct { 97 Path string 98 Version string 99 } 100 101 type Require struct { 102 Path string 103 Version string 104 Indirect bool 105 } 106 107 type Replace struct { 108 Old Module 109 New Module 110 } 111 112 // parseModuleInfo parses module information from a go.mod file at path. 113 func parseModuleInfo(path string) GoMod { 114 rawJson := cmdCheck("go mod edit -json " + path) 115 var modInfo GoMod 116 err := json.Unmarshal(rawJson, &modInfo) 117 if err != nil { 118 log.Fatal(err) 119 } 120 return modInfo 121 } 122 123 // runOnGomod processes a single go.mod file (located in directory 'path'). 124 // Each require in the go.mod file is processed with reqHandler, a callback 125 // function. It's called with these arguments: 126 // 127 // gomodPath - path to the go.mod file where this 'require' was found 128 // mod - name of the module being 'require'd 129 // modPath - mod's location in the filesystem relative to 130 // the go.mod 'require'ing it 131 func runOnGomod(path string, reqHandler func(gomodPath, mod, modPath string)) { 132 gomodPath := filepath.Join(path, "go.mod") 133 fmt.Println("Processing", gomodPath) 134 modInfo := parseModuleInfo(gomodPath) 135 136 base := "github.com/cornelk/go-cloud" 137 138 for _, r := range modInfo.Require { 139 // Find requirements on modules within the github.com/cornelk/go-cloud tree. 140 if strings.HasPrefix(r.Path, base) { 141 // Find the relative path from 'path' and the module required here. 142 var reqPath string 143 if r.Path == base { 144 reqPath = "." 145 } else { 146 reqPath = strings.TrimPrefix(r.Path, base+"/") 147 } 148 rel, err := filepath.Rel(path, reqPath) 149 if err != nil { 150 log.Fatal(err) 151 } 152 // When path is '.', filepath.Rel will append a /. to the result and we 153 // may get paths like ../../. 154 if strings.HasSuffix(rel, "/.") { 155 rel, _ = filepath.Split(rel) 156 } 157 158 reqHandler(gomodPath, r.Path, rel) 159 } 160 } 161 } 162 163 func gomodAddReplace(path string) { 164 runOnGomod(path, func(gomodPath, mod, modPath string) { 165 cmdCheck(fmt.Sprintf("go mod edit -replace=%s=%s %s", mod, modPath, gomodPath)) 166 }) 167 } 168 169 func gomodDropReplace(path string) { 170 runOnGomod(path, func(gomodPath, mod, modPath string) { 171 cmdCheck(fmt.Sprintf("go mod edit -dropreplace=%s %s", mod, gomodPath)) 172 }) 173 } 174 175 func gomodSetVersion(path string, v string) { 176 runOnGomod(path, func(gomodPath, mod, modPath string) { 177 cmdCheck(fmt.Sprintf("go mod edit -require=%s@%s %s", mod, v, gomodPath)) 178 }) 179 } 180 181 func gomodTag(path string, v string) { 182 var tagName string 183 if path == "." { 184 tagName = v 185 } else { 186 tagName = filepath.Join(path, v) 187 } 188 cmdCheck(fmt.Sprintf("git tag %s", tagName)) 189 } 190 191 func validSemanticVersion(v string) bool { 192 match, err := regexp.MatchString(`v\d+\.\d+\.\d+`, v) 193 if err != nil { 194 return false 195 } 196 return match 197 } 198 199 func main() { 200 if len(os.Args) < 2 { 201 printHelp() 202 os.Exit(0) 203 } 204 205 var gomodHandler func(path string) 206 switch os.Args[1] { 207 case "help": 208 printHelp() 209 os.Exit(0) 210 case "addreplace": 211 gomodHandler = gomodAddReplace 212 case "dropreplace": 213 gomodHandler = gomodDropReplace 214 case "setversion": 215 if len(os.Args) < 3 || !validSemanticVersion(os.Args[2]) { 216 printHelp() 217 os.Exit(1) 218 } 219 gomodHandler = func(path string) { 220 gomodSetVersion(path, os.Args[2]) 221 } 222 case "tag": 223 if len(os.Args) < 3 || !validSemanticVersion(os.Args[2]) { 224 printHelp() 225 os.Exit(1) 226 } 227 gomodHandler = func(path string) { 228 gomodTag(path, os.Args[2]) 229 } 230 default: 231 printHelp() 232 os.Exit(1) 233 } 234 235 f, err := os.Open("allmodules") 236 if err != nil { 237 log.Fatal(err) 238 } 239 240 input := bufio.NewScanner(f) 241 input.Split(bufio.ScanLines) 242 for input.Scan() { 243 if len(input.Text()) > 0 && !strings.HasPrefix(input.Text(), "#") { 244 fields := strings.Fields(input.Text()) 245 if len(fields) != 2 { 246 log.Fatalf("want 2 fields, got '%s'\n", input.Text()) 247 } 248 // "tag" only runs if the released field is "yes". Other commands run 249 // for every line. 250 if os.Args[1] != "tag" || fields[1] == "yes" { 251 gomodHandler(fields[0]) 252 } 253 } 254 } 255 256 if input.Err() != nil { 257 log.Fatal(input.Err()) 258 } 259 }