github.com/mckael/restic@v0.8.3/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  	IgnoreChangelogReleaseDate bool
    27  	IgnoreChangelogCurrent     bool
    28  	IgnoreDockerBuildGoVersion bool
    29  
    30  	tarFilename string
    31  	buildDir    string
    32  }{}
    33  
    34  var versionRegex = regexp.MustCompile(`^\d+\.\d+\.\d+$`)
    35  
    36  func init() {
    37  	pflag.BoolVar(&opts.IgnoreBranchName, "ignore-branch-name", false, "allow releasing from other branches as 'master'")
    38  	pflag.BoolVar(&opts.IgnoreUncommittedChanges, "ignore-uncommitted-changes", false, "allow uncommitted changes")
    39  	pflag.BoolVar(&opts.IgnoreChangelogVersion, "ignore-changelog-version", false, "ignore missing entry in CHANGELOG.md")
    40  	pflag.BoolVar(&opts.IgnoreChangelogReleaseDate, "ignore-changelog-release-date", false, "ignore missing subdir with date in changelog/")
    41  	pflag.BoolVar(&opts.IgnoreChangelogCurrent, "ignore-changelog-current", false, "ignore check if CHANGELOG.md is up to date")
    42  	pflag.BoolVar(&opts.IgnoreDockerBuildGoVersion, "ignore-docker-build-go-version", false, "ignore check if docker builder go version is up to date")
    43  	pflag.Parse()
    44  }
    45  
    46  func die(f string, args ...interface{}) {
    47  	if !strings.HasSuffix(f, "\n") {
    48  		f += "\n"
    49  	}
    50  	f = "\x1b[31m" + f + "\x1b[0m"
    51  	fmt.Fprintf(os.Stderr, f, args...)
    52  	os.Exit(1)
    53  }
    54  
    55  func msg(f string, args ...interface{}) {
    56  	if !strings.HasSuffix(f, "\n") {
    57  		f += "\n"
    58  	}
    59  	f = "\x1b[32m" + f + "\x1b[0m"
    60  	fmt.Printf(f, args...)
    61  }
    62  
    63  func run(cmd string, args ...string) {
    64  	c := exec.Command(cmd, args...)
    65  	c.Stdout = os.Stdout
    66  	c.Stderr = os.Stderr
    67  	err := c.Run()
    68  	if err != nil {
    69  		die("error running %s %s: %v", cmd, args, err)
    70  	}
    71  }
    72  
    73  func rm(file string) {
    74  	err := os.Remove(file)
    75  	if err != nil {
    76  		die("error removing %v: %v", file, err)
    77  	}
    78  }
    79  
    80  func rmdir(dir string) {
    81  	err := os.RemoveAll(dir)
    82  	if err != nil {
    83  		die("error removing %v: %v", dir, err)
    84  	}
    85  }
    86  
    87  func mkdir(dir string) {
    88  	err := os.Mkdir(dir, 0755)
    89  	if err != nil {
    90  		die("mkdir %v: %v", dir, err)
    91  	}
    92  }
    93  
    94  func getwd() string {
    95  	pwd, err := os.Getwd()
    96  	if err != nil {
    97  		die("Getwd(): %v", err)
    98  	}
    99  	return pwd
   100  }
   101  
   102  func uncommittedChanges(dirs ...string) string {
   103  	args := []string{"status", "--porcelain", "--untracked-files=no"}
   104  	if len(dirs) > 0 {
   105  		args = append(args, dirs...)
   106  	}
   107  
   108  	changes, err := exec.Command("git", args...).Output()
   109  	if err != nil {
   110  		die("unable to run command: %v", err)
   111  	}
   112  
   113  	return string(changes)
   114  }
   115  
   116  func preCheckBranchMaster() {
   117  	if opts.IgnoreBranchName {
   118  		return
   119  	}
   120  
   121  	branch, err := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD").Output()
   122  	if err != nil {
   123  		die("error running 'git': %v", err)
   124  	}
   125  
   126  	if strings.TrimSpace(string(branch)) != "master" {
   127  		die("wrong branch: %s", branch)
   128  	}
   129  }
   130  
   131  func preCheckUncommittedChanges() {
   132  	if opts.IgnoreUncommittedChanges {
   133  		return
   134  	}
   135  
   136  	changes := uncommittedChanges()
   137  	if len(changes) > 0 {
   138  		die("uncommitted changes found:\n%s\n", changes)
   139  	}
   140  }
   141  
   142  func preCheckVersionExists() {
   143  	buf, err := exec.Command("git", "tag", "-l").Output()
   144  	if err != nil {
   145  		die("error running 'git tag -l': %v", err)
   146  	}
   147  
   148  	sc := bufio.NewScanner(bytes.NewReader(buf))
   149  	for sc.Scan() {
   150  		if sc.Err() != nil {
   151  			die("error scanning version tags: %v", sc.Err())
   152  		}
   153  
   154  		if strings.TrimSpace(sc.Text()) == "v"+opts.Version {
   155  			die("tag v%v already exists", opts.Version)
   156  		}
   157  	}
   158  }
   159  
   160  func preCheckChangelogCurrent() {
   161  	if opts.IgnoreChangelogCurrent {
   162  		return
   163  	}
   164  
   165  	// regenerate changelog
   166  	run("calens", "--output", "CHANGELOG.md")
   167  
   168  	// check for uncommitted changes in changelog
   169  	if len(uncommittedChanges("CHANGELOG.md")) > 0 {
   170  		msg("committing file CHANGELOG.md")
   171  		run("git", "commit", "-m", fmt.Sprintf("Generate CHANGELOG.md for %v", opts.Version), "CHANGELOG.md")
   172  	}
   173  }
   174  
   175  func preCheckChangelogRelease() {
   176  	if opts.IgnoreChangelogReleaseDate {
   177  		return
   178  	}
   179  
   180  	d, err := os.Open("changelog")
   181  	if err != nil {
   182  		die("error opening dir: %v", err)
   183  	}
   184  
   185  	names, err := d.Readdirnames(-1)
   186  	if err != nil {
   187  		_ = d.Close()
   188  		die("error listing dir: %v", err)
   189  	}
   190  
   191  	err = d.Close()
   192  	if err != nil {
   193  		die("error closing dir: %v", err)
   194  	}
   195  
   196  	for _, name := range names {
   197  		if strings.HasPrefix(name, opts.Version+"_") {
   198  			return
   199  		}
   200  	}
   201  
   202  	die("unable to find subdir with date for version %v in changelog", opts.Version)
   203  }
   204  
   205  func preCheckChangelogVersion() {
   206  	if opts.IgnoreChangelogVersion {
   207  		return
   208  	}
   209  
   210  	f, err := os.Open("CHANGELOG.md")
   211  	if err != nil {
   212  		die("unable to open CHANGELOG.md: %v", err)
   213  	}
   214  	defer f.Close()
   215  
   216  	sc := bufio.NewScanner(f)
   217  	for sc.Scan() {
   218  		if sc.Err() != nil {
   219  			die("error scanning: %v", sc.Err())
   220  		}
   221  
   222  		if strings.Contains(strings.TrimSpace(sc.Text()), fmt.Sprintf("Changelog for restic %v", opts.Version)) {
   223  			return
   224  		}
   225  	}
   226  
   227  	die("CHANGELOG.md does not contain version %v", opts.Version)
   228  }
   229  
   230  func preCheckDockerBuilderGoVersion() {
   231  	if opts.IgnoreDockerBuildGoVersion {
   232  		return
   233  	}
   234  
   235  	buf, err := exec.Command("go", "version").Output()
   236  	if err != nil {
   237  		die("unable to check local Go version: %v", err)
   238  	}
   239  	localVersion := strings.TrimSpace(string(buf))
   240  
   241  	run("docker", "pull", "restic/builder")
   242  	buf, err = exec.Command("docker", "run", "--rm", "restic/builder", "go", "version").Output()
   243  	if err != nil {
   244  		die("unable to check Go version in docker image: %v", err)
   245  	}
   246  	containerVersion := strings.TrimSpace(string(buf))
   247  
   248  	if localVersion != containerVersion {
   249  		die("version in docker container restic/builder is different:\n  local:     %v\n  container: %v\n",
   250  			localVersion, containerVersion)
   251  	}
   252  }
   253  
   254  func generateFiles() {
   255  	msg("generate files")
   256  	run("go", "run", "build.go", "-o", "restic-generate.temp")
   257  
   258  	mandir := filepath.Join("doc", "man")
   259  	rmdir(mandir)
   260  	mkdir(mandir)
   261  	run("./restic-generate.temp", "generate",
   262  		"--man", "doc/man",
   263  		"--zsh-completion", "doc/zsh-completion.zsh",
   264  		"--bash-completion", "doc/bash-completion.sh")
   265  	rm("restic-generate.temp")
   266  
   267  	run("git", "add", "doc")
   268  	changes := uncommittedChanges("doc")
   269  	if len(changes) > 0 {
   270  		msg("committing manpages and auto-completion")
   271  		run("git", "commit", "-m", "Update manpages and auto-completion", "doc")
   272  	}
   273  }
   274  
   275  func updateVersion() {
   276  	err := ioutil.WriteFile("VERSION", []byte(opts.Version+"\n"), 0644)
   277  	if err != nil {
   278  		die("unable to write version to file: %v", err)
   279  	}
   280  
   281  	if len(uncommittedChanges("VERSION")) > 0 {
   282  		msg("committing file VERSION")
   283  		run("git", "commit", "-m", fmt.Sprintf("Add VERSION for %v", opts.Version), "VERSION")
   284  	}
   285  }
   286  
   287  func addTag() {
   288  	tagname := "v" + opts.Version
   289  	msg("add tag %v", tagname)
   290  	run("git", "tag", "-a", "-s", "-m", tagname, tagname)
   291  }
   292  
   293  func exportTar() {
   294  	cmd := fmt.Sprintf("git archive --format=tar --prefix=restic-%s/ v%s | gzip -n > %s",
   295  		opts.Version, opts.Version, opts.tarFilename)
   296  	run("sh", "-c", cmd)
   297  	msg("build restic-%s.tar.gz", opts.Version)
   298  }
   299  
   300  func runBuild() {
   301  	msg("building binaries...")
   302  	run("docker", "run", "--rm", "--volume", getwd()+":/home/build", "restic/builder", "build.sh", opts.tarFilename)
   303  }
   304  
   305  func findBuildDir() string {
   306  	nameRegex := regexp.MustCompile(`restic-` + opts.Version + `-\d{8}-\d{6}`)
   307  
   308  	f, err := os.Open(".")
   309  	if err != nil {
   310  		die("Open(.): %v", err)
   311  	}
   312  
   313  	entries, err := f.Readdirnames(-1)
   314  	if err != nil {
   315  		die("Readdirnames(): %v", err)
   316  	}
   317  
   318  	err = f.Close()
   319  	if err != nil {
   320  		die("Close(): %v", err)
   321  	}
   322  
   323  	sort.Slice(entries, func(i, j int) bool {
   324  		return entries[j] < entries[i]
   325  	})
   326  
   327  	for _, entry := range entries {
   328  		if nameRegex.MatchString(entry) {
   329  			msg("found restic build dir: %v", entry)
   330  			return entry
   331  		}
   332  	}
   333  
   334  	die("restic build dir not found")
   335  	return ""
   336  }
   337  
   338  func signFiles() {
   339  	run("gpg", "--armor", "--detach-sign", filepath.Join(opts.buildDir, "SHA256SUMS"))
   340  	run("gpg", "--armor", "--detach-sign", filepath.Join(opts.buildDir, opts.tarFilename))
   341  }
   342  
   343  func updateDocker() {
   344  	cmd := fmt.Sprintf("bzcat %s/restic_%s_linux_amd64.bz2 > restic", opts.buildDir, opts.Version)
   345  	run("sh", "-c", cmd)
   346  	run("chmod", "+x", "restic")
   347  	run("docker", "build", "--rm", "--tag", "restic/restic:latest", "-f", "docker/Dockerfile", ".")
   348  	run("docker", "tag", "restic/restic:latest", "restic/restic:"+opts.Version)
   349  }
   350  
   351  func main() {
   352  	if len(pflag.Args()) == 0 {
   353  		die("USAGE: release-version [OPTIONS] VERSION")
   354  	}
   355  
   356  	opts.Version = pflag.Args()[0]
   357  	if !versionRegex.MatchString(opts.Version) {
   358  		die("invalid new version")
   359  	}
   360  
   361  	opts.tarFilename = fmt.Sprintf("restic-%s.tar.gz", opts.Version)
   362  
   363  	preCheckBranchMaster()
   364  	preCheckUncommittedChanges()
   365  	preCheckVersionExists()
   366  	preCheckDockerBuilderGoVersion()
   367  	preCheckChangelogRelease()
   368  	preCheckChangelogCurrent()
   369  	preCheckChangelogVersion()
   370  
   371  	generateFiles()
   372  	updateVersion()
   373  	addTag()
   374  
   375  	exportTar()
   376  	runBuild()
   377  	opts.buildDir = findBuildDir()
   378  	signFiles()
   379  
   380  	updateDocker()
   381  
   382  	msg("done, build dir is %v", opts.buildDir)
   383  
   384  	msg("now run:\n\ngit push --tags origin master\ndocker push restic/restic\n")
   385  }