44 "context"
55 "fmt"
66 "regexp"
7+ "sort"
78 "strings"
89
910 "github.com/google/go-github/v68/github"
@@ -77,7 +78,7 @@ func (g *Generator) getReferencedPRs(ctx context.Context, _ []*github.Commit) ([
7778 var prs []* github.PullRequest
7879
7980 // Search for PRs referenced in commit messages
80- query := fmt .Sprintf (" repo:%s/%s is:pr is:merged" , g .owner , g .repo )
81+ query := fmt .Sprintf (` repo:%s/%s is:pr is:merged label:"release-note","release-note-needed"` , g .owner , g .repo )
8182 result , _ , err := g .client .Search .Issues (ctx , query , & github.SearchOptions {
8283 TextMatch : true ,
8384 ListOptions : github.ListOptions {
@@ -110,14 +111,41 @@ func (g *Generator) generateChangelog(prs []*github.PullRequest) string {
110111 kind := g .getPRKind (pr )
111112 buckets [kind ] = append (buckets [kind ], pr )
112113 }
113-
114- // Print each bucket
115- for kind , header := range kindHeaders {
116- if prs , ok := buckets [kind ]; ok && len (prs ) > 0 {
117- changelog .WriteString (fmt .Sprintf ("\n ## %s\n \n " , header ))
118- for _ , pr := range prs {
119- changelog .WriteString (fmt .Sprintf ("- %s (#%d)\n " , * pr .Title , * pr .Number ))
114+ // sort the buckets by kind to ensure deterministic output
115+ kinds := make ([]string , 0 , len (kindHeaders ))
116+ for kind := range kindHeaders {
117+ kinds = append (kinds , kind )
118+ }
119+ sort .Strings (kinds )
120+
121+ // build the changelog
122+ for _ , kind := range kinds {
123+ header := kindHeaders [kind ]
124+ prs := buckets [kind ]
125+ if len (prs ) == 0 {
126+ continue
127+ }
128+ changelog .WriteString (fmt .Sprintf ("\n ## %s\n \n " , header ))
129+ for _ , pr := range prs {
130+ body := pr .GetBody ()
131+ if body == "" {
132+ continue
120133 }
134+ // attempt to extract a release-note from the PR body
135+ match := releaseNoteRE .FindStringSubmatch (body )
136+ if len (match ) < 2 {
137+ // skip PRs that have the release-note label but no release-note body.
138+ // TODO(tim): this shouldn't be possible with the labeler being a required
139+ // check, but we'll check for it anyway in case users have manually added
140+ // the label to a PR.
141+ continue
142+ }
143+ note := match [1 ]
144+ if note == "" {
145+ // TODO(tim): we should probably log this as an error as this is unexpected
146+ continue
147+ }
148+ changelog .WriteString (fmt .Sprintf ("- %s (#%d)\n " , note , pr .GetNumber ()))
121149 }
122150 }
123151
@@ -129,9 +157,9 @@ func (g *Generator) getPRKind(pr *github.PullRequest) string {
129157 // Check labels first
130158 for _ , label := range pr .Labels {
131159 switch * label .Name {
132- case "kind/new- feature" :
160+ case "kind/feature" , "kind/new_feature " :
133161 return "new_feature"
134- case "kind/bug " :
162+ case "kind/fix" , "kind/bug_fix " :
135163 return "bug_fix"
136164 case "kind/breaking_change" :
137165 return "breaking_change"
@@ -142,20 +170,5 @@ func (g *Generator) getPRKind(pr *github.PullRequest) string {
142170 }
143171 }
144172
145- // Fall back to title-based detection
146- title := strings .ToLower (* pr .Title )
147- switch {
148- case strings .Contains (title , "feat" ) || strings .Contains (title , "feature" ):
149- return "new_feature"
150- case strings .Contains (title , "fix" ) || strings .Contains (title , "bug" ):
151- return "bug_fix"
152- case strings .Contains (title , "break" ) || strings .Contains (title , "breaking" ):
153- return "breaking_change"
154- case strings .Contains (title , "doc" ) || strings .Contains (title , "docs" ):
155- return "documentation"
156- case strings .Contains (title , "perf" ) || strings .Contains (title , "performance" ):
157- return "performance"
158- default :
159- return "other"
160- }
173+ return "other"
161174}
0 commit comments