Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a tool to generate the release name #8629

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 37 additions & 23 deletions tekton/release-cheat-sheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<version number>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-<version number>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 <<EOF > 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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
237 changes: 237 additions & 0 deletions tekton/release_names.go
Original file line number Diff line number Diff line change
@@ -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:
<cat breed> <robot name>

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 <li><b>Robot Name</b>
re := regexp.MustCompile(`<li>\s*<b>\s*<a[^>]*>([^<]+)</a>\s*</b>`)
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
}
Loading