Skip to content

Commit bcb914a

Browse files
authored
fix(librarian): shrink release PR size when there are bulk changes (#2585)
If there are bulk library changes that affect 10+ libraries we now will put them in thier own dedicated section at the bottom of the release notes. This means if there is a feat that affects 200 libraries we now will only have one entry for it in the PR body. Fixes: #2543
1 parent c55f3ce commit bcb914a

File tree

8 files changed

+231
-17
lines changed

8 files changed

+231
-17
lines changed

internal/config/state.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,15 @@ type Commit struct {
154154
CommitHash string `json:"commit_hash,omitempty"`
155155
// PiperCLNumber is the Piper CL number associated with the commit.
156156
PiperCLNumber string `json:"piper_cl_number,omitempty"`
157+
158+
// A list of library IDs associated with the commit.
159+
LibraryIDs string `json:"-"`
160+
}
161+
162+
// IsBulkCommit returns true if the commit is associated with 10 or more
163+
// libraries.
164+
func (c *Commit) IsBulkCommit() bool {
165+
return len(strings.Split(c.LibraryIDs, ",")) >= 10
157166
}
158167

159168
var (

internal/config/state_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,3 +463,39 @@ func TestLibraryState_LibraryByID(t *testing.T) {
463463
})
464464
}
465465
}
466+
467+
func TestCommit_IsBulkCommit(t *testing.T) {
468+
for _, test := range []struct {
469+
name string
470+
libraryIDs string
471+
want bool
472+
}{
473+
{
474+
name: "less than 10",
475+
libraryIDs: "a,b,c",
476+
want: false,
477+
},
478+
{
479+
name: "exactly 10",
480+
libraryIDs: "a,b,c,d,e,f,g,h,i,j",
481+
want: true,
482+
},
483+
{
484+
name: "more than 10",
485+
libraryIDs: "a,b,c,d,e,f,g,h,i,j,k",
486+
want: true,
487+
},
488+
{
489+
name: "empty",
490+
libraryIDs: "",
491+
want: false,
492+
},
493+
} {
494+
t.Run(test.name, func(t *testing.T) {
495+
c := &Commit{LibraryIDs: test.libraryIDs}
496+
if got := c.IsBulkCommit(); got != test.want {
497+
t.Errorf("Commit.IsBulkCommit() = %v, want %v", got, test.want)
498+
}
499+
})
500+
}
501+
}

internal/librarian/release_init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,8 +316,8 @@ func toCommit(c []*gitrepo.ConventionalCommit) []*config.Commit {
316316
Body: cc.Body,
317317
CommitHash: cc.CommitHash,
318318
PiperCLNumber: cc.Footers["PiperOrigin-RevId"],
319+
LibraryIDs: cc.Footers["Library-IDs"],
319320
})
320321
}
321322
return commits
322-
323323
}

