Skip to content

Commit

Permalink
Add new reusable release note generator
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
mquhuy committed Jan 10, 2025
1 parent 6d3818e commit eb1dc9a
Show file tree
Hide file tree
Showing 3 changed files with 242 additions and 0 deletions.
11 changes: 11 additions & 0 deletions hack/release/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module github.com/metal3-io/project-infra/hack/release

go 1.23.2

require (
github.com/blang/semver v3.5.1+incompatible
github.com/google/go-github v17.0.0+incompatible
golang.org/x/oauth2 v0.25.0
)

require github.com/google/go-querystring v1.1.0 // indirect
12 changes: 12 additions & 0 deletions hack/release/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
219 changes: 219 additions & 0 deletions hack/release/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
//go:build tools
// +build tools

/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"context"
"fmt"
"regexp"
"log"
"os"
"strings"

"github.com/blang/semver"
"github.com/google/go-github/github"
"golang.org/x/oauth2"
)

/*
This tool prints all the titles of all PRs from previous release to HEAD.
This needs to be run *before* a tag is created.
Use these as the base of your release notes.
*/

const (
features = ":sparkles: New Features"
bugs = ":bug: Bug Fixes"
documentation = ":book: Documentation"
warning = ":warning: Breaking Changes"
other = ":seedling: Others"
unknown = ":question: Sort these by hand"
superseded = ":recycle: Superseded or Reverted"
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"
)

var (
outputOrder = []string{
warning,
features,
bugs,
documentation,
other,
unknown,
superseded,
}
releaseTag string
repoOwner string
repoName string
semVersion semver.Version
lastReleaseTag string
)

func main() {
releaseTag = os.Getenv("RELEASE_TAG")
if releaseTag == "" {
log.Fatal("RELEASE_TAG is required")
}
repoOwner = os.Getenv("REPO_OWNER")
if repoOwner == "" {
log.Fatal("REPO_OWNER is required")
}
repoName = os.Getenv("REPO_NAME")
if repoName == "" {
log.Fatal("REPO_NAME is required")
}

// Create a context
ctx := context.Background()

// Authenticate with GitHub token if available
token := os.Getenv("GITHUB_TOKEN")
var client *github.Client
if token != "" {
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
tc := oauth2.NewClient(ctx, ts)
client = github.NewClient(tc)
} else {
client = github.NewClient(nil)
}
releaseName := strings.TrimPrefix(releaseTag, "v")
var err error
semVersion, err = semver.Make(releaseName)
if err != nil {
log.Fatalf("Incorrect releaseTag: %v", err)
}

// Get the name of the release branch. Default to "main" if it's a minor release
releaseBranch := fmt.Sprintf("release-%d.%d", semVersion.Major, semVersion.Minor)
if semVersion.Patch == 0 {
releaseBranch = "main"
}

// Get the release tag used for comparison
lastVersion := semVersion
if lastVersion.Patch == 0 {
lastVersion.Minor--
} else {
lastVersion.Patch--
}
lastReleaseTag = fmt.Sprintf("v%d.%d.%d", lastVersion.Major, lastVersion.Minor, lastVersion.Patch)

// Compare commits between the tag and the release branch
comparison, _, err := client.Repositories.CompareCommits(ctx, repoOwner, repoName, lastReleaseTag, releaseBranch)
if err != nil {
log.Fatalf("failed to compare commits: %v", err)
}
merges := map[string][]string{
features: {},
bugs: {},
documentation: {},
warning: {},
other: {},
unknown: {},
superseded: {},
}

for _, commit := range comparison.Commits {
// Only takes the merge commits.
if len(commit.Parents) == 1 {
continue
}
mergeCommitRegex := regexp.MustCompile(`Merge pull request #(\d+) from`)
matches := mergeCommitRegex.FindStringSubmatch(commit.GetCommit().GetMessage())
var prNumber string
if len(matches) > 1 {
// This is a merge commit, extract the PR number
prNumber = matches[1]
}

// Append commit message and PR number
lines := strings.Split(commit.GetCommit().GetMessage(), "\n")
body := lines[len(lines)-1]
if body == "" {
continue
}
var key string
switch {
case strings.HasPrefix(body, ":sparkles:"), strings.HasPrefix(body, "✨"):
key = features
body = strings.TrimPrefix(body, ":sparkles:")
body = strings.TrimPrefix(body, "✨")
case strings.HasPrefix(body, ":bug:"), strings.HasPrefix(body, "🐛"):
key = bugs
body = strings.TrimPrefix(body, ":bug:")
body = strings.TrimPrefix(body, "🐛")
case strings.HasPrefix(body, ":book:"), strings.HasPrefix(body, "📖"):
key = documentation
body = strings.TrimPrefix(body, ":book:")
body = strings.TrimPrefix(body, "📖")
case strings.HasPrefix(body, ":seedling:"), strings.HasPrefix(body, "🌱"):
key = other
body = strings.TrimPrefix(body, ":seedling:")
body = strings.TrimPrefix(body, "🌱")
case strings.HasPrefix(body, ":warning:"), strings.HasPrefix(body, "⚠️"):
key = warning
body = strings.TrimPrefix(body, ":warning:")
body = strings.TrimPrefix(body, "⚠️")
case strings.HasPrefix(body, ":rocket:"), strings.HasPrefix(body, "🚀"):
continue
default:
key = unknown
}
merges[key] = append(merges[key], fmt.Sprintf("- %s (#%d)", body, prNumber))
}
fmt.Println("<!-- markdownlint-disable no-inline-html line-length -->")
// if we're doing beta/rc, print breaking changes and hide the rest of the changes
if len(semVersion.Pre) > 0 {
switch semVersion.Pre[0].VersionStr {
case "beta":
fmt.Printf(warningTemplate, "BETA RELEASE", repoOwner, repoName)
case "rc":
fmt.Printf(warningTemplate, "RELEASE CANDIDATE", repoOwner, repoName)
}
fmt.Printf("<details>\n")
fmt.Printf("<summary>More details about the release</summary>\n\n")
}
fmt.Printf("# Changes since [%s](https://github.com/%s/%s/tree/%s)\n\n", lastReleaseTag, repoOwner, repoName, lastReleaseTag)
// print the changes by category
for _, key := range outputOrder {
mergeslice := merges[key]
if len(mergeslice) > 0 {
fmt.Printf("## %v\n\n", key)
for _, merge := range mergeslice {
fmt.Println(merge)
}
fmt.Println()
}
}

// close the details tag if we had it open, else add the Superseded or Reverted section
if len(semVersion.Pre) > 0 {
fmt.Printf("</details>\n\n")
} else {
fmt.Println("\n## :recycle: Superseded or Reverted")
}

fmt.Printf("The container image for this release is: %s\n", releaseTag)
if repoName == "cluster-api-provider-metal3" {
fmt.Printf("Mariadb image tag is: capm3-%s\n", releaseTag)
}
fmt.Println("\n_Thanks to all our contributors!_ 😊")
}

0 comments on commit eb1dc9a

Please sign in to comment.