From b8cace88e1db26724c30a4f9bce9e2be97f9937f Mon Sep 17 00:00:00 2001 From: Nawaz Hussain Khazielakha Date: Tue, 12 Mar 2024 13:48:15 -0700 Subject: [PATCH] add tests and update provider_issues.go --- .../internal/update_providers/README.md | 6 +- .../update_providers/provider_issues.go | 86 +++++++++--- .../update_providers/provider_issues_test.go | 125 ++++++++++++++++++ 3 files changed, 194 insertions(+), 23 deletions(-) create mode 100644 hack/tools/release/internal/update_providers/provider_issues_test.go diff --git a/hack/tools/release/internal/update_providers/README.md b/hack/tools/release/internal/update_providers/README.md index 80b52fdb646f..a67565b307d4 100644 --- a/hack/tools/release/internal/update_providers/README.md +++ b/hack/tools/release/internal/update_providers/README.md @@ -15,14 +15,14 @@ export PROVIDER_ISSUES_DRY_RUN="true" ``` -- Export `RELEASE_TAG` environment variable to the CAPI release version e.g. `1.6.0`. The suffix `-beta.0` is appended by the utility. +- Export `RELEASE_TAG` environment variable to the CAPI release version e.g. `v1.7.0-beta.0`. Example: ```sh - export RELEASE_TAG="1.6.0" + export RELEASE_TAG="v1.7.0-beta.0" ``` -- Export `RELEASE_DATE` to the targeted CAPI release version date. Fetch the target date from latest [release file](https://github.com/kubernetes-sigs/cluster-api/tree/main/docs/release/releases). +- Export `RELEASE_DATE` to the targeted CAPI release version date. Fetch the target date from latest [release file](https://github.com/kubernetes-sigs/cluster-api/tree/main/docs/release/releases). The `RELEASE_DATE` should be in the format `YYYY-MM-DD`. Example: ```sh diff --git a/hack/tools/release/internal/update_providers/provider_issues.go b/hack/tools/release/internal/update_providers/provider_issues.go index c76f6e91fe83..62ba18184069 100644 --- a/hack/tools/release/internal/update_providers/provider_issues.go +++ b/hack/tools/release/internal/update_providers/provider_issues.go @@ -59,6 +59,28 @@ var ( } ) +// Exiter is a simple interface to exit the program. +type Exiter interface { + Exit(code int) +} + +// RealExiter is the real implementation of the Exiter interface. +type RealExiter struct{} + +// Exit exits the program with the given code. +func (re *RealExiter) Exit(code int) { + os.Exit(code) +} + +// ProviderIssues is a struct that contains the Exiter interface. +type ProviderIssues struct { + Exiter Exiter +} + +func newProviderIssues() *ProviderIssues { + return &ProviderIssues{Exiter: &RealExiter{}} +} + // Issue is the struct for the issue. type Issue struct { Title string `json:"title"` @@ -72,15 +94,16 @@ type IssueResponse struct { // releaseDetails is the struct for the release details. type releaseDetails struct { - ReleaseTag string - BetaTag string - ReleaseLink string - ReleaseDate string + ReleaseTag string + BetaTag string + ReleaseLink string + ReleaseDate string + ReleaseNotesLink string } // Example command: // -// GITHUB_ISSUE_OPENER_TOKEN="fake" RELEASE_TAG="1.6.0" RELEASE_DATE="2023-11-28" PROVIDER_ISSUES_DRY_RUN="true" make release-provider-issues-tool +// GITHUB_ISSUE_OPENER_TOKEN="fake" RELEASE_TAG="v1.6.0-beta.0" RELEASE_DATE="2023-11-28" PROVIDER_ISSUES_DRY_RUN="true" make release-provider-issues-tool func main() { githubToken, keySet := os.LookupEnv("GITHUB_ISSUE_OPENER_TOKEN") if !keySet || githubToken == "" { @@ -108,7 +131,9 @@ func main() { fmt.Println("-", strings.Join(repoList, "\n- ")) fmt.Printf("\n") - details := getReleaseDetails() + // get release details + m := newProviderIssues() + details := m.getReleaseDetails() // generate title titleBuffer := bytes.NewBuffer([]byte{}) @@ -249,42 +274,63 @@ func continueOrAbort() { } // getReleaseDetails returns the release details from the environment variables. -func getReleaseDetails() releaseDetails { +func (m *ProviderIssues) getReleaseDetails() releaseDetails { + // Parse the release tag releaseSemVer, keySet := os.LookupEnv("RELEASE_TAG") if !keySet || releaseSemVer == "" { fmt.Println("RELEASE_TAG is a required environmental variable.") fmt.Println("Refer to README.md in folder for more information.") - os.Exit(1) + m.Exiter.Exit(1) } - match, err := regexp.Match("\\d\\.\\d\\.\\d", []byte(releaseSemVer)) + // allow patterns like v1.7.0-beta.0 + pattern := `^v\d+\.\d+\.\d+-beta\.\d+$` + match, err := regexp.MatchString(pattern, releaseSemVer) if err != nil || !match { - fmt.Println("RELEASE_TAG must be in format `\\d\\.\\d\\.\\d` e.g. 1.5") - os.Exit(1) + fmt.Println("RELEASE_TAG must be in format `^v\\d+\\.\\d+\\.\\d+-beta\\.\\d+$` e.g. v1.7.0-beta.0") + m.Exiter.Exit(1) } + major, minor, patch := "", "", "" + majorMinorPatchPattern := `v(\d+)\.(\d+)\.(\d+)` + re := regexp.MustCompile(majorMinorPatchPattern) + releaseSemVerMatch := re.FindStringSubmatch(releaseSemVer) + if len(releaseSemVerMatch) > 3 { + major = releaseSemVerMatch[1] + minor = releaseSemVerMatch[2] + patch = releaseSemVerMatch[3] + } else { + fmt.Println("RELEASE_TAG contains invalid Major.Minor.Patch SemVer. It must be in format v(\\d+)\\.(\\d+)\\.(\\d+) e.g. v1.7.0") + m.Exiter.Exit(1) + } + + // Parse the release date releaseDate, keySet := os.LookupEnv("RELEASE_DATE") if !keySet || releaseDate == "" { fmt.Println("RELEASE_DATE is a required environmental variable.") fmt.Println("Refer to README.md in folder for more information.") - os.Exit(1) + m.Exiter.Exit(1) } formattedReleaseDate, err := formatDate(releaseDate) if err != nil { fmt.Println("Unable to parse the date.", err) fmt.Println("Refer to README.md in folder for more information.") + m.Exiter.Exit(1) } - releaseTag := fmt.Sprintf("v%s", releaseSemVer) - betaTag := fmt.Sprintf("v%s%s", releaseSemVer, "-beta.0") - releaseLink := fmt.Sprintf("https://github.com/kubernetes-sigs/cluster-api/tree/main/docs/release/releases/release-%s.md#timeline", releaseSemVer) + majorMinorWithoutPrefixV := fmt.Sprintf("%s.%s", major, minor) // e.g. 1.7 . Note that there is no "v" in the majorMinor + releaseTag := fmt.Sprintf("v%s.%s.%s", major, minor, patch) // e.g. v1.7.0 + betaTag := fmt.Sprintf("%s%s", releaseTag, "-beta.0") // e.g. v1.7.0-beta.0 + releaseLink := fmt.Sprintf("https://github.com/kubernetes-sigs/cluster-api/tree/main/docs/release/releases/release-%s.md#timeline", majorMinorWithoutPrefixV) + releaseNotesLink := fmt.Sprintf("https://github.com/kubernetes-sigs/cluster-api/releases/tag/%s", betaTag) return releaseDetails{ - ReleaseDate: formattedReleaseDate, - ReleaseTag: releaseTag, - BetaTag: betaTag, - ReleaseLink: releaseLink, + ReleaseDate: formattedReleaseDate, + ReleaseTag: releaseTag, + BetaTag: betaTag, + ReleaseLink: releaseLink, + ReleaseNotesLink: releaseNotesLink, } } @@ -324,7 +370,7 @@ Looking forward to your feedback before {{.ReleaseTag}} release! ## For quick reference -- [CAPI {{.BetaTag}} release notes](https://github.com/kubernetes-sigs/cluster-api/releases/tag/{{.BetaTag}}) +- [CAPI {{.BetaTag}} release notes]({{.ReleaseNotesLink}}) - [Shortcut to CAPI git issues](https://github.com/kubernetes-sigs/cluster-api/issues) ## Following are the planned dates for the upcoming releases diff --git a/hack/tools/release/internal/update_providers/provider_issues_test.go b/hack/tools/release/internal/update_providers/provider_issues_test.go new file mode 100644 index 000000000000..477bbe704101 --- /dev/null +++ b/hack/tools/release/internal/update_providers/provider_issues_test.go @@ -0,0 +1,125 @@ +//go:build tools +// +build tools + +/* +Copyright 2022 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 main + +import ( + "os" + "testing" + + . "github.com/onsi/gomega" +) + +// MockExiter is a mock implementation of the Exiter interface. +type MockExiter struct { + ExitCalled bool + ExitCode int +} + +// Exit is a mock implementation of the Exiter interface. +func (me *MockExiter) Exit(code int) { + me.ExitCalled = true + me.ExitCode = code +} + +func Test_GetReleaseDetails(t *testing.T) { + tests := []struct { + name string + releaseTag string + releaseDate string + want releaseDetails + expectExit bool + }{ + { + name: "Correct RELEASE_TAG and RELEASE_DATE are set", + releaseTag: "v1.7.0-beta.0", + releaseDate: "2024-04-16", + want: releaseDetails{ + ReleaseDate: "Tuesday, 16th April 2024", + ReleaseTag: "v1.7.0", + BetaTag: "v1.7.0-beta.0", + ReleaseLink: "https://github.com/kubernetes-sigs/cluster-api/tree/main/docs/release/releases/release-1.7.md#timeline", + ReleaseNotesLink: "https://github.com/kubernetes-sigs/cluster-api/releases/tag/v1.7.0-beta.0", + }, + expectExit: false, + }, + { + name: "RELEASE_TAG is not in the format ^v\\d+\\.\\d+\\.\\d+-beta\\.\\d+$", + releaseTag: "v1.7.0.1", + releaseDate: "2024-04-16", + expectExit: true, + }, + { + name: "RELEASE_TAG does not have prefix 'v' in its semver", + releaseTag: "1.7.0-beta.0", + releaseDate: "2024-04-16", + expectExit: true, + }, + { + name: "RELEASE_TAG contains invalid Major.Minor.Patch SemVer", + releaseTag: "v1.x.0-beta.0", + releaseDate: "2024-04-16", + expectExit: true, + }, + { + name: "invalid yyyy-dd-mm RELEASE_DATE entered", + releaseTag: "v1.7.0-beta.0", + releaseDate: "2024-16-4", + expectExit: true, + }, + { + name: "invalid yyyy/dd/mm RELEASE_DATE entered", + releaseTag: "v1.7.0-beta.0", + releaseDate: "2024/16/4", + expectExit: true, + }, + { + name: "invalid yyyy/mm/dd RELEASE_DATE entered", + releaseTag: "v1.7.0-beta.0", + releaseDate: "2024/4/16", + expectExit: true, + }, + } + + for _, tt := range tests { + mockExiter := &MockExiter{} + m := &ProviderIssues{Exiter: mockExiter} + + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + _ = os.Setenv("RELEASE_TAG", tt.releaseTag) + _ = os.Setenv("RELEASE_DATE", tt.releaseDate) + + got := m.getReleaseDetails() + if tt.expectExit { + g.Expect(mockExiter.ExitCalled).To(BeTrue()) + g.Expect(mockExiter.ExitCode).To(Equal(1)) + } else { + g.Expect(mockExiter.ExitCalled).To(BeFalse()) + g.Expect(got.ReleaseDate).To(Equal(tt.want.ReleaseDate)) + g.Expect(got.ReleaseTag).To(Equal(tt.want.ReleaseTag)) + g.Expect(got.BetaTag).To(Equal(tt.want.BetaTag)) + g.Expect(got.ReleaseLink).To(Equal(tt.want.ReleaseLink)) + } + _ = os.Unsetenv("RELEASE_TAG") + _ = os.Unsetenv("RELEASE_DATE") + }) + } +}