internal/librarian/release_init_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,6 +1362,35 @@ func TestUpdateLibrary(t *testing.T) {
13621362
wantErr: true,
13631363
wantErrMsg: "inputted version is not SemVer greater than the current version. Set a version SemVer greater than current than",
13641364
},
1365+
{
1366+
name: "update a library with library ids in footer",
1367+
libraryState: &config.LibraryState{
1368+
ID: "one-id",
1369+
Version: "1.2.3",
1370+
},
1371+
commits: []*gitrepo.ConventionalCommit{
1372+
{
1373+
Type: "feat",
1374+
Subject: "add a config file",
1375+
Body: "This is the body.",
1376+
Footers: map[string]string{"Library-IDs": "a,b,c"},
1377+
},
1378+
},
1379+
want: &config.LibraryState{
1380+
ID: "one-id",
1381+
Version: "1.3.0",
1382+
PreviousVersion: "1.2.3",
1383+
Changes: []*config.Commit{
1384+
{
1385+
Type: "feat",
1386+
Subject: "add a config file",
1387+
Body: "This is the body.",
1388+
LibraryIDs: "a,b,c",
1389+
},
1390+
},
1391+
ReleaseTriggered: true,
1392+
},
1393+
},
13651394
{
13661395
name: "library has breaking changes",
13671396
libraryState: &config.LibraryState{

internal/librarian/release_notes.go

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"errors"
2020
"fmt"
2121
"html/template"
22+
"sort"
2223
"strings"
2324
"time"
2425

@@ -66,25 +67,40 @@ var (
6667
"shortSHA": shortSHA,
6768
}).Parse(`Librarian Version: {{.LibrarianVersion}}
6869
Language Image: {{.ImageVersion}}
69-
70+
{{ $prInfo := . }}
7071
{{- range .NoteSections -}}
71-
{{ $noteSection := . }}
7272
<details><summary>{{.LibraryID}}: {{.NewVersion}}</summary>
7373
74-
## [{{.NewVersion}}]({{"https://github.com/"}}{{.RepoOwner}}/{{.RepoName}}/compare/{{.PreviousTag}}...{{.NewTag}}) ({{.Date}})
74+
## [{{.NewVersion}}]({{"https://github.com/"}}{{$prInfo.RepoOwner}}/{{$prInfo.RepoName}}/compare/{{.PreviousTag}}...{{.NewTag}}) ({{$prInfo.Date}})
7575
{{ range .CommitSections }}
7676
### {{.Heading}}
7777
{{ range .Commits }}
78+
{{ if not .IsBulkCommit -}}
7879
{{ if .PiperCLNumber -}}
79-
* {{.Subject}} (PiperOrigin-RevId: {{.PiperCLNumber}}) ([{{shortSHA .CommitHash}}]({{"https://github.com/"}}{{$noteSection.RepoOwner}}/{{$noteSection.RepoName}}/commit/{{shortSHA .CommitHash}}))
80+
* {{.Subject}} (PiperOrigin-RevId: {{.PiperCLNumber}}) ([{{shortSHA .CommitHash}}]({{"https://github.com/"}}{{$prInfo.RepoOwner}}/{{$prInfo.RepoName}}/commit/{{shortSHA .CommitHash}}))
8081
{{- else -}}
81-
* {{.Subject}} ([{{shortSHA .CommitHash}}]({{"https://github.com/"}}{{$noteSection.RepoOwner}}/{{$noteSection.RepoName}}/commit/{{shortSHA .CommitHash}}))
82+
* {{.Subject}} ([{{shortSHA .CommitHash}}]({{"https://github.com/"}}{{$prInfo.RepoOwner}}/{{$prInfo.RepoName}}/commit/{{shortSHA .CommitHash}}))
83+
{{- end }}
8284
{{- end }}
8385
{{ end }}
8486
8587
{{- end }}
8688
</details>
8789
90+
91+
{{ end }}
92+
{{- if .BulkChanges -}}
93+
<details><summary>Bulk Changes</summary>
94+
{{ range .BulkChanges }}
95+
{{ if .PiperCLNumber -}}
96+
* {{.Type}}: {{.Subject}} (PiperOrigin-RevId: {{.PiperCLNumber}}) ([{{shortSHA .CommitHash}}]({{"https://github.com/"}}{{$prInfo.RepoOwner}}/{{$prInfo.RepoName}}/commit/{{shortSHA .CommitHash}}))
97+
Libraries: {{.LibraryIDs}}
98+
{{- else -}}
99+
* {{.Type}}: {{.Subject}} ([{{shortSHA .CommitHash}}]({{"https://github.com/"}}{{$prInfo.RepoOwner}}/{{$prInfo.RepoName}}/commit/{{shortSHA .CommitHash}}))
100+
Libraries: {{.LibraryIDs}}
101+
{{- end }}
102+
{{- end }}
103+
</details>
88104
{{ end }}
89105
`))
90106

