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 }