github.com/thiagoyeds/go-cloud@v0.26.0/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 gocloud.dev 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 := "gocloud.dev"
   137  
   138  	for _, r := range modInfo.Require {
   139  		// Find requirements on modules within the gocloud.dev 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  }