github.com/fawick/restic@v0.1.1-0.20171126184616-c02923fbfc79/scripts/release.go (about)

     1  // +build ignore
     2  
     3  package main
     4  
     5  import (
     6  	"bufio"
     7  	"bytes"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"regexp"
    14  	"sort"
    15  	"strings"
    16  
    17  	"github.com/spf13/pflag"
    18  )
    19  
    20  var opts = struct {
    21  	Version string
    22  
    23  	IgnoreBranchName         bool
    24  	IgnoreUncommittedChanges bool
    25  	IgnoreChangelogVersion   bool
    26  
    27  	tarFilename string
    28  	buildDir    string
    29  }{}
    30  
    31  var versionRegex = regexp.MustCompile(`^\d+\.\d+\.\d+$`)
    32  
    33  func init() {
    34  	pflag.BoolVar(&opts.IgnoreBranchName, "ignore-branch-name", false, "allow releasing from other branches as 'master'")
    35  	pflag.BoolVar(&opts.IgnoreUncommittedChanges, "ignore-uncommitted-changes", false, "allow uncommitted changes")
    36  	pflag.BoolVar(&opts.IgnoreChangelogVersion, "ignore-changelgo-version", false, "ignore missing entry in CHANGELOG.md")
    37  	pflag.Parse()
    38  }
    39  
    40  func die(f string, args ...interface{}) {
    41  	if !strings.HasSuffix(f, "\n") {
    42  		f += "\n"
    43  	}
    44  	f = "\x1b[31m" + f + "\x1b[0m"
    45  	fmt.Fprintf(os.Stderr, f, args...)
    46  	os.Exit(1)
    47  }
    48  
    49  func msg(f string, args ...interface{}) {
    50  	if !strings.HasSuffix(f, "\n") {
    51  		f += "\n"
    52  	}
    53  	f = "\x1b[32m" + f + "\x1b[0m"
    54  	fmt.Printf(f, args...)
    55  }
    56  
    57  func run(cmd string, args ...string) {
    58  	c := exec.Command(cmd, args...)
    59  	c.Stdout = os.Stdout
    60  	c.Stderr = os.Stderr
    61  	err := c.Run()
    62  	if err != nil {
    63  		die("error running %s %s: %v", cmd, args, err)
    64  	}
    65  }
    66  
    67  func rm(file string) {
    68  	err := os.Remove(file)
    69  	if err != nil {
    70  		die("error removing %v: %v", file, err)
    71  	}
    72  }
    73  
    74  func rmdir(dir string) {
    75  	err := os.RemoveAll(dir)
    76  	if err != nil {
    77  		die("error removing %v: %v", dir, err)
    78  	}
    79  }
    80  
    81  func mkdir(dir string) {
    82  	err := os.Mkdir(dir, 0755)
    83  	if err != nil {
    84  		die("mkdir %v: %v", dir, err)
    85  	}
    86  }
    87  
    88  func getwd() string {
    89  	pwd, err := os.Getwd()
    90  	if err != nil {
    91  		die("Getwd(): %v", err)
    92  	}
    93  	return pwd
    94  }
    95  
    96  func uncommittedChanges(dirs ...string) string {
    97  	args := []string{"status", "--porcelain", "--untracked-files=no"}
    98  	if len(dirs) > 0 {
    99  		args = append(args, dirs...)
   100  	}
   101  
   102  	changes, err := exec.Command("git", args...).Output()
   103  	if err != nil {
   104  		die("unable to run command: %v", err)
   105  	}
   106  
   107  	return string(changes)
   108  }
   109  
   110  func preCheckBranchMaster() {
   111  	if opts.IgnoreBranchName {
   112  		return
   113  	}
   114  
   115  	branch, err := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD").Output()
   116  	if err != nil {
   117  		die("error running 'git': %v", err)
   118  	}
   119  
   120  	if string(branch) != "master" {
   121  		die("wrong branch: %s", branch)
   122  	}
   123  }
   124  
   125  func preCheckUncommittedChanges() {
   126  	if opts.IgnoreUncommittedChanges {
   127  		return
   128  	}
   129  
   130  	changes := uncommittedChanges()
   131  	if len(changes) > 0 {
   132  		die("uncommited changes found:\n%s\n", changes)
   133  	}
   134  }
   135  
   136  func preCheckVersionExists() {
   137  	buf, err := exec.Command("git", "tag", "-l").Output()
   138  	if err != nil {
   139  		die("error running 'git tag -l': %v", err)
   140  	}
   141  
   142  	sc := bufio.NewScanner(bytes.NewReader(buf))
   143  	for sc.Scan() {
   144  		if sc.Err() != nil {
   145  			die("error scanning version tags: %v", sc.Err())
   146  		}
   147  
   148  		if strings.TrimSpace(sc.Text()) == "v"+opts.Version {
   149  			die("tag v%v already exists", opts.Version)
   150  		}
   151  	}
   152  }
   153  
   154  func preCheckChangelog() {
   155  	if opts.IgnoreChangelogVersion {
   156  		return
   157  	}
   158  
   159  	f, err := os.Open("CHANGELOG.md")
   160  	if err != nil {
   161  		die("unable to open CHANGELOG.md: %v", err)
   162  	}
   163  	defer f.Close()
   164  
   165  	sc := bufio.NewScanner(f)
   166  	for sc.Scan() {
   167  		if sc.Err() != nil {
   168  			die("error scanning: %v", sc.Err())
   169  		}
   170  
   171  		if strings.TrimSpace(sc.Text()) == fmt.Sprintf("Important Changes in %v", opts.Version) {
   172  			return
   173  		}
   174  	}
   175  
   176  	die("CHANGELOG.md does not contain version %v", opts.Version)
   177  }
   178  
   179  func generateFiles() {
   180  	msg("generate files")
   181  	run("go", "run", "build.go", "-o", "restic-generate.temp")
   182  
   183  	mandir := filepath.Join("doc", "man")
   184  	rmdir(mandir)
   185  	mkdir(mandir)
   186  	run("./restic-generate.temp", "generate",
   187  		"--man", "doc/man",
   188  		"--zsh-completion", "doc/zsh-completion.zsh",
   189  		"--bash-completion", "doc/bash-completion.sh")
   190  	rm("restic-generate.temp")
   191  
   192  	changes := uncommittedChanges("doc")
   193  	if len(changes) > 0 {
   194  		msg("comitting manpages and auto-completion")
   195  		run("git", "commit", "-m", "Update manpages and auto-completion", "doc")
   196  	}
   197  }
   198  
   199  func updateVersion() {
   200  	err := ioutil.WriteFile("VERSION", []byte(opts.Version), 0644)
   201  	if err != nil {
   202  		die("unable to write version to file: %v", err)
   203  	}
   204  
   205  	if len(uncommittedChanges("VERSION")) > 0 {
   206  		msg("comitting file VERSION")
   207  		run("git", "commit", "-m", fmt.Sprintf("Add VERSION for %v", opts.Version), "VERSION")
   208  	}
   209  }
   210  
   211  func addTag() {
   212  	tagname := "v" + opts.Version
   213  	msg("add tag %v", tagname)
   214  	run("git", "tag", "-a", "-s", "-m", tagname, tagname)
   215  }
   216  
   217  func exportTar() {
   218  	cmd := fmt.Sprintf("git archive --format=tar --prefix=restic-%s/ v%s | gzip -n > %s",
   219  		opts.Version, opts.Version, opts.tarFilename)
   220  	run("sh", "-c", cmd)
   221  	msg("build restic-%s.tar.gz", opts.Version)
   222  }
   223  
   224  func runBuild() {
   225  	msg("building binaries...")
   226  	run("docker", "pull", "restic/builder")
   227  	run("docker", "run", "--volume", getwd()+":/home/build", "restic/builder", opts.tarFilename)
   228  }
   229  
   230  func findBuildDir() string {
   231  	nameRegex := regexp.MustCompile(`restic-` + opts.Version + `-\d{8}-\d{6}`)
   232  
   233  	f, err := os.Open(".")
   234  	if err != nil {
   235  		die("Open(.): %v", err)
   236  	}
   237  
   238  	entries, err := f.Readdirnames(-1)
   239  	if err != nil {
   240  		die("Readdirnames(): %v", err)
   241  	}
   242  
   243  	err = f.Close()
   244  	if err != nil {
   245  		die("Close(): %v", err)
   246  	}
   247  
   248  	sort.Slice(entries, func(i, j int) bool {
   249  		return entries[j] < entries[i]
   250  	})
   251  
   252  	for _, entry := range entries {
   253  		if nameRegex.MatchString(entry) {
   254  			msg("found restic build dir: %v", entry)
   255  			return entry
   256  		}
   257  	}
   258  
   259  	die("restic build dir not found")
   260  	return ""
   261  }
   262  
   263  func signFiles() {
   264  	run("gpg", "--armor", "--detach-sign", filepath.Join(opts.buildDir, "SHA256SUMS"))
   265  	run("gpg", "--armor", "--detach-sign", filepath.Join(opts.buildDir, opts.tarFilename))
   266  }
   267  
   268  func updateDocker() {
   269  	cmd := fmt.Sprintf("bzcat %s/restic_%s_linux_amd64.bz2 > restic", opts.buildDir, opts.Version)
   270  	run("sh", "-c", cmd)
   271  	run("chmod", "+x", "restic")
   272  	run("docker", "build", "--rm", "--tag", "restic/restic:latest", "-f", "docker/Dockerfile", ".")
   273  }
   274  
   275  func main() {
   276  	if len(pflag.Args()) == 0 {
   277  		die("USAGE: release-version [OPTIONS] VERSION")
   278  	}
   279  
   280  	opts.Version = pflag.Args()[0]
   281  	if !versionRegex.MatchString(opts.Version) {
   282  		die("invalid new version")
   283  	}
   284  
   285  	opts.tarFilename = fmt.Sprintf("restic-%s.tar.gz", opts.Version)
   286  
   287  	preCheckBranchMaster()
   288  	preCheckUncommittedChanges()
   289  	preCheckVersionExists()
   290  	preCheckChangelog()
   291  
   292  	generateFiles()
   293  	updateVersion()
   294  	addTag()
   295  
   296  	exportTar()
   297  	runBuild()
   298  	opts.buildDir = findBuildDir()
   299  	signFiles()
   300  
   301  	updateDocker()
   302  
   303  	msg("done, build dir is %v", opts.buildDir)
   304  
   305  	msg("now run:\n\ngit push --tags origin master\ndocker push restic/restic\n")
   306  }