Skip to content

Commit eb1dc9a

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 eb1dc9a

File tree

3 files changed

+242
-0
lines changed

3 files changed

+242
-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: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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+
"regexp"
26+
"log"
27+
"os"
28+
"strings"
29+
30+
"github.com/blang/semver"
31+
"github.com/google/go-github/github"
32+
"golang.org/x/oauth2"
33+
)
34+
35+
/*
36+
This tool prints all the titles of all PRs from previous release to HEAD.
37+
This needs to be run *before* a tag is created.
38+
39+
Use these as the base of your release notes.
40+
*/
41+
42+
const (
43+
features = ":sparkles: New Features"
44+
bugs = ":bug: Bug Fixes"
45+
documentation = ":book: Documentation"
46+
warning = ":warning: Breaking Changes"
47+
other = ":seedling: Others"
48+
unknown = ":question: Sort these by hand"
49+
superseded = ":recycle: Superseded or Reverted"
50+
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"
51+
)
52+
53+
var (
54+
outputOrder = []string{
55+
warning,
56+
features,
57+
bugs,
58+
documentation,
59+
other,
60+
unknown,
61+
superseded,
62+
}
63+
releaseTag string
64+
repoOwner string
65+
repoName string
66+
semVersion semver.Version
67+
lastReleaseTag string
68+
)
69+
70+
func main() {
71+
releaseTag = os.Getenv("RELEASE_TAG")
72+
if releaseTag == "" {
73+
log.Fatal("RELEASE_TAG is required")
74+
}
75+
repoOwner = os.Getenv("REPO_OWNER")
76+
if repoOwner == "" {
77+
log.Fatal("REPO_OWNER is required")
78+
}
79+
repoName = os.Getenv("REPO_NAME")
80+
if repoName == "" {
81+
log.Fatal("REPO_NAME is required")
82+
}
83+
84+
// Create a context
85+
ctx := context.Background()
86+
87+
// Authenticate with GitHub token if available
88+
token := os.Getenv("GITHUB_TOKEN")
89+
var client *github.Client
90+
if token != "" {
91+
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
92+
tc := oauth2.NewClient(ctx, ts)
93+
client = github.NewClient(tc)
94+
} else {
95+
client = github.NewClient(nil)
96+
}
97+
releaseName := strings.TrimPrefix(releaseTag, "v")
98+
var err error
99+
semVersion, err = semver.Make(releaseName)
100+
if err != nil {
101+
log.Fatalf("Incorrect releaseTag: %v", err)
102+
}
103+
104+
// Get the name of the release branch. Default to "main" if it's a minor release
105+
releaseBranch := fmt.Sprintf("release-%d.%d", semVersion.Major, semVersion.Minor)
106+
if semVersion.Patch == 0 {
107+
releaseBranch = "main"
108+
}
109+
110+
// Get the release tag used for comparison
111+
lastVersion := semVersion
112+
if lastVersion.Patch == 0 {
113+
lastVersion.Minor--
114+
} else {
115+
lastVersion.Patch--
116+
}
117+
lastReleaseTag = fmt.Sprintf("v%d.%d.%d", lastVersion.Major, lastVersion.Minor, lastVersion.Patch)
118+
119+
// Compare commits between the tag and the release branch
120+
comparison, _, err := client.Repositories.CompareCommits(ctx, repoOwner, repoName, lastReleaseTag, releaseBranch)
121+
if err != nil {
122+
log.Fatalf("failed to compare commits: %v", err)
123+
}
124+
merges := map[string][]string{
125+
features: {},
126+
bugs: {},
127+
documentation: {},
128+
warning: {},
129+
other: {},
130+
unknown: {},
131+
superseded: {},
132+
}
133+
134+
for _, commit := range comparison.Commits {
135+
// Only takes the merge commits.
136+
if len(commit.Parents) == 1 {
137+
continue
138+
}
139+
mergeCommitRegex := regexp.MustCompile(`Merge pull request #(\d+) from`)
140+
matches := mergeCommitRegex.FindStringSubmatch(commit.GetCommit().GetMessage())
141+
var prNumber string
142+
if len(matches) > 1 {
143+
// This is a merge commit, extract the PR number
144+
prNumber = matches[1]
145+
}
146+
147+
// Append commit message and PR number
148+
lines := strings.Split(commit.GetCommit().GetMessage(), "\n")
149+
body := lines[len(lines)-1]
150+
if body == "" {
151+
continue
152+
}
153+
var key string
154+
switch {
155+
case strings.HasPrefix(body, ":sparkles:"), strings.HasPrefix(body, "✨"):
156+
key = features
157+
body = strings.TrimPrefix(body, ":sparkles:")
158+
body = strings.TrimPrefix(body, "✨")
159+
case strings.HasPrefix(body, ":bug:"), strings.HasPrefix(body, "🐛"):
160+
key = bugs
161+
body = strings.TrimPrefix(body, ":bug:")
162+
body = strings.TrimPrefix(body, "🐛")
163+
case strings.HasPrefix(body, ":book:"), strings.HasPrefix(body, "📖"):
164+
key = documentation
165+
body = strings.TrimPrefix(body, ":book:")
166+
body = strings.TrimPrefix(body, "📖")
167+
case strings.HasPrefix(body, ":seedling:"), strings.HasPrefix(body, "🌱"):
168+
key = other
169+
body = strings.TrimPrefix(body, ":seedling:")
170+
body = strings.TrimPrefix(body, "🌱")
171+
case strings.HasPrefix(body, ":warning:"), strings.HasPrefix(body, "⚠️"):
172+
key = warning
173+
body = strings.TrimPrefix(body, ":warning:")
174+
body = strings.TrimPrefix(body, "⚠️")
175+
case strings.HasPrefix(body, ":rocket:"), strings.HasPrefix(body, "🚀"):
176+
continue
177+
default:
178+
key = unknown
179+
}
180+
merges[key] = append(merges[key], fmt.Sprintf("- %s (#%d)", body, prNumber))
181+
}
182+
fmt.Println("<!-- markdownlint-disable no-inline-html line-length -->")
183+
// if we're doing beta/rc, print breaking changes and hide the rest of the changes
184+
if len(semVersion.Pre) > 0 {
185+
switch semVersion.Pre[0].VersionStr {
186+
case "beta":
187+
fmt.Printf(warningTemplate, "BETA RELEASE", repoOwner, repoName)
188+
case "rc":
189+
fmt.Printf(warningTemplate, "RELEASE CANDIDATE", repoOwner, repoName)
190+
}
191+
fmt.Printf("<details>\n")
192+
fmt.Printf("<summary>More details about the release</summary>\n\n")
193+
}
194+
fmt.Printf("# Changes since [%s](https://github.com/%s/%s/tree/%s)\n\n", lastReleaseTag, repoOwner, repoName, lastReleaseTag)
195+
// print the changes by category
196+
for _, key := range outputOrder {
197+
mergeslice := merges[key]
198+
if len(mergeslice) > 0 {
199+
fmt.Printf("## %v\n\n", key)
200+
for _, merge := range mergeslice {
201+
fmt.Println(merge)
202+
}
203+
fmt.Println()
204+
}
205+
}
206+
207+
// close the details tag if we had it open, else add the Superseded or Reverted section
208+
if len(semVersion.Pre) > 0 {
209+
fmt.Printf("</details>\n\n")
210+
} else {
211+
fmt.Println("\n## :recycle: Superseded or Reverted")
212+
}
213+
214+
fmt.Printf("The container image for this release is: %s\n", releaseTag)
215+
if repoName == "cluster-api-provider-metal3" {
216+
fmt.Printf("Mariadb image tag is: capm3-%s\n", releaseTag)
217+
}
218+
fmt.Println("\n_Thanks to all our contributors!_ 😊")
219+
}

0 commit comments

Comments
 (0)