diff --git a/hack/tools/release/internal/update_providers/provider_issues.go b/hack/tools/release/internal/update_providers/provider_issues.go index e8cdd4f013cc..b46d7a953162 100644 --- a/hack/tools/release/internal/update_providers/provider_issues.go +++ b/hack/tools/release/internal/update_providers/provider_issues.go @@ -75,7 +75,7 @@ type IssueResponse struct { // releaseDetails is the struct for the release details. type releaseDetails struct { ReleaseTag string - BetaTag string + PreReleaseTag string ReleaseLink string ReleaseDate string ReleaseNotesLink string @@ -83,7 +83,9 @@ type releaseDetails struct { // Example command: // +// GITHUB_ISSUE_OPENER_TOKEN="fake" RELEASE_TAG="v1.6.0-alpha.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 +// GITHUB_ISSUE_OPENER_TOKEN="fake" RELEASE_TAG="v1.6.0-rc.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 == "" { @@ -264,11 +266,11 @@ func getReleaseDetails() (releaseDetails, error) { return releaseDetails{}, errors.New("release tag is a required. Refer to README.md in folder for more information") } - // allow patterns like v1.7.0-beta.0 - pattern := `^v\d+\.\d+\.\d+-beta\.\d+$` + // allow patterns like v1.7.0-alpha.0, v1.7.0-beta.0, v1.7.0-rc.0 + pattern := `^v\d+\.\d+\.\d+-(alpha|beta|rc)\.\d+$` match, err := regexp.MatchString(pattern, releaseSemVer) if err != nil || !match { - return releaseDetails{}, errors.New("release tag must be in format `^v\\d+\\.\\d+\\.\\d+-beta\\.\\d+$` e.g. v1.7.0-beta.0") + return releaseDetails{}, errors.New("release tag must be in format `^v\\d+\\.\\d+\\.\\d+-(alpha|beta|rc)\\.\\d+$` e.g. v1.7.0-beta.0") } major, minor, patch := "", "", "" @@ -296,14 +298,13 @@ func getReleaseDetails() (releaseDetails, error) { 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) + releaseNotesLink := fmt.Sprintf("https://github.com/kubernetes-sigs/cluster-api/releases/tag/%s", releaseSemVer) return releaseDetails{ ReleaseDate: formattedReleaseDate, ReleaseTag: releaseTag, - BetaTag: betaTag, + PreReleaseTag: releaseSemVer, ReleaseLink: releaseLink, ReleaseNotesLink: releaseNotesLink, }, nil @@ -339,13 +340,13 @@ func formatDate(inputDate string) (string, error) { func getIssueBody() *template.Template { // do not indent the body // indenting the body will result in the body being posted as a code snippet - issueBody, err := template.New("issue").Parse(`CAPI {{.BetaTag}} has been released and is ready for testing. + issueBody, err := template.New("issue").Parse(`CAPI {{.PreReleaseTag}} has been released and is ready for testing. Looking forward to your feedback before {{.ReleaseTag}} release! ## For quick reference -- [CAPI {{.BetaTag}} release notes]({{.ReleaseNotesLink}}) +- [CAPI {{.PreReleaseTag}} 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 @@ -365,7 +366,7 @@ More details of the upcoming schedule can be seen at [CAPI {{.ReleaseTag}} relea // getIssueTitle returns the issue title template. func getIssueTitle() *template.Template { - issueTitle, err := template.New("title").Parse(`CAPI {{.BetaTag}} has been released and is ready for testing`) + issueTitle, err := template.New("title").Parse(`CAPI {{.PreReleaseTag}} has been released and is ready for testing`) if err != nil { panic(err) } diff --git a/hack/tools/release/internal/update_providers/provider_issues_test.go b/hack/tools/release/internal/update_providers/provider_issues_test.go index 8629f3e6a238..50302993bb89 100644 --- a/hack/tools/release/internal/update_providers/provider_issues_test.go +++ b/hack/tools/release/internal/update_providers/provider_issues_test.go @@ -35,38 +35,71 @@ func Test_GetReleaseDetails(t *testing.T) { err string }{ { - name: "Correct RELEASE_TAG and RELEASE_DATE are set", + name: "Correct RELEASE_TAG and RELEASE_DATE are set for alpha", + releaseTag: "v1.7.0-alpha.0", + releaseDate: "2024-04-16", + want: releaseDetails{ + ReleaseDate: "Tuesday, 16th April 2024", + ReleaseTag: "v1.7.0", + PreReleaseTag: "v1.7.0-alpha.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-alpha.0", + }, + expectErr: false, + }, + { + name: "Correct RELEASE_TAG and RELEASE_DATE are set for beta", 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", + PreReleaseTag: "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", }, expectErr: false, }, { - name: "RELEASE_TAG is not in the format ^v\\d+\\.\\d+\\.\\d+-beta\\.\\d+$", + name: "Correct RELEASE_TAG and RELEASE_DATE are set for rc", + releaseTag: "v1.7.0-rc.0", + releaseDate: "2024-04-16", + want: releaseDetails{ + ReleaseDate: "Tuesday, 16th April 2024", + ReleaseTag: "v1.7.0", + PreReleaseTag: "v1.7.0-rc.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-rc.0", + }, + expectErr: false, + }, + { + name: "RELEASE_TAG is not in the correct format", releaseTag: "v1.7.0.1", releaseDate: "2024-04-16", expectErr: true, - err: "release tag must be in format `^v\\d+\\.\\d+\\.\\d+-beta\\.\\d+$` e.g. v1.7.0-beta.0", + err: "release tag must be in format `^v\\d+\\.\\d+\\.\\d+-(alpha|beta|rc)\\.\\d+$` e.g. v1.7.0-beta.0", }, { name: "RELEASE_TAG does not have prefix 'v' in its semver", releaseTag: "1.7.0-beta.0", releaseDate: "2024-04-16", expectErr: true, - err: "release tag must be in format `^v\\d+\\.\\d+\\.\\d+-beta\\.\\d+$` e.g. v1.7.0-beta.0", + err: "release tag must be in format `^v\\d+\\.\\d+\\.\\d+-(alpha|beta|rc)\\.\\d+$` e.g. v1.7.0-beta.0", }, { name: "RELEASE_TAG contains invalid Major.Minor.Patch SemVer", releaseTag: "v1.x.0-beta.0", releaseDate: "2024-04-16", expectErr: true, - err: "release tag must be in format `^v\\d+\\.\\d+\\.\\d+-beta\\.\\d+$` e.g. v1.7.0-beta.0", + err: "release tag must be in format `^v\\d+\\.\\d+\\.\\d+-(alpha|beta|rc)\\.\\d+$` e.g. v1.7.0-beta.0", + }, + { + name: "RELEASE_TAG contains unsupported pre-release type", + releaseTag: "v1.7.0-gamma.0", + releaseDate: "2024-04-16", + expectErr: true, + err: "release tag must be in format `^v\\d+\\.\\d+\\.\\d+-(alpha|beta|rc)\\.\\d+$` e.g. v1.7.0-beta.0", }, { name: "invalid yyyy-dd-mm RELEASE_DATE entered", @@ -104,7 +137,7 @@ func Test_GetReleaseDetails(t *testing.T) { } else { 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.PreReleaseTag).To(Equal(tt.want.PreReleaseTag)) g.Expect(got.ReleaseLink).To(Equal(tt.want.ReleaseLink)) } }) diff --git a/hack/tools/release/notes/main.go b/hack/tools/release/notes/main.go index 5815791bd68a..4d81d60a3343 100644 --- a/hack/tools/release/notes/main.go +++ b/hack/tools/release/notes/main.go @@ -25,6 +25,7 @@ import ( "fmt" "log" "os/exec" + "strings" "github.com/blang/semver/v4" "github.com/pkg/errors" @@ -42,6 +43,11 @@ const ( alphaRelease = "ALPHA RELEASE" betaRelease = "BETA RELEASE" releaseCandidate = "RELEASE CANDIDATE" + + // Pre-release type constants. + preReleaseAlpha = "alpha" + preReleaseBeta = "beta" + preReleaseRC = "rc" ) func main() { @@ -140,15 +146,15 @@ func releaseTypeFromNewTag(newTagConfig string) string { return "" } - // Only allow RC and beta releases. More types must be defined here. + // Only allow alpha, beta and rc releases. More types must be defined here. // If a new type is not defined, no warning banner will be printed. switch newTag.Pre[0].VersionStr { - case "rc": - return releaseCandidate - case "beta": - return betaRelease - case "alpha": + case preReleaseAlpha: return alphaRelease + case preReleaseBeta: + return betaRelease + case preReleaseRC: + return releaseCandidate } return "" } @@ -187,6 +193,45 @@ func validateConfig(config *notesCmdConfig) error { } } + if config.previousReleaseVersion != "" { + if err := validatePreviousReleaseVersion(config.previousReleaseVersion); err != nil { + return err + } + } + + return nil +} + +func validatePreviousReleaseVersion(previousReleaseVersion string) error { + // Extract version string from ref format (e.g. "tags/v1.0.0-rc.1" -> "v1.0.0-rc.1") + if !strings.Contains(previousReleaseVersion, "/") { + return errors.New("--previous-release-version must be in ref format (e.g. tags/v1.0.0-rc.1)") + } + + parts := strings.SplitN(previousReleaseVersion, "/", 2) + if len(parts) != 2 { + return errors.New("--previous-release-version must be in ref format (e.g. tags/v1.0.0-rc.1)") + } + + versionStr := parts[1] + + // Parse the version to check if it contains alpha, beta, or rc + version, err := semver.ParseTolerant(versionStr) + if err != nil { + return errors.Wrap(err, "invalid --previous-release-version, is not a valid semver") + } + + // Check if the version has pre-release identifiers + if len(version.Pre) == 0 { + return errors.Errorf("--previous-release-version must contain '%s', '%s', or '%s' pre-release identifier", preReleaseAlpha, preReleaseBeta, preReleaseRC) + } + + // Check if the first pre-release identifier is 'alpha', 'beta', or 'rc' + preReleaseType := version.Pre[0].VersionStr + if preReleaseType != preReleaseAlpha && preReleaseType != preReleaseBeta && preReleaseType != preReleaseRC { + return errors.Errorf("--previous-release-version must contain '%s', '%s', or '%s' pre-release identifier", preReleaseAlpha, preReleaseBeta, preReleaseRC) + } + return nil } diff --git a/hack/tools/release/notes/main_test.go b/hack/tools/release/notes/main_test.go index ab279d43a9c1..fda8b933bd0e 100644 --- a/hack/tools/release/notes/main_test.go +++ b/hack/tools/release/notes/main_test.go @@ -20,6 +20,7 @@ limitations under the License. package main import ( + "strings" "testing" "github.com/blang/semver/v4" @@ -229,12 +230,86 @@ func Test_validateConfig(t *testing.T) { }, wantErr: false, }, + { + name: "Invalid previousReleaseVersion without ref format", + args: ¬esCmdConfig{ + fromRef: "ref1/tags", + toRef: "ref2/tags", + newTag: "v1.0.0", + previousReleaseVersion: "v1.0.0-rc.0", + }, + wantErr: true, + errorMessage: "--previous-release-version must be in ref format", + }, + { + name: "Valid previousReleaseVersion with rc in ref format", + args: ¬esCmdConfig{ + fromRef: "ref1/tags", + toRef: "ref2/tags", + newTag: "v1.0.0", + previousReleaseVersion: "tags/v1.0.0-rc.0", + }, + wantErr: false, + }, + { + name: "Valid previousReleaseVersion with alpha in ref format", + args: ¬esCmdConfig{ + fromRef: "ref1/tags", + toRef: "ref2/tags", + newTag: "v1.0.0", + previousReleaseVersion: "tags/v1.0.0-alpha.1", + }, + wantErr: false, + }, + { + name: "Valid previousReleaseVersion with beta in ref format", + args: ¬esCmdConfig{ + fromRef: "ref1/tags", + toRef: "ref2/tags", + newTag: "v1.0.0", + previousReleaseVersion: "tags/v1.0.0-beta.1", + }, + wantErr: false, + }, + { + name: "Invalid previousReleaseVersion without pre-release in ref format", + args: ¬esCmdConfig{ + fromRef: "ref1/tags", + toRef: "ref2/tags", + newTag: "v1.0.0", + previousReleaseVersion: "tags/v1.0.0", + }, + wantErr: true, + errorMessage: "--previous-release-version must contain 'alpha', 'beta', or 'rc' pre-release identifier", + }, + { + name: "Invalid previousReleaseVersion with unsupported pre-release type", + args: ¬esCmdConfig{ + fromRef: "ref1/tags", + toRef: "ref2/tags", + newTag: "v1.0.0", + previousReleaseVersion: "tags/v1.0.0-dev.1", + }, + wantErr: true, + errorMessage: "--previous-release-version must contain 'alpha', 'beta', or 'rc' pre-release identifier", + }, + { + name: "Invalid previousReleaseVersion with invalid semver", + args: ¬esCmdConfig{ + fromRef: "ref1/tags", + toRef: "ref2/tags", + newTag: "v1.0.0", + previousReleaseVersion: "tags/invalid-version", + }, + wantErr: true, + errorMessage: "invalid --previous-release-version, is not a valid semver", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateConfig(tt.args) if tt.wantErr { - if err == nil || err.Error() != tt.errorMessage { + if err == nil || !strings.Contains(err.Error(), tt.errorMessage) { t.Errorf("expected error '%s', got '%v'", tt.errorMessage, err) } } else if err != nil {