@@ -130,20 +146,21 @@ Language Image: {{.ImageVersion}}
130146
`))
131147
)
132148

133-
type releaseNote struct {
149+
type releasePRBody struct {
134150
LibrarianVersion string
135151
ImageVersion string
152+
RepoOwner string
153+
RepoName string
154+
Date string
136155
NoteSections []*releaseNoteSection
156+
BulkChanges []*config.Commit
137157
}
138158

139159
type releaseNoteSection struct {
140-
RepoOwner string
141-
RepoName string
142160
LibraryID string
143161
PreviousTag string
144162
NewTag string
145163
NewVersion string
146-
Date string
147164
CommitSections []*commitSection
148165
}
149166

@@ -156,19 +173,39 @@ type commitSection struct {
156173
func formatReleaseNotes(state *config.LibrarianState, ghRepo *github.Repository) (string, error) {
157174
librarianVersion := cli.Version()
158175
var releaseSections []*releaseNoteSection
176+
// create a map to deduplicate bulk changes based on their commit hash
177+
// and subject
178+
bulkChangesMap := make(map[string]*config.Commit)
159179
for _, library := range state.Libraries {
160180
if !library.ReleaseTriggered {
161181
continue
162182
}
163183

164-
section := formatLibraryReleaseNotes(library, ghRepo)
184+
for _, commit := range library.Changes {
185+
if commit.IsBulkCommit() {
186+
bulkChangesMap[commit.CommitHash+commit.Subject] = commit
187+
}
188+
}
189+
190+
section := formatLibraryReleaseNotes(library)
165191
releaseSections = append(releaseSections, section)
166192
}
193+
var bulkChanges []*config.Commit
194+
for _, commit := range bulkChangesMap {
195+
bulkChanges = append(bulkChanges, commit)
196+
}
197+
sort.Slice(bulkChanges, func(i, j int) bool {
198+
return bulkChanges[i].CommitHash < bulkChanges[j].CommitHash
199+
})
167200

168-
data := &releaseNote{
201+
data := &releasePRBody{
169202
LibrarianVersion: librarianVersion,
203+
Date: time.Now().Format("2006-01-02"),
204+
RepoOwner: ghRepo.Owner,
205+
RepoName: ghRepo.Name,
170206
ImageVersion: state.Image,
171207
NoteSections: releaseSections,
208+
BulkChanges: bulkChanges,
172209
}
173210

174211
var out bytes.Buffer
@@ -181,7 +218,7 @@ func formatReleaseNotes(state *config.LibrarianState, ghRepo *github.Repository)
181218

182219
// formatLibraryReleaseNotes generates release notes in Markdown format for a single library.
183220
// It returns the generated release notes and the new version string.
184-
func formatLibraryReleaseNotes(library *config.LibraryState, ghRepo *github.Repository) *releaseNoteSection {
221+
func formatLibraryReleaseNotes(library *config.LibraryState) *releaseNoteSection {
185222
// The version should already be updated to the next version.
186223
newVersion := library.Version
187224
tagFormat := config.DetermineTagFormat(library.ID, library, nil)
@@ -190,7 +227,9 @@ func formatLibraryReleaseNotes(library *config.LibraryState, ghRepo *github.Repo
190227

191228
commitsByType := make(map[string][]*config.Commit)
192229
for _, commit := range library.Changes {
193-
commitsByType[commit.Type] = append(commitsByType[commit.Type], commit)
230+
if !commit.IsBulkCommit() {
231+
commitsByType[commit.Type] = append(commitsByType[commit.Type], commit)
232+
}
194233
}
195234

196235
var sections []*commitSection
@@ -207,13 +246,10 @@ func formatLibraryReleaseNotes(library *config.LibraryState, ghRepo *github.Repo
207246
}
208247

209248
section := &releaseNoteSection{
210-
RepoOwner: ghRepo.Owner,
211-
RepoName: ghRepo.Name,
212249
LibraryID: library.ID,
213250
NewVersion: newVersion,
214251
PreviousTag: previousTag,
215252
NewTag: newTag,
216-
Date: time.Now().Format("2006-01-02"),
217253
CommitSections: sections,
218254
}
219255

internal/librarian/release_notes_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func TestFormatReleaseNotes(t *testing.T) {
3535
today := time.Now().Format("2006-01-02")
3636
hash1 := plumbing.NewHash("1234567890abcdef")
3737
hash2 := plumbing.NewHash("fedcba0987654321")
38+
hash3 := plumbing.NewHash("abcdefg123456789")
3839
librarianVersion := cli.Version()
3940

4041
for _, test := range []struct {
@@ -358,6 +359,83 @@ Language Image: go:1.21
358359
</details>`,
359360
librarianVersion, today),
360361
},
362+
{
363+
name: "release with bulk commits",
364+
state: &config.LibrarianState{
365+
Image: "go:1.21",
366+
Libraries: []*config.LibraryState{
367+
{
368+
ID: "j",
369+
Version: "1.1.0",
370+
PreviousVersion: "1.0.0",
371+
ReleaseTriggered: true,
372+
Changes: []*config.Commit{
373+
{
374+
Type: "feat",
375+
Subject: "new feature",
376+
CommitHash: hash1.String(),
377+
},
378+
{
379+
Type: "fix",
380+
Subject: "bulk change",
381+
CommitHash: hash2.String(),
382+
LibraryIDs: "a,b,c,d,e,f,g,h,i,j,k",
383+
},
384+
{
385+
Type: "chore",
386+
Subject: "bulk change 2",
387+
CommitHash: hash3.String(),
388+
LibraryIDs: "j,k,l,m,n,o,p,q,r,s",
389+
PiperCLNumber: "12345",
390+
},
391+
},
392+
},
393+
{
394+
ID: "k",
395+
Version: "2.4.0",
396+
PreviousVersion: "2.3.0",
397+
ReleaseTriggered: true,
398+
Changes: []*config.Commit{
399+
{
400+
Type: "fix",
401+
Subject: "bulk change",
402+
CommitHash: hash2.String(),
403+
LibraryIDs: "a,b,c,d,e,f,g,h,i,j,k",
404+
},
405+
},
406+
},
407+
},
408+
},
409+
ghRepo: &github.Repository{Owner: "owner", Name: "repo"},
410+
wantReleaseNote: fmt.Sprintf(`Librarian Version: %s
411+
Language Image: go:1.21
412+
<details><summary>j: 1.1.0</summary>
413+
414+
## [1.1.0](https://github.com/owner/repo/compare/j-1.0.0...j-1.1.0) (%s)
415+
416+
### Features
417+
418+
* new feature ([12345678](https://github.com/owner/repo/commit/12345678))
419+
420+
</details>
421+
422+
423+
<details><summary>k: 2.4.0</summary>
424+
425+
## [2.4.0](https://github.com/owner/repo/compare/k-2.3.0...k-2.4.0) (%s)
426+
427+
</details>
428+
429+
430+
<details><summary>Bulk Changes</summary>
431+
432+
* chore: bulk change 2 (PiperOrigin-RevId: 12345) ([abcdef00](https://github.com/owner/repo/commit/abcdef00))
433+
Libraries: j,k,l,m,n,o,p,q,r,s
434+
* fix: bulk change ([fedcba09](https://github.com/owner/repo/commit/fedcba09))
435+
Libraries: a,b,c,d,e,f,g,h,i,j,k
436+
</details>`,
437+
librarianVersion, today, today),
438+
},
361439
} {
362440
t.Run(test.name, func(t *testing.T) {
363441
t.Parallel()

internal/librarian/tag_and_release.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@ func parsePullRequestBody(body string) []libraryRelease {
211211
matches := detailsRegex.FindAllStringSubmatch(body, -1)
212212
for _, match := range matches {
213213
summary := match[1]
214+
if summary == "Bulk Changes" {
215+
continue
216+
}
214217
content := strings.TrimSpace(match[2])
215218

216219
summaryMatches := summaryRegex.FindStringSubmatch(summary)

internal/librarian/tag_and_release_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,29 @@ some content
318318
319319
[v1.2.3](https://github.com/googleapis/google-cloud-go/compare/google-cloud-storage-v1.2.2...google-cloud-storage-v1.2.3) (2025-08-15)
320320
321+
</details>`,
322+
want: []libraryRelease{
323+
{
324+
Version: "v1.2.3",
325+
Library: "google-cloud-storage",
326+
Body: "[v1.2.3](https://github.com/googleapis/google-cloud-go/compare/google-cloud-storage-v1.2.2...google-cloud-storage-v1.2.3) (2025-08-15)",
327+
},
328+
},
329+
},
330+
{
331+
name: "with bulk changes",
332+
body: `
333+
<details><summary>google-cloud-storage: v1.2.3</summary>
334+
335+
[v1.2.3](https://github.com/googleapis/google-cloud-go/compare/google-cloud-storage-v1.2.2...google-cloud-storage-v1.2.3) (2025-08-15)
336+
337+
</details>
338+
339+
340+
<details><summary>Bulk Changes</summary>
341+
342+
some content
343+
321344
</details>`,
322345
want: []libraryRelease{
323346
{

0 commit comments

Comments
 (0)