Skip to content

Commit 4d2e4de

Browse files
authored
feat: release init can read per-library version override from config.yaml (#2083)
Fixes #2041
1 parent 867928b commit 4d2e4de

File tree

6 files changed

+254
-37
lines changed

6 files changed

+254
-37
lines changed

internal/librarian/commit_version_analyzer.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,7 @@ func formatTag(library *config.LibraryState, versionOverride string) string {
115115
}
116116

117117
// NextVersion calculates the next semantic version based on a slice of conventional commits.
118-
// If overrideNextVersion is not empty, it is returned as the next version.
119-
func NextVersion(commits []*conventionalcommits.ConventionalCommit, currentVersion, overrideNextVersion string) (string, error) {
120-
if overrideNextVersion != "" {
121-
return overrideNextVersion, nil
122-
}
118+
func NextVersion(commits []*conventionalcommits.ConventionalCommit, currentVersion string) (string, error) {
123119
highestChange := getHighestChange(commits)
124120
return semver.DeriveNext(highestChange, currentVersion)
125121
}

internal/librarian/commit_version_analyzer_test.go

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -330,65 +330,52 @@ func TestGetHighestChange(t *testing.T) {
330330
func TestNextVersion(t *testing.T) {
331331
t.Parallel()
332332
for _, test := range []struct {
333-
name string
334-
commits []*conventionalcommits.ConventionalCommit
335-
currentVersion string
336-
overrideNextVersion string
337-
wantVersion string
338-
wantErr bool
333+
name string
334+
commits []*conventionalcommits.ConventionalCommit
335+
currentVersion string
336+
wantVersion string
337+
wantErr bool
339338
}{
340-
{
341-
name: "with override version",
342-
commits: []*conventionalcommits.ConventionalCommit{},
343-
currentVersion: "1.0.0",
344-
overrideNextVersion: "2.0.0",
345-
wantVersion: "2.0.0",
346-
wantErr: false,
347-
},
348339
{
349340
name: "without override version",
350341
commits: []*conventionalcommits.ConventionalCommit{
351342
{Type: "feat"},
352343
},
353-
currentVersion: "1.0.0",
354-
overrideNextVersion: "",
355-
wantVersion: "1.1.0",
356-
wantErr: false,
344+
currentVersion: "1.0.0",
345+
wantVersion: "1.1.0",
346+
wantErr: false,
357347
},
358348
{
359349
name: "derive next returns error",
360350
commits: []*conventionalcommits.ConventionalCommit{
361351
{Type: "feat"},
362352
},
363-
currentVersion: "invalid-version",
364-
overrideNextVersion: "",
365-
wantVersion: "",
366-
wantErr: true,
353+
currentVersion: "invalid-version",
354+
wantVersion: "",
355+
wantErr: true,
367356
},
368357
{
369358
name: "breaking change on nested commit results in minor bump",
370359
commits: []*conventionalcommits.ConventionalCommit{
371360
{Type: "feat", IsBreaking: true, IsNested: true},
372361
},
373-
currentVersion: "1.2.3",
374-
overrideNextVersion: "",
375-
wantVersion: "1.3.0",
376-
wantErr: false,
362+
currentVersion: "1.2.3",
363+
wantVersion: "1.3.0",
364+
wantErr: false,
377365
},
378366
{
379367
name: "major change before nested commit results in major bump",
380368
commits: []*conventionalcommits.ConventionalCommit{
381369
{Type: "feat", IsBreaking: true},
382370
{Type: "fix", IsNested: true},
383371
},
384-
currentVersion: "1.2.3",
385-
overrideNextVersion: "",
386-
wantVersion: "2.0.0",
387-
wantErr: false,
372+
currentVersion: "1.2.3",
373+
wantVersion: "2.0.0",
374+
wantErr: false,
388375
},
389376
} {
390377
t.Run(test.name, func(t *testing.T) {
391-
gotVersion, err := NextVersion(test.commits, test.currentVersion, test.overrideNextVersion)
378+
gotVersion, err := NextVersion(test.commits, test.currentVersion)
392379
if (err != nil) != test.wantErr {
393380
t.Errorf("NextVersion() error = %v, wantErr %v", err, test.wantErr)
394381
return

internal/librarian/release_init.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import (
2121
"os"
2222
"path/filepath"
2323

24+
"github.com/googleapis/librarian/internal/conventionalcommits"
2425
"github.com/googleapis/librarian/internal/docker"
26+
"github.com/googleapis/librarian/internal/semver"
2527

2628
"github.com/googleapis/librarian/internal/cli"
2729
"github.com/googleapis/librarian/internal/config"
@@ -255,7 +257,7 @@ func (r *initRunner) updateLibrary(library *config.LibraryState) error {
255257
return nil
256258
}
257259

258-
nextVersion, err := NextVersion(commits, library.Version, r.cfg.LibraryVersion)
260+
nextVersion, err := r.determineNextVersion(commits, library.Version, library.ID)
259261
if err != nil {
260262
return err
261263
}
@@ -266,6 +268,39 @@ func (r *initRunner) updateLibrary(library *config.LibraryState) error {
266268
return nil
267269
}
268270

271+
func (r *initRunner) determineNextVersion(commits []*conventionalcommits.ConventionalCommit, currentVersion string, libraryID string) (string, error) {
272+
// If library version explicitly passed to CLI, use it
273+
if r.cfg.LibraryVersion != "" {
274+
slog.Info("Library version override specified", "currentVersion", currentVersion, "version", r.cfg.LibraryVersion)
275+
newVersion := semver.MaxVersion(currentVersion, r.cfg.LibraryVersion)
276+
if newVersion == r.cfg.LibraryVersion {
277+
return newVersion, nil
278+
} else {
279+
slog.Warn("Specified version is not higher than the current version, ignoring override.")
280+
}
281+
}
282+
283+
nextVersionFromCommits, err := NextVersion(commits, currentVersion)
284+
if err != nil {
285+
return "", err
286+
}
287+
288+
if r.librarianConfig == nil {
289+
slog.Info("No librarian config")
290+
return nextVersionFromCommits, nil
291+
}
292+
293+
// Look for next_version override from config.yaml
294+
libraryConfig := r.librarianConfig.LibraryConfigFor(libraryID)
295+
slog.Info("Looking up library config", "library", libraryID, slog.Any("config", libraryConfig))
296+
if libraryConfig == nil || libraryConfig.NextVersion == "" {
297+
return nextVersionFromCommits, nil
298+
}
299+
300+
// Compare versions and pick latest
301+
return semver.MaxVersion(nextVersionFromCommits, libraryConfig.NextVersion), nil
302+
}
303+
269304
// copyGlobalAllowlist copies files in the global file allowlist excluding
270305
//
271306
// read-only files and copies global files from src.

internal/librarian/release_init_test.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,3 +1038,142 @@ func TestCopyGlobalAllowlist(t *testing.T) {
10381038
})
10391039
}
10401040
}
1041+
1042+
func TestDetermineNextVersion(t *testing.T) {
1043+
t.Parallel()
1044+
for _, test := range []struct {
1045+
name string
1046+
commits []*conventionalcommits.ConventionalCommit
1047+
currentVersion string
1048+
libraryID string
1049+
config *config.Config
1050+
librarianConfig *config.LibrarianConfig
1051+
wantVersion string
1052+
wantErr bool
1053+
wantErrMsg string
1054+
}{
1055+
{
1056+
name: "from commits",
1057+
commits: []*conventionalcommits.ConventionalCommit{
1058+
{Type: "feat"},
1059+
},
1060+
config: &config.Config{
1061+
Library: "some-library",
1062+
},
1063+
libraryID: "some-library",
1064+
librarianConfig: &config.LibrarianConfig{
1065+
Libraries: []*config.LibraryConfig{},
1066+
},
1067+
currentVersion: "1.0.0",
1068+
wantVersion: "1.1.0",
1069+
wantErr: false,
1070+
},
1071+
{
1072+
name: "with CLI override version",
1073+
commits: []*conventionalcommits.ConventionalCommit{
1074+
{Type: "feat"},
1075+
},
1076+
config: &config.Config{
1077+
Library: "some-library",
1078+
LibraryVersion: "1.2.3",
1079+
},
1080+
libraryID: "some-library",
1081+
librarianConfig: &config.LibrarianConfig{
1082+
Libraries: []*config.LibraryConfig{
1083+
&config.LibraryConfig{
1084+
LibraryID: "some-library",
1085+
NextVersion: "2.3.4",
1086+
},
1087+
},
1088+
},
1089+
currentVersion: "1.0.0",
1090+
wantVersion: "1.2.3",
1091+
wantErr: false,
1092+
},
1093+
{
1094+
name: "with CLI override version cannot revert version",
1095+
commits: []*conventionalcommits.ConventionalCommit{
1096+
{Type: "feat"},
1097+
},
1098+
config: &config.Config{
1099+
Library: "some-library",
1100+
LibraryVersion: "1.2.3",
1101+
},
1102+
libraryID: "some-library",
1103+
librarianConfig: &config.LibrarianConfig{
1104+
Libraries: []*config.LibraryConfig{
1105+
&config.LibraryConfig{
1106+
LibraryID: "some-library",
1107+
},
1108+
},
1109+
},
1110+
currentVersion: "2.4.0",
1111+
wantVersion: "2.5.0",
1112+
wantErr: false,
1113+
},
1114+
{
1115+
name: "with config.yaml override version",
1116+
commits: []*conventionalcommits.ConventionalCommit{
1117+
{Type: "feat"},
1118+
},
1119+
config: &config.Config{
1120+
Library: "some-library",
1121+
},
1122+
libraryID: "some-library",
1123+
librarianConfig: &config.LibrarianConfig{
1124+
Libraries: []*config.LibraryConfig{
1125+
&config.LibraryConfig{
1126+
LibraryID: "some-library",
1127+
NextVersion: "2.3.4",
1128+
},
1129+
},
1130+
},
1131+
currentVersion: "1.0.0",
1132+
wantVersion: "2.3.4",
1133+
wantErr: false,
1134+
},
1135+
{
1136+
name: "with outdated config.yaml override version",
1137+
commits: []*conventionalcommits.ConventionalCommit{
1138+
{Type: "feat"},
1139+
},
1140+
config: &config.Config{
1141+
Library: "some-library",
1142+
},
1143+
libraryID: "some-library",
1144+
librarianConfig: &config.LibrarianConfig{
1145+
Libraries: []*config.LibraryConfig{
1146+
&config.LibraryConfig{
1147+
LibraryID: "some-library",
1148+
NextVersion: "2.3.4",
1149+
},
1150+
},
1151+
},
1152+
currentVersion: "2.4.0",
1153+
wantVersion: "2.5.0",
1154+
wantErr: false,
1155+
},
1156+
} {
1157+
t.Run(test.name, func(t *testing.T) {
1158+
runner := &initRunner{
1159+
cfg: test.config,
1160+
librarianConfig: test.librarianConfig,
1161+
}
1162+
got, err := runner.determineNextVersion(test.commits, test.currentVersion, test.libraryID)
1163+
if test.wantErr {
1164+
if err == nil {
1165+
t.Error("determineNextVersion() should return error")
1166+
}
1167+
1168+
if !strings.Contains(err.Error(), test.wantErrMsg) {
1169+
t.Errorf("want error message: %q, got %q", test.wantErrMsg, err.Error())
1170+
}
1171+
1172+
return
1173+
}
1174+
if diff := cmp.Diff(test.wantVersion, got); diff != "" {
1175+
t.Errorf("state mismatch (-want +got):\n%s", diff)
1176+
}
1177+
})
1178+
}
1179+
}

internal/semver/semver.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package semver
1616

1717
import (
1818
"fmt"
19+
"log/slog"
1920
"regexp"
2021
"strconv"
2122
"strings"
@@ -162,6 +163,29 @@ func (v *Version) incrementPrerelease() error {
162163
return nil
163164
}
164165

166+
// MaxVersion returns the largest semantic version string among the provided version strings.
167+
func MaxVersion(versionStrings ...string) string {
168+
if len(versionStrings) == 0 {
169+
return ""
170+
}
171+
versions := make([]*Version, 0)
172+
for _, versionString := range versionStrings {
173+
v, err := Parse(versionString)
174+
if err != nil {
175+
slog.Warn("Invalid version string", "version", v)
176+
continue
177+
}
178+
versions = append(versions, v)
179+
}
180+
largest := versions[0]
181+
for i := 1; i < len(versions); i++ {
182+
if largest.Compare(versions[i]) < 0 {
183+
largest = versions[i]
184+
}
185+
}
186+
return largest.String()
187+
}
188+
165189
// ChangeLevel represents the level of change, corresponding to semantic versioning.
166190
type ChangeLevel int
167191

internal/semver/semver_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,3 +386,39 @@ func TestCompare(t *testing.T) {
386386
})
387387
}
388388
}
389+
390+
func TestMaxVersion(t *testing.T) {
391+
for _, test := range []struct {
392+
name string
393+
versions []string
394+
want string
395+
}{
396+
{
397+
name: "empty",
398+
versions: []string{},
399+
want: "",
400+
},
401+
{
402+
name: "single",
403+
versions: []string{"1.2.3"},
404+
want: "1.2.3",
405+
},
406+
{
407+
name: "multiple",
408+
versions: []string{"1.2.3", "1.2.4", "1.2.2"},
409+
want: "1.2.4",
410+
},
411+
{
412+
name: "multiple with pre-release",
413+
versions: []string{"1.2.4", "1.2.4-alpha", "1.2.4-beta"},
414+
want: "1.2.4",
415+
},
416+
} {
417+
t.Run(test.name, func(t *testing.T) {
418+
got := MaxVersion(test.versions...)
419+
if diff := cmp.Diff(test.want, got); diff != "" {
420+
t.Errorf("TestMaxVersion() returned diff (-want +got):\n%s", diff)
421+
}
422+
})
423+
}
424+
}

0 commit comments

Comments
 (0)