From 333dd2f6488ac8ac4f005058de23f6e2b37ea536 Mon Sep 17 00:00:00 2001 From: Andrea Frittoli Date: Fri, 7 Mar 2025 15:19:47 +0000 Subject: [PATCH] Add a tool to generate the release name Signed-off-by: Andrea Frittoli --- tekton/release-cheat-sheet.md | 60 +++++---- tekton/release_names.go | 237 ++++++++++++++++++++++++++++++++++ 2 files changed, 274 insertions(+), 23 deletions(-) create mode 100644 tekton/release_names.go diff --git a/tekton/release-cheat-sheet.md b/tekton/release-cheat-sheet.md index 683f8e6623b..c9a47e565fe 100644 --- a/tekton/release-cheat-sheet.md +++ b/tekton/release-cheat-sheet.md @@ -12,24 +12,54 @@ the pipelines repo, a terminal window and a text editor. 1. [Install kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize) if you haven't already. -1. Ensure the correct version of the release pipeline is installed on the cluster: +1. Select the commit you would like to build the release from (NOTE: the commit is full (40-digit) hash.) + - Select the most recent commit on the ***main branch*** if you are cutting a major or minor release i.e. `x.0.0` or `0.x.0` + - Select the most recent commit on the ***`release-x` branch***, e.g. [`release-v0.47.x`](https://github.com/tektoncd/pipeline/tree/release-v0.47.x) if you are patching a release i.e. `v0.47.2`. + +1. Ensure the correct version of the release pipeline is installed on the cluster. + To do that, the selected commit should be checked-out locally ```bash kustomize build tekton | kubectl --context dogfooding replace -f - ``` -1. Create environment variables for bash scripts in later steps. +1. Choose a name for the new release! The usual pattern is "< cat breed > < famous robot >" e.g. "Ragdoll Norby". For LTS releases, add a suffix "LTS" in the name such as "< cat breed > < famous robot > LTS" e.g. "Ragdoll Norby LTS". Use this command to generate a name that has not yet been used: ```bash - TEKTON_VERSION=# Example: v0.21.0 + go run tekton/release_names.go + ``` + + It returns something like: + + ```json + { + "release_name": "Khao Manee KARR", + "cat_breed_url": "https://en.wikipedia.org/wiki/Khao_Manee", + "robot_url": "https://en.wikipedia.org/wiki/KARR" + } + ``` + + The URLs can be used to find out more about the cat breed and robot selected by the tool. + Previous release names can also be found with the following command: + + ```bash + curl \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/tektoncd/pipeline/releases\?per_page=100 \ + | jq ".[].name" | cut -d'"' -f 3 | tr -d '\' | sort -u ``` - - Select the commit you would like to build the release from (NOTE: the commit is full (40-digit) hash.) - - Select the most recent commit on the ***main branch*** if you are cutting a major or minor release i.e. `x.0.0` or `0.x.0` - - Select the most recent commit on the ***`release-x` branch***, e.g. [`release-v0.47.x`](https://github.com/tektoncd/pipeline/tree/release-v0.47.x) if you are patching a release i.e. `v0.47.2`. +1. Create a `release.env` file with environment variables for bash scripts in later steps, and source it: ```bash - TEKTON_RELEASE_GIT_SHA=# SHA of the release to be released + cat < release.env + TEKTON_VERSION= # Example: v0.69.0 + TEKTON_RELEASE_GIT_SHA= # SHA of the release to be released, e.g. 5b082b1106753e093593d12152c82e1c4b0f37e5 + TEKTON_OLD_VERSION= # Example: v0.68.0 + TEKTON_RELEASE_NAME="Oriental Longhair Omnibot" # Name of the release + TEKTON_PACKAGE=tektoncd/pipeline + EOF + . ./release.env ``` 1. Confirm commit SHA matches what you want to release. @@ -103,15 +133,6 @@ the pipelines repo, a terminal window and a text editor. 1. The YAMLs are now released! Anyone installing Tekton Pipelines will get the new version. Time to create a new GitHub release announcement: - 1. Choose a name for the new release! The usual pattern is "< cat breed > < famous robot >" e.g. "Ragdoll Norby". For LTS releases, add a suffix "LTS" in the name such as "< cat breed > < famous robot > LTS" e.g. "Ragdoll Norby LTS". Browse [the releases page](https://github.com/tektoncd/pipeline/releases) or run this command to check which names have already been used: - - ```bash - curl \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos/tektoncd/pipeline/releases\?per_page=100 \ - | jq ".[].name" - ``` - 1. Find the Rekor UUID for the release ```bash @@ -121,13 +142,6 @@ the pipelines repo, a terminal window and a text editor. echo -e "CONTROLLER_IMAGE_SHA: ${CONTROLLER_IMAGE_SHA}\nREKOR_UUID: ${REKOR_UUID}" ``` - 1. Create additional environment variables - - ```bash - TEKTON_OLD_VERSION=# Example: v0.11.1 - TEKTON_RELEASE_NAME=# The release name you just chose, e.g.: "Ragdoll Norby" - ``` - 1. Execute the Draft Release Pipeline. ```bash diff --git a/tekton/release_names.go b/tekton/release_names.go new file mode 100644 index 00000000000..5a972e33b06 --- /dev/null +++ b/tekton/release_names.go @@ -0,0 +1,237 @@ +/* +Copyright 2025 The Tekton 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. +*/ + +/* +This utility can be used to generate a new release name in the format: + + +to be used for a Tekton Pipelines release. +It looks for cat breeds from CatAPIURL and it parses robot names out +of Wikipedia WikiURL. It filters names that have been used already, +based on the GitHub API GitHubReleasesURL + +To use, run: + go run release_names.go + + +Example output: + { + "release_name": "California Spangled Clank", + "cat_breed_url": "https://en.wikipedia.org/wiki/California_Spangled", + "robot_url": "https://en.wikipedia.org/wiki/Clank" + } +*/ + +package main + +import ( + "context" + "crypto/rand" + "encoding/json" + "errors" + "fmt" + "io" + "math/big" + "net/http" + "regexp" + "strings" +) + +// API Endpoints +const ( + CatAPIURL = "https://api.thecatapi.com/v1/breeds" + RobotWikiURL = "https://en.wikipedia.org/wiki/List_of_fictional_robots_and_androids" + WikiURL = "https://en.wikipedia.org/wiki/" + GitHubReleasesURL = "https://api.github.com/repos/tektoncd/pipeline/releases" +) + +// Structs to hold API responses +type CatBreed struct { + Name string `json:"name"` +} + +type Release struct { + Name string `json:"name"` +} + +func httpGet(url string) (*http.Response, error) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) + if err != nil { + return nil, err + } + return http.DefaultClient.Do(req) +} + +// Fetch cat breeds and organize them by first letter +func getCatBreeds() (map[string][]string, error) { + resp, err := httpGet(CatAPIURL) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var breeds []CatBreed + if err := json.NewDecoder(resp.Body).Decode(&breeds); err != nil { + return nil, err + } + + catDict := make(map[string][]string) + for _, breed := range breeds { + firstLetter := strings.ToUpper(string(breed.Name[0])) + catDict[firstLetter] = append(catDict[firstLetter], breed.Name) + } + + return catDict, nil +} + +// Scrape Wikipedia for robot names +func getRobotNames() (map[string][]string, error) { + resp, err := httpGet(RobotWikiURL) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + robotDict := make(map[string][]string) + + // Regex to extract robot names from
  • Robot Name + re := regexp.MustCompile(`
  • \s*\s*]*>([^<]+)\s*`) + matches := re.FindAllStringSubmatch(string(bodyBytes), -1) + + for _, match := range matches { + if len(match) > 1 { + name := strings.TrimSpace(match[1]) + firstLetter := strings.ToUpper(string(name[0])) + robotDict[firstLetter] = append(robotDict[firstLetter], name) + } + } + + return robotDict, nil +} + +// Fetch past releases from GitHub +func getPastReleases() (map[string]bool, error) { + resp, err := httpGet(GitHubReleasesURL + "?per_page=100") + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var releases []Release + if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil { + return nil, err + } + + pastReleases := make(map[string]bool) + for _, release := range releases { + pastReleases[release.Name] = true + } + + return pastReleases, nil +} + +func randomElement(array []string) (string, error) { + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(array)))) + if err != nil { + return "", err + } + return array[n.Int64()], nil +} + +// Generate a unique release name +func generateUniqueTuple() (string, string, error) { + catBreeds, err := getCatBreeds() + if err != nil { + return "", "", err + } + + robotNames, err := getRobotNames() + if err != nil { + return "", "", err + } + + pastReleases, err := getPastReleases() + if err != nil { + return "", "", err + } + + // Find common letters + commonLetters := []string{} + for letter := range catBreeds { + if _, exists := robotNames[letter]; exists { + commonLetters = append(commonLetters, letter) + } + } + + if len(commonLetters) == 0 { + return "", "", errors.New("no matching names found") + } + + maxAttempts := 10 + for range maxAttempts { + chosenLetter, err := randomElement(commonLetters) + if err != nil { + return "", "", err + } + + cat, err := randomElement(catBreeds[chosenLetter]) + if err != nil { + return "", "", err + } + + robot, err := randomElement(robotNames[chosenLetter]) + if err != nil { + return "", "", err + } + + newName := cat + " " + robot + if !pastReleases[newName] { + return cat, robot, nil + } + } + + return "", "", errors.New("could not generate a unique name after multiple attempts") +} + +func printJsonError(err error) { + fmt.Println(`{"error": "` + err.Error() + `"}`) //nolint:forbidigo +} + +func main() { + cat, robot, err := generateUniqueTuple() + if err != nil { + printJsonError(err) + return + } + + output := map[string]string{ + "release_name": cat + " " + robot, + "cat_breed_url": WikiURL + strings.ReplaceAll(cat, " ", "_"), + "robot_url": WikiURL + strings.ReplaceAll(robot, " ", "_"), + } + + jsonOutput, err := json.MarshalIndent(output, "", " ") + if err != nil { + printJsonError(err) + return + } + fmt.Println(string(jsonOutput)) //nolint:forbidigo +}