github.com/letsencrypt/boulder@v0.20251208.0/tools/release/branch/main.go (about) 1 /* 2 Branch Release creates a new Boulder hotfix release branch and pushes it to 3 GitHub. It ensures that the release branch has a standard name, and starts at 4 a previously-tagged mainline release. 5 6 The expectation is that this branch will then be the target of one or more PRs 7 copying (cherry-picking) commits from main to the release branch, and then a 8 hotfix release will be tagged on the branch using the related Tag Release tool. 9 10 Usage: 11 12 go run github.com/letsencrypt/boulder/tools/release/branch@main [-push] tagname 13 14 The provided tagname must be a pre-existing release tag which is reachable from 15 the "main" branch. 16 17 If the -push flag is not provided, it will simply print the details of the new 18 branch and then exit. If it is provided, it will initiate a push to the remote. 19 20 In all cases, it assumes that the upstream remote is named "origin". 21 */ 22 package main 23 24 import ( 25 "errors" 26 "flag" 27 "fmt" 28 "os" 29 "os/exec" 30 "strings" 31 "time" 32 ) 33 34 type cmdError struct { 35 error 36 output string 37 } 38 39 func (e cmdError) Unwrap() error { 40 return e.error 41 } 42 43 func git(args ...string) (string, error) { 44 cmd := exec.Command("git", args...) 45 fmt.Println("Running:", cmd.String()) 46 out, err := cmd.CombinedOutput() 47 if err != nil { 48 return string(out), cmdError{ 49 error: fmt.Errorf("running %q: %w", cmd.String(), err), 50 output: string(out), 51 } 52 } 53 return string(out), nil 54 } 55 56 func show(output string) { 57 for line := range strings.SplitSeq(strings.TrimSpace(output), "\n") { 58 fmt.Println(" ", line) 59 } 60 } 61 62 func main() { 63 err := branch(os.Args[1:]) 64 if err != nil { 65 var cmdErr cmdError 66 if errors.As(err, &cmdErr) { 67 show(cmdErr.output) 68 } 69 fmt.Println(err.Error()) 70 os.Exit(1) 71 } 72 } 73 74 func branch(args []string) error { 75 fs := flag.NewFlagSet("branch", flag.ContinueOnError) 76 var push bool 77 fs.BoolVar(&push, "push", false, "If set, push the resulting hotfix release branch to GitHub.") 78 err := fs.Parse(args) 79 if err != nil { 80 return fmt.Errorf("invalid flags: %w", err) 81 } 82 83 if len(fs.Args()) != 1 { 84 return fmt.Errorf("must supply exactly one argument, got %d: %#v", len(fs.Args()), fs.Args()) 85 } 86 87 tag := fs.Arg(0) 88 89 // Confirm the reasonableness of the given tag name by inspecting each of its 90 // components. 91 parts := strings.SplitN(tag, ".", 3) 92 if len(parts) != 3 { 93 return fmt.Errorf("failed to parse release tag %q as semver", tag) 94 } 95 96 major := parts[0] 97 if major != "v0" { 98 return fmt.Errorf("expected major portion of release tag to be 'v0', got %q", major) 99 } 100 101 minor := parts[1] 102 t, err := time.Parse("20060102", minor) 103 if err != nil { 104 return fmt.Errorf("expected minor portion of release tag to be a date: %w", err) 105 } 106 if t.Year() < 2015 { 107 return fmt.Errorf("minor portion of release tag appears to be an unrealistic date: %q", t.String()) 108 } 109 110 patch := parts[2] 111 if patch != "0" { 112 return fmt.Errorf("expected patch portion of release tag to be '0', got %q", patch) 113 } 114 115 // Fetch all of the latest refs from origin, so that we can get the most 116 // complete view of this tag and its relationship to main. 117 _, err = git("fetch", "origin") 118 if err != nil { 119 return err 120 } 121 122 _, err = git("merge-base", "--is-ancestor", tag, "origin/main") 123 if err != nil { 124 return fmt.Errorf("tag %q is not reachable from origin/main, may not have been created properly: %w", tag, err) 125 } 126 127 // Create the branch. We could skip this and instead push the tag directly 128 // to the desired ref name on the remote, but that wouldn't give the operator 129 // a chance to inspect it locally. 130 branch := fmt.Sprintf("release-branch-%s.%s", major, minor) 131 _, err = git("branch", branch, tag) 132 if err != nil { 133 return err 134 } 135 136 // Show the HEAD of the new branch, not including its diff. 137 out, err := git("show", "-s", branch) 138 if err != nil { 139 return err 140 } 141 show(out) 142 143 refspec := fmt.Sprintf("%s:%s", branch, branch) 144 145 if push { 146 _, err = git("push", "origin", refspec) 147 if err != nil { 148 return err 149 } 150 } else { 151 fmt.Println() 152 fmt.Println("Please inspect the branch above, then run:") 153 fmt.Printf(" git push origin %s\n", refspec) 154 } 155 return nil 156 }