| 
 | 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