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 }