Skip to content

Commit 0ffe6e5

Browse files
author
Huy Mai
committed
Add new reusable release note generator
Compare to the current script that we are having in all releasing repositories, this tool: - Is simpler - Take custom repo name and repo owner - Doesn't use os.Exec(), hence doesn't need to run inside the repo - Doesn't require uptream tag fetching - Doesn't require, but still supports, GITHUB_TOKEN - Is more reliable as everything is fetched from github Signed-off-by: Huy Mai <[email protected]>
1 parent 6d3818e commit 0ffe6e5

File tree

3 files changed

+243
-0
lines changed

3 files changed

+243
-0
lines changed

hack/release/go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module github.com/metal3-io/project-infra/hack/release
2+
3+
go 1.23.2
4+
5+
require (
6+
github.com/blang/semver v3.5.1+incompatible
7+
github.com/google/go-github v17.0.0+incompatible
8+
golang.org/x/oauth2 v0.25.0
9+
)
10+
11+
require github.com/google/go-querystring v1.1.0 // indirect

hack/release/go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
2+
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
3+
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
4+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
5+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
6+
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
7+
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
8+
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
9+
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
10+
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
11+
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
12+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

hack/release/main.go

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
//go:build tools
2+
// +build tools
3+
4+
/*
5+
Copyright 2025 The Kubernetes Authors.
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
*/
19+
20+
package main
21+
22+
import (
23+
"context"
24+
"fmt"
25+
"log"
26+
"os"
27+
"strings"
28+
29+
"github.com/blang/semver"
30+
"github.com/google/go-github/github"
31+
"golang.org/x/oauth2"
32+
)
33+
34+
/*
35+
This tool prints all the titles of all PRs from previous release to HEAD.
36+
This needs to be run *before* a tag is created.
37+
38+
Use these as the base of your release notes.
39+
*/
40+
41+
const (
42+
features = ":sparkles: New Features"
43+
bugs = ":bug: Bug Fixes"
44+
documentation = ":book: Documentation"
45+
warning = ":warning: Breaking Changes"
46+
other = ":seedling: Others"
47+
unknown = ":question: Sort these by hand"
48+
superseded = ":recycle: Superseded or Reverted"
49+
warningTemplate = ":rotating_light: This is a %s. Use it only for testing purposes.\nIf you find any bugs, file an [issue](https://github.com/%s/%s/issues/new/).\n\n"
50+
)
51+
52+
var (
53+
outputOrder = []string{
54+
warning,
55+
features,
56+
bugs,
57+
documentation,
58+
other,
59+
unknown,
60+
superseded,
61+
}
62+
releaseTag string
63+
repoOwner string
64+
repoName string
65+
semVersion semver.Version
66+
lastReleaseTag string
67+
)
68+
69+
func main() {
70+
releaseTag = os.Getenv("RELEASE_TAG")
71+
if releaseTag == "" {
72+
log.Fatal("RELEASE_TAG is required")
73+
}
74+
repoOwner = os.Getenv("REPO_OWNER")
75+
if repoOwner == "" {
76+
log.Fatal("REPO_OWNER is required")
77+
}
78+
repoName = os.Getenv("REPO_NAME")
79+
if repoName == "" {
80+
log.Fatal("REPO_NAME is required")
81+
}
82+
83+
// Create a context
84+
ctx := context.Background()
85+
86+
// Authenticate with GitHub token if available
87+
token := os.Getenv("GITHUB_TOKEN")
88+
var client *github.Client
89+
if token != "" {
90+
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
91+
tc := oauth2.NewClient(ctx, ts)
92+
client = github.NewClient(tc)
93+
} else {
94+
client = github.NewClient(nil)
95+
}
96+
releaseName := strings.TrimPrefix(releaseTag, "v")
97+
var err error
98+
semVersion, err = semver.Make(releaseName)
99+
if err != nil {
100+
log.Fatalf("Incorrect releaseTag: %v", err)
101+
}
102+
103+
// Get the name of the release branch. Default to "main" if it's a minor release
104+
releaseBranch := fmt.Sprintf("release-%d.%d", semVersion.Major, semVersion.Minor)
105+
if semVersion.Patch == 0 {
106+
releaseBranch = "main"
107+
}
108+
109+
// Get the release tag used for comparison
110+
lastVersion := semVersion
111+
if lastVersion.Patch == 0 {
112+
lastVersion.Minor--
113+
} else {
114+
lastVersion.Patch--
115+
}
116+
lastReleaseTag = fmt.Sprintf("v%d.%d.%d", lastVersion.Major, lastVersion.Minor, lastVersion.Patch)
117+
118+
// Compare commits between the tag and the release branch
119+
comparison, _, err := client.Repositories.CompareCommits(ctx, repoOwner, repoName, lastReleaseTag, releaseBranch)
120+
if err != nil {
121+
log.Fatalf("failed to compare commits: %w", err)
122+
}
123+
merges := map[string][]string{
124+
features: {},
125+
bugs: {},
126+
documentation: {},
127+
warning: {},
128+
other: {},
129+
unknown: {},
130+
superseded: {},
131+
}
132+
133+
for _, commit := range comparison.Commits {
134+
// Skip merge commits (those with more than one parent)
135+
if len(commit.Parents) > 1 {
136+
continue
137+
}
138+
// Search for PRs associated with the commit
139+
query := fmt.Sprintf("%s repo:%s/%s type:pr", commit.GetSHA(), repoOwner, repoName)
140+
searchResults, _, err := client.Search.Issues(ctx, query, nil)
141+
if err != nil {
142+
log.Fatalf("failed to fetch PRs for commit %s: %w", commit.GetSHA(), err)
143+
}
144+
145+
// Format the result
146+
var prNumber int
147+
if len(searchResults.Issues) > 0 {
148+
prNumber = searchResults.Issues[0].GetNumber()
149+
}
150+
151+
// Append commit message and PR number
152+
body := strings.Split(commit.GetCommit().GetMessage(), "\n")[0] // Take the first line of the commit message
153+
if body == "" {
154+
continue
155+
}
156+
var key string
157+
switch {
158+
case strings.HasPrefix(body, ":sparkles:"), strings.HasPrefix(body, "✨"):
159+
key = features
160+
body = strings.TrimPrefix(body, ":sparkles:")
161+
body = strings.TrimPrefix(body, "✨")
162+
case strings.HasPrefix(body, ":bug:"), strings.HasPrefix(body, "🐛"):
163+
key = bugs
164+
body = strings.TrimPrefix(body, ":bug:")
165+
body = strings.TrimPrefix(body, "🐛")
166+
case strings.HasPrefix(body, ":book:"), strings.HasPrefix(body, "📖"):
167+
key = documentation
168+
body = strings.TrimPrefix(body, ":book:")
169+
body = strings.TrimPrefix(body, "📖")
170+
case strings.HasPrefix(body, ":seedling:"), strings.HasPrefix(body, "🌱"):
171+
key = other
172+
body = strings.TrimPrefix(body, ":seedling:")
173+
body = strings.TrimPrefix(body, "🌱")
174+
case strings.HasPrefix(body, ":warning:"), strings.HasPrefix(body, "⚠️"):
175+
key = warning
176+
body = strings.TrimPrefix(body, ":warning:")
177+
body = strings.TrimPrefix(body, "⚠️")
178+
case strings.HasPrefix(body, ":rocket:"), strings.HasPrefix(body, "🚀"):
179+
continue
180+
default:
181+
key = unknown
182+
}
183+
merges[key] = append(merges[key], fmt.Sprintf("- %s (#%d)", body, prNumber))
184+
}
185+
fmt.Println("<!-- markdownlint-disable no-inline-html line-length -->")
186+
// if we're doing beta/rc, print breaking changes and hide the rest of the changes
187+
if len(semVersion.Pre) > 0 {
188+
switch semVersion.Pre[0].VersionStr {
189+
case "beta":
190+
fmt.Printf(warningTemplate, "BETA RELEASE", repoOwner, repoName)
191+
case "rc":
192+
fmt.Printf(warningTemplate, "RELEASE CANDIDATE", repoOwner, repoName)
193+
}
194+
fmt.Printf("<details>\n")
195+
fmt.Printf("<summary>More details about the release</summary>\n\n")
196+
}
197+
fmt.Printf("# Changes since [%s](https://github.com/%s/%s/tree/%s)\n\n", lastReleaseTag, repoOwner, repoName, lastReleaseTag)
198+
// print the changes by category
199+
for _, key := range outputOrder {
200+
mergeslice := merges[key]
201+
if len(mergeslice) > 0 {
202+
fmt.Printf("## %v\n\n", key)
203+
for _, merge := range mergeslice {
204+
fmt.Println(merge)
205+
}
206+
fmt.Println()
207+
}
208+
}
209+
210+
// close the details tag if we had it open
211+
if len(semVersion.Pre) > 0 {
212+
fmt.Printf("</details>\n\n")
213+
}
214+
215+
fmt.Printf("The container image for this release is: %s\n", releaseTag)
216+
if repoName == "cluster-api-provider-metal3" {
217+
fmt.Printf("Mariadb image tag is: capm3-%s\n", releaseTag)
218+
}
219+
fmt.Println("\n_Thanks to all our contributors!_ 😊")
220+
}

0 commit comments

Comments
 (0)