From f7547ea304ad3e3e7719af5cc564c8062acb61e3 Mon Sep 17 00:00:00 2001 From: willie-yao Date: Wed, 30 Aug 2023 18:22:28 +0000 Subject: [PATCH] Add weekly update script --- Makefile | 6 +- docs/release/release-tasks.md | 13 + hack/tools/release/internal/constants.go | 42 ++++ .../tools/release/{notes.go => notes/main.go} | 64 +++-- .../{notes_test.go => notes/main_test.go} | 0 hack/tools/release/weekly/main.go | 222 ++++++++++++++++++ 6 files changed, 310 insertions(+), 37 deletions(-) create mode 100644 hack/tools/release/internal/constants.go rename hack/tools/release/{notes.go => notes/main.go} (94%) rename hack/tools/release/{notes_test.go => notes/main_test.go} (100%) create mode 100644 hack/tools/release/weekly/main.go diff --git a/Makefile b/Makefile index 919ad8133b05..d3873b1c890e 100644 --- a/Makefile +++ b/Makefile @@ -1099,7 +1099,11 @@ release-alias-tag: ## Add the release alias tag to the last build tag .PHONY: release-notes-tool release-notes-tool: - go build -o bin/notes hack/tools/release/notes.go + go build -o bin/notes hack/tools/release/notes/main.go + +.PHONY: release-weekly-update +release-weekly-update: + go build -o bin/weekly hack/tools/release/weekly/main.go .PHONY: promote-images promote-images: $(KPROMO) diff --git a/docs/release/release-tasks.md b/docs/release/release-tasks.md index b05a102d9ef7..26e9bd58936c 100644 --- a/docs/release/release-tasks.md +++ b/docs/release/release-tasks.md @@ -318,6 +318,19 @@ The goal of this task to make the book for the current release available under e 3. Update references in introduction.md only on the main branch (drop unsupported versions, add the new release version).
Prior art: [Add release 1.2 book link](https://github.com/kubernetes-sigs/cluster-api/pull/6697) +#### Generate weekly PR updates to post in Slack +The goal of this task is to keep the CAPI community updated on recent PRs that have been merged. This is done by using the weekly update tool in `hack/tools/release/weekly/main.go`. Here is how to use it: +1. Checkout the latest commit on the release branch, e.g. `release-1.4`, or the main branch if the release branch doesn't yet exist (e.g. beta release). +2. Build the release weekly update tools binary. + ```bash + make release-weekly-update + ``` +3. Generate the weekly update with the following command: + ```bash + ./bin/weekly --from YYYY-MM-DD --to YYYY-MM-DD --milestone v1.x + ``` +4. Paste the output into a new Slack message in the [`#cluster-api`](https://kubernetes.slack.com/archives/C8TSNPY4T) channel. Currently, we post separate messages in a thread for `main` and the two most recent release branches (e.g. `release-1.5` and `release-1.4`). + #### Create PR for release notes 1. Checkout the `main` branch. 1. Build the release note tools binary. diff --git a/hack/tools/release/internal/constants.go b/hack/tools/release/internal/constants.go new file mode 100644 index 000000000000..5f2446676d09 --- /dev/null +++ b/hack/tools/release/internal/constants.go @@ -0,0 +1,42 @@ +/* +Copyright 2021 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 release is the package for the release notes generator. +package release + +// Common tags used by PRs. +const ( + // Features is the tag used for PRs that add new features. + Features = ":sparkles: New Features" + + // Bugs is the tag used for PRs that fix bugs. + Bugs = ":bug: Bug Fixes" + + // Documentation is the tag used for PRs that update documentation. + Documentation = ":book: Documentation" + + // Proposals is the tag used for PRs that add new proposals. + Proposals = ":memo: Proposals" + + // Warning is the tag used for PRs that add breaking changes. + Warning = ":warning: Breaking Changes" + + // Other is the tag used for PRs that don't fit in any other category. + Other = ":seedling: Others" + + // Unknown is the tag used for PRs that need to be sorted by hand. + Unknown = ":question: Sort these by hand" +) diff --git a/hack/tools/release/notes.go b/hack/tools/release/notes/main.go similarity index 94% rename from hack/tools/release/notes.go rename to hack/tools/release/notes/main.go index fc7c2a2344be..29e69ec50d50 100644 --- a/hack/tools/release/notes.go +++ b/hack/tools/release/notes/main.go @@ -33,6 +33,8 @@ import ( "strings" "sync" "time" + + release "sigs.k8s.io/cluster-api/hack/tools/release/internal" ) /* @@ -42,25 +44,15 @@ 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" - proposals = ":memo: Proposals" - warning = ":warning: Breaking Changes" - other = ":seedling: Others" - unknown = ":question: Sort these by hand" -) - var ( outputOrder = []string{ - proposals, - warning, - features, - bugs, - other, - documentation, - unknown, + release.Proposals, + release.Warning, + release.Features, + release.Bugs, + release.Other, + release.Documentation, + release.Unknown, } repo = flag.String("repository", "kubernetes-sigs/cluster-api", "The repo to run the tool from.") @@ -234,12 +226,12 @@ func run() int { } merges := map[string][]string{ - features: {}, - bugs: {}, - documentation: {}, - warning: {}, - other: {}, - unknown: {}, + release.Features: {}, + release.Bugs: {}, + release.Documentation: {}, + release.Warning: {}, + release.Other: {}, + release.Unknown: {}, } out, err := cmd.CombinedOutput() if err != nil { @@ -303,7 +295,7 @@ func run() int { continue } - if result.prEntry.section == documentation { + if result.prEntry.section == release.Documentation { merges[result.prEntry.section] = append(merges[result.prEntry.section], result.prEntry.prNumber) } else { merges[result.prEntry.section] = append(merges[result.prEntry.section], result.prEntry.title) @@ -349,17 +341,17 @@ REPLACE ME: A couple sentences describing the deprecation, including links to do } else if count > 1 { fmt.Printf("- %d new commits merged\n", count) } - if count := len(merges[warning]); count == 1 { + if count := len(merges[release.Warning]); count == 1 { fmt.Println("- 1 breaking change :warning:") } else if count > 1 { fmt.Printf("- %d breaking changes :warning:\n", count) } - if count := len(merges[features]); count == 1 { + if count := len(merges[release.Features]); count == 1 { fmt.Println("- 1 feature addition ✨") } else if count > 1 { fmt.Printf("- %d feature additions ✨\n", count) } - if count := len(merges[bugs]); count == 1 { + if count := len(merges[release.Bugs]); count == 1 { fmt.Println("- 1 bug fixed 🐛") } else if count > 1 { fmt.Printf("- %d bugs fixed 🐛\n", count) @@ -373,7 +365,7 @@ REPLACE ME: A couple sentences describing the deprecation, including links to do } switch key { - case documentation: + case release.Documentation: sort.Strings(mergeslice) if len(mergeslice) == 1 { fmt.Printf( @@ -502,29 +494,29 @@ func generateReleaseNoteEntry(c *commit) (*releaseNoteEntry, error) { switch { case strings.HasPrefix(entry.title, ":sparkles:"), strings.HasPrefix(entry.title, "✨"): - entry.section = features + entry.section = release.Features entry.title = removePrefixes(entry.title, []string{":sparkles:", "✨"}) case strings.HasPrefix(entry.title, ":bug:"), strings.HasPrefix(entry.title, "🐛"): - entry.section = bugs + entry.section = release.Bugs entry.title = removePrefixes(entry.title, []string{":bug:", "🐛"}) case strings.HasPrefix(entry.title, ":book:"), strings.HasPrefix(entry.title, "📖"): - entry.section = documentation + entry.section = release.Documentation entry.title = removePrefixes(entry.title, []string{":book:", "📖"}) if strings.Contains(entry.title, "CAEP") || strings.Contains(entry.title, "proposal") { - entry.section = proposals + entry.section = release.Proposals } case strings.HasPrefix(entry.title, ":warning:"), strings.HasPrefix(entry.title, "⚠️"): - entry.section = warning + entry.section = release.Warning entry.title = removePrefixes(entry.title, []string{":warning:", "⚠️"}) case strings.HasPrefix(entry.title, "🚀"), strings.HasPrefix(entry.title, "🌱 Release v1."): // TODO(g-gaston): remove the second condition using 🌱 prefix once 1.6 is released // Release trigger PRs from previous releases are not included in the release notes return nil, nil case strings.HasPrefix(entry.title, ":seedling:"), strings.HasPrefix(entry.title, "🌱"): - entry.section = other + entry.section = release.Other entry.title = removePrefixes(entry.title, []string{":seedling:", "🌱"}) default: - entry.section = unknown + entry.section = release.Unknown } // If the area label indicates documentation, use documentation as the section @@ -532,7 +524,7 @@ func generateReleaseNoteEntry(c *commit) (*releaseNoteEntry, error) { // tends to be more accurate than the emoji (data point observed by the release team). // We handle this after the switch statement to make sure we remove all emoji prefixes. if area == documentationAreaLabel { - entry.section = documentation + entry.section = release.Documentation } entry.title = strings.TrimSpace(entry.title) diff --git a/hack/tools/release/notes_test.go b/hack/tools/release/notes/main_test.go similarity index 100% rename from hack/tools/release/notes_test.go rename to hack/tools/release/notes/main_test.go diff --git a/hack/tools/release/weekly/main.go b/hack/tools/release/weekly/main.go new file mode 100644 index 000000000000..bedbe2edc7bb --- /dev/null +++ b/hack/tools/release/weekly/main.go @@ -0,0 +1,222 @@ +//go:build tools +// +build tools + +/* +Copyright 2019 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. +*/ + +// main is the main package for the release notes generator. +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "regexp" + "strings" + "time" + + release "sigs.k8s.io/cluster-api/hack/tools/release/internal" +) + +/* +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. +*/ + +var ( + outputOrder = []string{ + release.Proposals, + release.Warning, + release.Features, + release.Bugs, + release.Other, + release.Documentation, + release.Unknown, + } + + from = flag.String("from", "", "Include commits starting from and including this date. Accepts format: YYYY-MM-DD") + to = flag.String("to", "", "Include commits up to and including this date. Accepts format: YYYY-MM-DD") + + milestone = flag.String("milestone", "v1.4", "Milestone. Accepts format: v1.4") + + tagRegex = regexp.MustCompile(`^\[release-[\w-\.]*\]`) +) + +func main() { + flag.Parse() + os.Exit(run()) +} + +// Since git doesn't include the last day in rev-list we want to increase 1 day to include it in the interval. +func increaseDateByOneDay(date string) (string, error) { + layout := "2006-01-02" + datetime, err := time.Parse(layout, date) + if err != nil { + return "", err + } + datetime = datetime.Add(time.Hour * 24) + return datetime.Format(layout), nil +} + +func run() int { + var commitRange string + var cmd *exec.Cmd + + if *from == "" && *to == "" { + fmt.Println("--from and --to are required together or both unset") + return 1 + } + + commitRange = fmt.Sprintf("%s to %s", *from, *to) + lastDay, err := increaseDateByOneDay(*to) + if err != nil { + fmt.Println(err) + return 1 + } + + cmd = exec.Command("git", "rev-list", "HEAD", "--since=\""+*from+" 00:00:01\"", "--until=\""+lastDay+" 23:59:59\"", "--merges", "--pretty=format:%B") //nolint:gosec + + merges := map[string][]string{ + release.Features: {}, + release.Bugs: {}, + release.Documentation: {}, + release.Warning: {}, + release.Other: {}, + release.Unknown: {}, + } + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println("Error") + fmt.Println(err) + fmt.Println(string(out)) + return 1 + } + + commits := []*commit{} + outLines := strings.Split(string(out), "\n") + for _, line := range outLines { + line = strings.TrimSpace(line) + last := len(commits) - 1 + switch { + case strings.HasPrefix(line, "commit"): + commits = append(commits, &commit{}) + case strings.HasPrefix(line, "Merge"): + commits[last].merge = line + continue + case line == "": + default: + commits[last].body = line + } + } + + for _, c := range commits { + body := trimTitle(c.body) + var key, prNumber, fork string + switch { + case strings.HasPrefix(body, ":sparkles:"), strings.HasPrefix(body, "✨"): + key = release.Features + body = strings.TrimPrefix(body, ":sparkles:") + body = strings.TrimPrefix(body, "✨") + case strings.HasPrefix(body, ":bug:"), strings.HasPrefix(body, "🐛"): + key = release.Bugs + body = strings.TrimPrefix(body, ":bug:") + body = strings.TrimPrefix(body, "🐛") + case strings.HasPrefix(body, ":book:"), strings.HasPrefix(body, "📖"): + key = release.Documentation + body = strings.TrimPrefix(body, ":book:") + body = strings.TrimPrefix(body, "📖") + if strings.Contains(body, "CAEP") || strings.Contains(body, "proposal") { + key = release.Proposals + } + case strings.HasPrefix(body, ":seedling:"), strings.HasPrefix(body, "🌱"): + key = release.Other + body = strings.TrimPrefix(body, ":seedling:") + body = strings.TrimPrefix(body, "🌱") + case strings.HasPrefix(body, ":warning:"), strings.HasPrefix(body, "⚠️"): + key = release.Warning + body = strings.TrimPrefix(body, ":warning:") + body = strings.TrimPrefix(body, "⚠️") + default: + key = release.Unknown + } + + body = strings.TrimSpace(body) + if body == "" { + continue + } + body = fmt.Sprintf("\t - %s", body) + _, _ = fmt.Sscanf(c.merge, "Merge pull request %s from %s", &prNumber, &fork) + if key == release.Documentation { + merges[key] = append(merges[key], prNumber) + continue + } + merges[key] = append(merges[key], formatMerge(body, prNumber)) + } + + // TODO Turn this into a link (requires knowing the project name + organization) + fmt.Println("Weekly update :rotating_light:") + fmt.Printf("Changes from %v a total of %d new commits where merged into main.\n\n", commitRange, len(commits)) + + for _, key := range outputOrder { + mergeslice := merges[key] + if len(mergeslice) == 0 { + continue + } + + switch key { + case release.Documentation: + fmt.Printf("- %d Documentation and book contributions :book: \n\n", len(mergeslice)) + case release.Other: + fmt.Printf("- %d Other changes :seedling:\n\n", len(merges[release.Other])) + default: + fmt.Printf("- %d %s\n", len(merges[key]), key) + for _, merge := range mergeslice { + fmt.Println(merge) + } + fmt.Println() + } + } + + fmt.Println("All merged PRs can be viewed in GitHub:") + fmt.Println("https://github.com/kubernetes-sigs/cluster-api/pulls?q=is%3Apr+closed%3A" + *from + ".." + lastDay + "+is%3Amerged+milestone%3A" + *milestone + "+\n") + + fmt.Println("_Thanks to all our contributors!_ 😊") + fmt.Println("/Your friendly comms release team") + + return 0 +} + +func trimTitle(title string) string { + // Remove a tag prefix if found. + title = tagRegex.ReplaceAllString(title, "") + + return strings.TrimSpace(title) +} + +type commit struct { + merge string + body string +} + +func formatMerge(line, prNumber string) string { + if prNumber == "" { + return line + } + return fmt.Sprintf("%s (%s)", line, prNumber) +}