fix(scripts/releaser): simplify branch regex and fix changelog range (#23947)

Two fixes for the release script:

**1. Branch regex cleanup** — Simplified to only match `release/X.Y`.
Removed
support for `release/X.Y.Z` and `release/X.Y-rc.N` branch formats. RCs
are
now tagged from main (not from release branches), and the three-segment
`release/X.Y.Z` format will not be used going forward.

**2. Changelog range for first release on a new minor** — When no tags
match
the branch's major.minor, the commit range fell back to `HEAD` (entire
git
history, ~13k lines of changelog). Now computes `git merge-base` with
the
previous minor's release branch (e.g. `origin/release/2.32`) as the
changelog
starting point. This works even when that branch has no tags pushed yet.
Falls
back to the latest reachable tag from a previous minor if the branch
doesn't
exist.
This commit is contained in:
Garrett Delfosse
2026-04-07 13:07:21 -04:00
committed by GitHub
parent 21c08a37d7
commit 5453a6c6d6
+44 -35
View File
@@ -68,17 +68,17 @@ func runRelease(ctx context.Context, inv *serpent.Invocation, executor ReleaseEx
return xerrors.Errorf("detecting branch: %w", err)
}
// Match standard release branches (release/2.32) and RC
// branches (release/2.32-rc.0).
branchRe := regexp.MustCompile(`^release/(\d+)\.(\d+)(?:-rc\.(\d+))?$`)
// Match release branches (release/X.Y). RCs are tagged
// from main, not from release branches.
branchRe := regexp.MustCompile(`^release/(\d+)\.(\d+)$`)
m := branchRe.FindStringSubmatch(currentBranch)
if m == nil {
warnf(w, "Current branch %q is not a release branch (release/X.Y or release/X.Y-rc.N).", currentBranch)
warnf(w, "Current branch %q is not a release branch (release/X.Y).", currentBranch)
branchInput, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "Enter the release branch to use (e.g. release/2.21 or release/2.21-rc.0)",
Text: "Enter the release branch to use (e.g. release/2.21)",
Validate: func(s string) error {
if !branchRe.MatchString(s) {
return xerrors.New("must be in format release/X.Y or release/X.Y-rc.N (e.g. release/2.21 or release/2.21-rc.0)")
return xerrors.New("must be in format release/X.Y (e.g. release/2.21)")
}
return nil
},
@@ -91,10 +91,6 @@ func runRelease(ctx context.Context, inv *serpent.Invocation, executor ReleaseEx
}
branchMajor, _ := strconv.Atoi(m[1])
branchMinor, _ := strconv.Atoi(m[2])
branchRC := -1 // -1 means not an RC branch.
if m[3] != "" {
branchRC, _ = strconv.Atoi(m[3])
}
successf(w, "Using release branch: %s", currentBranch)
// --- Fetch & sync check ---
@@ -138,31 +134,44 @@ func runRelease(ctx context.Context, inv *serpent.Invocation, executor ReleaseEx
}
}
// changelogBaseRef is the git ref used as the starting point
// for release notes generation. When a tag already exists in
// this minor series we use it directly. For the first release
// on a new minor no matching tag exists, so we compute the
// merge-base with the previous minor's release branch instead.
// This works even when that branch has no tags yet (it was
// just cut and pushed). As a last resort we fall back to the
// latest reachable tag from a previous minor.
var changelogBaseRef string
if prevVersion != nil {
changelogBaseRef = prevVersion.String()
} else {
prevReleaseBranch := fmt.Sprintf("release/%d.%d", branchMajor, branchMinor-1)
if err := gitRun("fetch", "--quiet", "origin", prevReleaseBranch); err != nil {
warnf(w, "Could not fetch %s: %v", prevReleaseBranch, err)
}
if mb, mbErr := gitOutput("merge-base", "HEAD", "origin/"+prevReleaseBranch); mbErr == nil && mb != "" {
changelogBaseRef = mb
infof(w, "Using merge-base with %s as changelog base: %s", prevReleaseBranch, mb[:12])
} else {
// No previous release branch found; fall back to
// the latest reachable tag from a previous minor.
for _, t := range mergedTags {
if t.Major == branchMajor && t.Minor < branchMinor {
changelogBaseRef = t.String()
break
}
}
}
}
var suggested version
if prevVersion == nil {
infof(w, "No previous release tag found on this branch.")
suggested = version{Major: branchMajor, Minor: branchMinor, Patch: 0}
if branchRC >= 0 {
suggested.Pre = fmt.Sprintf("rc.%d", branchRC)
}
} else {
infof(w, "Previous release tag: %s", prevVersion.String())
if branchRC >= 0 {
// On an RC branch, suggest the next RC for
// the same base version.
nextRC := 0
if prevVersion.IsRC() {
nextRC = prevVersion.rcNumber() + 1
}
suggested = version{
Major: prevVersion.Major,
Minor: prevVersion.Minor,
Patch: prevVersion.Patch,
Pre: fmt.Sprintf("rc.%d", nextRC),
}
} else {
suggested = version{Major: prevVersion.Major, Minor: prevVersion.Minor, Patch: prevVersion.Patch + 1}
}
suggested = version{Major: prevVersion.Major, Minor: prevVersion.Minor, Patch: prevVersion.Patch + 1}
}
fmt.Fprintln(w)
@@ -366,8 +375,8 @@ func runRelease(ctx context.Context, inv *serpent.Invocation, executor ReleaseEx
infof(w, "Generating release notes...")
commitRange := "HEAD"
if prevVersion != nil {
commitRange = prevVersion.String() + "..HEAD"
if changelogBaseRef != "" {
commitRange = changelogBaseRef + "..HEAD"
}
commits, err := commitLog(commitRange)
@@ -473,16 +482,16 @@ func runRelease(ctx context.Context, inv *serpent.Invocation, executor ReleaseEx
}
if !hasContent {
prevStr := "the beginning of time"
if prevVersion != nil {
prevStr = prevVersion.String()
if changelogBaseRef != "" {
prevStr = changelogBaseRef
}
fmt.Fprintf(&notes, "\n_No changes since %s._\n", prevStr)
}
// Compare link.
if prevVersion != nil {
if changelogBaseRef != "" {
fmt.Fprintf(&notes, "\nCompare: [`%s...%s`](https://github.com/%s/%s/compare/%s...%s)\n",
prevVersion, newVersion, owner, repo, prevVersion, newVersion)
changelogBaseRef, newVersion, owner, repo, changelogBaseRef, newVersion)
}
// Container image.