github.com/letsencrypt/boulder@v0.20251208.0/tools/release/tag/main.go (about) 1 /* 2 Tag Release creates a new Boulder release tag and pushes it to GitHub. It 3 ensures that the release tag points to the correct commit, has standardized 4 formatting of both the tag itself and its message, and is GPG-signed. 5 6 It always produces Semantic Versioning tags of the form v0.YYYYMMDD.N, where: 7 - the major version of 0 indicates that we are not committing to any 8 backwards-compatibility guarantees; 9 - the minor version of the current date provides a human-readable date for the 10 release, and ensures that minor versions will be monotonically increasing; 11 and 12 - the patch version is always 0 for mainline releases, and a monotonically 13 increasing number for hotfix releases. 14 15 Usage: 16 17 go run github.com/letsencrypt/boulder/tools/release/tag@main [-push] [branchname] 18 19 If the "branchname" argument is not provided, it assumes "main". If it is 20 provided, it must be either "main" or a properly-formatted release branch name. 21 22 If the -push flag is not provided, it will simply print the details of the new 23 tag and then exit. If it is provided, it will initiate a push to the remote. 24 25 In all cases, it assumes that the upstream remote is named "origin". 26 */ 27 package main 28 29 import ( 30 "errors" 31 "flag" 32 "fmt" 33 "os" 34 "os/exec" 35 "strconv" 36 "strings" 37 "time" 38 ) 39 40 type cmdError struct { 41 error 42 output string 43 } 44 45 func (e cmdError) Unwrap() error { 46 return e.error 47 } 48 49 func git(args ...string) (string, error) { 50 cmd := exec.Command("git", args...) 51 fmt.Println("Running:", cmd.String()) 52 out, err := cmd.CombinedOutput() 53 if err != nil { 54 return string(out), cmdError{ 55 error: fmt.Errorf("running %q: %w", cmd.String(), err), 56 output: string(out), 57 } 58 } 59 return string(out), nil 60 } 61 62 func show(output string) { 63 for line := range strings.SplitSeq(strings.TrimSpace(output), "\n") { 64 fmt.Println(" ", line) 65 } 66 } 67 68 func main() { 69 err := tag(os.Args[1:]) 70 if err != nil { 71 var cmdErr cmdError 72 if errors.As(err, &cmdErr) { 73 show(cmdErr.output) 74 } 75 fmt.Println(err.Error()) 76 os.Exit(1) 77 } 78 } 79 80 func tag(args []string) error { 81 fs := flag.NewFlagSet("tag", flag.ContinueOnError) 82 var push bool 83 fs.BoolVar(&push, "push", false, "If set, push the resulting release tag to GitHub.") 84 err := fs.Parse(args) 85 if err != nil { 86 return fmt.Errorf("invalid flags: %w", err) 87 } 88 89 var branch string 90 switch len(fs.Args()) { 91 case 0: 92 branch = "main" 93 case 1: 94 branch = fs.Arg(0) 95 if !strings.HasPrefix(branch, "release-branch-") { 96 return fmt.Errorf("branch must be 'main' or 'release-branch-...', got %q", branch) 97 } 98 default: 99 return fmt.Errorf("too many args: %#v", fs.Args()) 100 } 101 102 // Fetch all of the latest commits on this ref from origin, so that we can 103 // ensure we're tagging the tip of the upstream branch, and that we have all 104 // of the extant tags along this branch if its a release branch. 105 _, err = git("fetch", "origin", branch) 106 if err != nil { 107 return err 108 } 109 110 var tag string 111 switch branch { 112 case "main": 113 tag = fmt.Sprintf("v0.%s.0", time.Now().Format("20060102")) 114 default: 115 tag, err = nextTagOnBranch(branch) 116 if err != nil { 117 return fmt.Errorf("failed to compute next hotfix tag: %w", err) 118 } 119 } 120 121 // Produce the tag, using -s to PGP sign it. This will fail if a tag with 122 // that name already exists. 123 message := fmt.Sprintf("Release %s", tag) 124 _, err = git("tag", "-s", "-m", message, tag, "origin/"+branch) 125 if err != nil { 126 return err 127 } 128 129 // Show the result of the tagging operation, including the tag message and 130 // signature, and the commit hash and message, but not the diff. 131 out, err := git("show", "-s", tag) 132 if err != nil { 133 return err 134 } 135 show(out) 136 137 if push { 138 _, err = git("push", "origin", tag) 139 if err != nil { 140 return err 141 } 142 } else { 143 fmt.Println() 144 fmt.Println("Please inspect the tag above, then run:") 145 fmt.Printf(" git push origin %s\n", tag) 146 } 147 return nil 148 } 149 150 func nextTagOnBranch(branch string) (string, error) { 151 baseVersion := strings.TrimPrefix(branch, "release-branch-") 152 out, err := git("tag", "--list", "--no-column", baseVersion+".*") 153 if err != nil { 154 return "", fmt.Errorf("failed to list extant tags on branch %q: %w", branch, err) 155 } 156 157 maxPatch := 0 158 for tag := range strings.SplitSeq(strings.TrimSpace(out), "\n") { 159 parts := strings.SplitN(tag, ".", 3) 160 if len(parts) != 3 { 161 return "", fmt.Errorf("failed to parse release tag %q as semver", tag) 162 } 163 164 major := parts[0] 165 if major != "v0" { 166 return "", fmt.Errorf("expected major portion of prior release tag %q to be 'v0'", tag) 167 } 168 169 minor := parts[1] 170 t, err := time.Parse("20060102", minor) 171 if err != nil { 172 return "", fmt.Errorf("expected minor portion of prior release tag %q to be a date: %w", tag, err) 173 } 174 if t.Year() < 2015 { 175 return "", fmt.Errorf("minor portion of prior release tag %q appears to be an unrealistic date: %q", tag, t.String()) 176 } 177 178 patch := parts[2] 179 patchInt, err := strconv.Atoi(patch) 180 if err != nil { 181 return "", fmt.Errorf("patch portion of prior release tag %q is not an integer: %w", tag, err) 182 } 183 184 if patchInt > maxPatch { 185 maxPatch = patchInt 186 } 187 } 188 189 return fmt.Sprintf("%s.%d", baseVersion, maxPatch+1), nil 190 }