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 the generate release announcement message command #483

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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
87 changes: 70 additions & 17 deletions cmd/release/cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,28 @@ var (
k3sPrevMilestone string
k3sMilestone string

concurrencyLimit int
imagesListURL string
ignoreImages []string
checkImages []string
registry string
rancherMissingImagesJSONOutput bool
rke2PrevMilestone string
rke2Milestone string
rancherArtifactsIndexWriteToPath string
rancherArtifactsIndexIgnoreVersions []string
rancherImagesDigestsOutputFile string
rancherImagesDigestsRegistry string
rancherImagesDigestsImagesURL string
rancherSyncImages []string
rancherSourceRegistry string
rancherTargetRegistry string
rancherSyncConfigOutputPath string
concurrencyLimit int
imagesListURL string
ignoreImages []string
checkImages []string
registry string
rancherMissingImagesJSONOutput bool
rke2PrevMilestone string
rke2Milestone string
rancherArtifactsIndexWriteToPath string
rancherArtifactsIndexIgnoreVersions []string
rancherImagesDigestsOutputFile string
rancherImagesDigestsRegistry string
rancherImagesDigestsImagesURL string
rancherSyncImages []string
rancherSourceRegistry string
rancherTargetRegistry string
rancherSyncConfigOutputPath string
rancherReleaseAnnouncementTag string
rancherReleaseAnnouncementPreviousTag string
rancherReleaseAnnouncementActionRunID string
rancherReleaseAnnouncementPrimeOnly bool
rancherReleaseAnnouncementFinalRC bool
)

// generateCmd represents the generate command
Expand Down Expand Up @@ -172,6 +177,34 @@ var rancherGenerateImagesSyncConfigSubCmd = &cobra.Command{
},
}

var rancherGenerateReleaseMessageSubCmd = &cobra.Command{
Use: "release-message",
Short: "Generate the release announcement message",
RunE: func(cmd *cobra.Command, args []string) error {
versionKey := rancherReleaseAnnouncementTag

// strip the pre release suffix (v2.9.2-alpha1 -> v2.9.2)
if strings.ContainsRune(rancherReleaseAnnouncementTag, '-') {
versionKey = strings.Split(rancherReleaseAnnouncementTag, "-")[0]
}

rancherRelease, found := rootConfig.Rancher.Versions[versionKey]
if !found {
return errors.New("verify your config file, version not found: " + versionKey)
}

ctx := context.Background()
client := repository.NewGithub(ctx, rootConfig.Auth.GithubToken)

message, err := rancher.GenerateAnnounceReleaseMessage(ctx, client, rancherReleaseAnnouncementTag, rancherReleaseAnnouncementPreviousTag, rancherRelease.RancherRepoOwner, rancherReleaseAnnouncementActionRunID, rancherReleaseAnnouncementPrimeOnly, rancherReleaseAnnouncementFinalRC)
if err != nil {
return err
}
fmt.Println(message)
return nil
},
}

