Skip to content

Commit cfd5c12

Browse files
authored
Add hotfix tag functionality to release tool (#8290)
If given the name of a pre-existing release branch (created with //tools/release/branch), it will: - fetch all commits on that branch; - compute the next release tag by incrementing the highest extant patch version; and - create (and optionally push) the new tag. Most of the logic is contained in the new `nextTagOnBranch` function. The rest of the diff is slight rearrangement to accommodate that new function elegantly, and minor cleanups of the branch tool that I found in the process of writing this change.
1 parent 7e04a71 commit cfd5c12

File tree

2 files changed

+71
-28
lines changed

2 files changed

+71
-28
lines changed

tools/release/branch/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func branch(args []string) error {
9090
// components.
9191
parts := strings.SplitN(tag, ".", 3)
9292
if len(parts) != 3 {
93-
return fmt.Errorf("failed to parse patch version from release tag %q", tag)
93+
return fmt.Errorf("failed to parse release tag %q as semver", tag)
9494
}
9595

9696
major := parts[0]
@@ -101,7 +101,7 @@ func branch(args []string) error {
101101
minor := parts[1]
102102
t, err := time.Parse("20060102", minor)
103103
if err != nil {
104-
return fmt.Errorf("expected minor portion of release tag to be a ")
104+
return fmt.Errorf("expected minor portion of release tag to be a date: %w", err)
105105
}
106106
if t.Year() < 2015 {
107107
return fmt.Errorf("minor portion of release tag appears to be an unrealistic date: %q", t.String())

tools/release/tag/main.go

Lines changed: 69 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"fmt"
3333
"os"
3434
"os/exec"
35+
"strconv"
3536
"strings"
3637
"time"
3738
)
@@ -85,63 +86,105 @@ func tag(args []string) error {
8586
return fmt.Errorf("invalid flags: %w", err)
8687
}
8788

88-
if len(fs.Args()) > 1 {
89-
return fmt.Errorf("too many args: %#v", fs.Args())
90-
}
91-
92-
branch := "main"
93-
if len(fs.Args()) == 1 {
89+
var branch string
90+
switch len(fs.Args()) {
91+
case 0:
92+
branch = "main"
93+
case 1:
9494
branch = fs.Arg(0)
95-
}
96-
97-
switch {
98-
case branch == "main":
99-
break
100-
case strings.HasPrefix(branch, "release-branch-"):
101-
return fmt.Errorf("sorry, tagging hotfix release branches is not yet supported")
95+
if !strings.HasPrefix(branch, "release-branch-") {
96+
return fmt.Errorf("branch must be 'main' or 'release-branch-...', got %q", branch)
97+
}
10298
default:
103-
return fmt.Errorf("branch must be 'main' or 'release-branch-...', got %q", branch)
99+
return fmt.Errorf("too many args: %#v", fs.Args())
104100
}
105101

106102
// Fetch all of the latest commits on this ref from origin, so that we can
107-
// ensure we're tagging the tip of the upstream branch.
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.
108105
_, err = git("fetch", "origin", branch)
109106
if err != nil {
110107
return err
111108
}
112109

113-
// We use semver's vMajor.Minor.Patch format, where the Major version is
114-
// always 0 (no backwards compatibility guarantees), the Minor version is
115-
// the date of the release, and the Patch number is zero for normal releases
116-
// and only non-zero for hotfix releases.
117-
minor := time.Now().Format("20060102")
118-
version := fmt.Sprintf("v0.%s.0", minor)
119-
message := fmt.Sprintf("Release %s", version)
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+
}
120120

121121
// Produce the tag, using -s to PGP sign it. This will fail if a tag with
122122
// that name already exists.
123-
_, err = git("tag", "-s", "-m", message, version, "origin/"+branch)
123+
message := fmt.Sprintf("Release %s", tag)
124+
_, err = git("tag", "-s", "-m", message, tag, "origin/"+branch)
124125
if err != nil {
125126
return err
126127
}
127128

128129
// Show the result of the tagging operation, including the tag message and
129130
// signature, and the commit hash and message, but not the diff.
130-
out, err := git("show", "-s", version)
131+
out, err := git("show", "-s", tag)
131132
if err != nil {
132133
return err
133134
}
134135
show(out)
135136

136137
if push {
137-
_, err = git("push", "origin", version)
138+
_, err = git("push", "origin", tag)
138139
if err != nil {
139140
return err
140141
}
141142
} else {
142143
fmt.Println()
143144
fmt.Println("Please inspect the tag above, then run:")
144-
fmt.Printf(" git push origin %s\n", version)
145+
fmt.Printf(" git push origin %s\n", tag)
145146
}
146147
return nil
147148
}
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+
}

0 commit comments

Comments
 (0)