func init() {
rootCmd.AddCommand(generateCmd)

Expand All @@ -182,6 +215,7 @@ func init() {
rancherGenerateSubCmd.AddCommand(rancherGenerateMissingImagesListSubCmd)
rancherGenerateSubCmd.AddCommand(rancherGenerateDockerImagesDigestsSubCmd)
rancherGenerateSubCmd.AddCommand(rancherGenerateImagesSyncConfigSubCmd)
rancherGenerateSubCmd.AddCommand(rancherGenerateReleaseMessageSubCmd)

generateCmd.AddCommand(k3sGenerateSubCmd)
generateCmd.AddCommand(rke2GenerateSubCmd)
Expand Down Expand Up @@ -260,4 +294,23 @@ func init() {
fmt.Println(err.Error())
os.Exit(1)
}

// rancher generate release-message
rancherGenerateReleaseMessageSubCmd.Flags().StringVarP(&rancherReleaseAnnouncementTag, "tag", "t", "", "Tag that will be announced")
rancherGenerateReleaseMessageSubCmd.Flags().StringVarP(&rancherReleaseAnnouncementPreviousTag, "previous-tag", "p", "", "Last tag before the current one")
rancherGenerateReleaseMessageSubCmd.Flags().StringVarP(&rancherReleaseAnnouncementActionRunID, "action-run-id", "a", "", "Run ID for the latest push-release.yml action")
rancherGenerateReleaseMessageSubCmd.Flags().BoolVarP(&rancherReleaseAnnouncementPrimeOnly, "prime-only", "o", false, "Version is prime-only and the artifacts are at prime.ribs.rancher.io")
rancherGenerateReleaseMessageSubCmd.Flags().BoolVarP(&rancherReleaseAnnouncementFinalRC, "final-rc", "f", false, "Version is the final RC, the announce message won't contain images or components with RC")
if err := rancherGenerateReleaseMessageSubCmd.MarkFlagRequired("tag"); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
if err := rancherGenerateReleaseMessageSubCmd.MarkFlagRequired("previous-tag"); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
if err := rancherGenerateReleaseMessageSubCmd.MarkFlagRequired("action-run-id"); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
}
166 changes: 163 additions & 3 deletions release/rancher/rancher.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,18 @@ type regsyncSync struct {
Tags regsyncTags `yaml:"tags"`
}

type releaseAnnnouncement struct {
Tag string
PreviousTag string
RancherRepoOwner string
CommitSHA string
ActionRunID string
ImagesWithRC []string
ComponentsWithRC []string
UIVersion string
CLIVersion string
}

func listS3Objects(ctx context.Context, s3Client *s3.Client, bucketName string, prefix string) ([]string, error) {
var keys []string
var continuationToken *string
Expand Down Expand Up @@ -446,7 +458,7 @@ func GenerateMissingImagesList(imagesListURL, registry string, concurrencyLimit
if imagesListURL == "" {
return nil, errors.New("if no images are provided, an images list URL must be provided")
}
rancherImages, err := rancherPrimeArtifact(imagesListURL)
rancherImages, err := remoteTextFileToSlice(imagesListURL)
if err != nil {
return nil, errors.New("failed to get rancher images: " + err.Error())
}
Expand Down Expand Up @@ -650,6 +662,135 @@ func GenerateDockerImageDigests(outputFile, imagesFileURL, registry string, verb
return createAssetFile(outputFile, imagesDigests)
}

func GenerateAnnounceReleaseMessage(ctx context.Context, ghClient *github.Client, tag, previousTag, rancherRepoOwner, actionRunID string, primeOnly, finalRC bool) (string, error) {
ref, _, err := ghClient.Git.GetRef(ctx, rancherRepoOwner, rancherRepo, "refs/tags/"+tag)
if err != nil {
return "", err
}
if ref.Object.SHA == nil {
return "", errors.New("release commit sha is nil")
}

commitSHA := ref.Object.GetSHA()

r := releaseAnnnouncement{
Tag: tag,
PreviousTag: previousTag,
RancherRepoOwner: rancherRepoOwner,
CommitSHA: commitSHA,
ActionRunID: actionRunID,
}

announceTemplate := announceReleaseFinalRCTemplate

if finalRC {
dockerfileURL := "https://github.com/raw/" + rancherRepoOwner + "/rancher/" + commitSHA + "/package/Dockerfile"
dockerfile, err := remoteTextFileToSlice(dockerfileURL)
if err != nil {
return "", err
}
uiVersion, cliVersion, err := rancherUICLIVersions(dockerfile)
if err != nil {
return "", err
}
r.UIVersion = uiVersion
r.CLIVersion = cliVersion
} else { // every alpha and rc before the final RC
announceTemplate = announceReleasePreReleaseTemplate

componentsURL := "https://github.com/" + rancherRepoOwner + "/rancher/releases/download/" + tag + "/rancher-components.txt"
if primeOnly {
componentsURL = rancherArtifactsBaseURL + "/rancher/" + tag + "/rancher-components.txt"
}
rancherComponents, err := remoteTextFileToSlice(componentsURL)
if err != nil {
return "", err
}

imagesWithRC, componentsWithRC, err := rancherImagesComponentsWithRC(rancherComponents)
if err != nil {
return "", err
}
r.ImagesWithRC = imagesWithRC
r.ComponentsWithRC = componentsWithRC
}

tmpl := template.New("announce-release")
tmpl, err = tmpl.Parse(announceTemplate)
if err != nil {
return "", errors.New("failed to parse announce template: " + err.Error())
}
buff := bytes.NewBuffer(nil)
if err := tmpl.ExecuteTemplate(buff, "announceRelease", r); err != nil {
return "", err
}
return buff.String(), nil
}

// rancherUICLIVersions scans a dockerfile line by line and returns the ui and cli versions, or an error if any of them are not found
func rancherUICLIVersions(dockerfile []string) (string, string, error) {
var uiVersion string
var cliVersion string
for _, line := range dockerfile {
if strings.Contains(line, "ENV CATTLE_UI_VERSION ") {
uiVersion = strings.TrimPrefix(line, "ENV CATTLE_UI_VERSION ")
continue
}
if strings.Contains(line, "ENV CATTLE_CLI_VERSION ") {
cliVersion = strings.TrimPrefix(line, "ENV CATTLE_CLI_VERSION ")
continue
}
if len(uiVersion) > 0 && len(cliVersion) > 0 {
break
}
}
if uiVersion == "" || cliVersion == "" {
return "", "", errors.New("missing ui or cli version")
}
return uiVersion, cliVersion, nil
}

// rancherImagesComponentsWithRC scans the rancher-components.txt file content and returns images and components, or an error
func rancherImagesComponentsWithRC(rancherComponents []string) ([]string, []string, error) {
if len(rancherComponents) < 2 {
return nil, nil, errors.New("rancher-components.txt should have at least two lines (images and components headers)")
}
images := make([]string, 0)
components := make([]string, 0)

var isImage bool
for _, line := range rancherComponents {
// always skip empty lines
if line == "" || line == " " {
continue
}

// if a line contains # it is a header for a section
isHeader := strings.Contains(line, "#")

if isHeader {
imagesHeader := strings.Contains(line, "Images")
componentsHeader := strings.Contains(line, "Components")
// if it's a header, but not for images or components, ignore it and everything else after it
if !imagesHeader && !componentsHeader {
break
}
// isImage's value will persist between iterations
// if imagesHeader is true, it means that all following lines are images
// if it's false, it means that all following images are components
isImage = imagesHeader
continue
}

if isImage {
images = append(images, line)
} else {
components = append(components, line)
}
}
return images, components, nil
}

func dockerImagesDigests(imagesFileURL, registry string) (imageDigest, error) {
imagesList, err := artifactImageList(imagesFileURL, registry)
if err != nil {
Expand Down Expand Up @@ -825,7 +966,7 @@ func registryAuth(authURL, service, image string) (string, error) {
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return "", errors.New("expected status code to be 200, got: " + strconv.Itoa(res.StatusCode))
return "", errors.New("expected status code to be 200, got: " + res.Status)
}

var auth registryAuthToken
Expand All @@ -836,12 +977,15 @@ func registryAuth(authURL, service, image string) (string, error) {
return auth.Token, nil
}

func rancherPrimeArtifact(url string) ([]string, error) {
func remoteTextFileToSlice(url string) ([]string, error) {
httpClient := ecmHTTP.NewClient(time.Second * 15)
res, err := httpClient.Get(url)
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
tashima42 marked this conversation as resolved.
Show resolved Hide resolved
return nil, errors.New("expected status code to be 200, got: " + res.Status)
}
defer res.Body.Close()

var file []string
Expand Down Expand Up @@ -958,3 +1102,19 @@ const checkRancherRCDepsTemplate = `{{- define "componentsFile" -}}
* {{ .Content }} ({{ .File }}, line {{ .Line }})
{{- end}}
{{ end }}`

const announceReleaseHeaderTemplate = "`{{ .Tag }}` is available based on this commit ([link](https://github.com/{{ .RancherRepoOwner }}/rancher/commit/{{ .CommitSHA }}))!\n" +
"* Link of commits between last 2 RCs. ([link](https://github.com/{{ .RancherRepoOwner }}/rancher/compare/{{ .PreviousTag }}...{{ .Tag }}))\n" +
"* Completed GHA build ([link](https://github.com/{{ .RancherRepoOwner }}/rancher/actions/runs/{{ .ActionRunID }})).\n"

const announceReleasePreReleaseTemplate = `{{ define "announceRelease" }}` + announceReleaseHeaderTemplate +
"* Images with -rc:\n" +
"{{ range .ImagesWithRC }}" +
" * {{ . }}\n{{ end }}" +
"* Components with -rc:\n" +
"{{ range .ComponentsWithRC }}" +
" * {{ . }}\n{{ end }}{{ end }}"

const announceReleaseFinalRCTemplate = `{{ define "announceRelease" }}` + announceReleaseHeaderTemplate +
"* UI Version: `{{ .UIVersion }}`\n" +
"* CLI Version: `{{ .CLIVersion }}`{{ end }}"
66 changes: 65 additions & 1 deletion release/rancher/rancher_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package rancher

import "testing"
import (
"testing"
)

const (
rancherRepoImage = "rancher/rancher"
Expand Down Expand Up @@ -86,3 +88,65 @@ func TestGenerateRegsyncConfig(t *testing.T) {
t.Error("rancher agent image should be: '" + sourceRancherAgentImage + "' instead, got: '" + config.Sync[1].Source + "'")
}
}

func TestRancherUICLIVersions(t *testing.T) {
ui := "2.9.2-alpha3"
cli := "v2.9.0"
dockerfile := []string{
"empty line",
"ENV CATTLE_UI_VERSION " + ui,
"ENV CATTLE_DASHBOARD_UI_VERSION v2.9.2-alpha3",
"ENV CATTLE_CLI_VERSION " + cli,
"",
"another empty line",
}
uiVersion, cliVersion, err := rancherUICLIVersions(dockerfile)
if err != nil {
t.Error(err)
}
if uiVersion != ui {
t.Error("wrong ui version, expected '" + ui + "', instead, got: " + uiVersion)
}
if cliVersion != cli {
t.Error("wrong cli version, expected '" + cli + "', instead, got: " + cliVersion)
}
}

func TestRancherImagesComponentsWithRC(t *testing.T) {
cisOperatorImage := "rancher/cis-operator v1.0.15-rc.2"
fleetImage := "rancher/fleet v0.9.9-rc.1"
systemAgentComponent := "SYSTEM_AGENT_VERSION v0.3.9-rc.4"
winsAgentComponent := "WINS_AGENT_VERSION v0.4.18-rc1"

rancherComponents := []string{
"# Images with -rc",
cisOperatorImage,
fleetImage,
"# Components with -rc",
systemAgentComponent,
winsAgentComponent,
"",
"# Min version components with -rc",
"",
"# Chart/KDM sources",
"* SYSTEM_CHART_DEFAULT_BRANCH: dev-v2.8 (`scripts/package-env`)",
}

images, components, err := rancherImagesComponentsWithRC(rancherComponents)
if err != nil {
t.Error(err)
}

if images[0] != cisOperatorImage {
t.Error("image mismatch, expected '" + cisOperatorImage + "', instead, got: " + images[0])
}
if images[1] != fleetImage {
t.Error("image mismatch, expected '" + fleetImage + "', instead, got: " + images[1])
}
if components[0] != systemAgentComponent {
t.Error("image mismatch, expected '" + systemAgentComponent + "', instead, got: " + components[0])
}
if components[1] != winsAgentComponent {
t.Error("image mismatch, expected '" + winsAgentComponent + "', instead, got: " + components[1])
}
}
Loading