From 4b33a1e21bb63327317ce73317375afd3b569b35 Mon Sep 17 00:00:00 2001 From: Sascha Grunert Date: Thu, 19 Dec 2019 12:11:20 +0100 Subject: [PATCH] Add first part of changelog command implementation We're now able to generate changelogs for the two needed use cases: A patch release: ``` > go run cmd/krel/main.go changelog -t $TOKEN --tars $HOME/tars --branch release-1.16 INFO Using branch "release-1.16" INFO Using discovery mode "patch-to-patch" ... INFO Parsing latest tag v1.16.4 INFO Parsing previous tag v1.16.3 INFO discovered start SHA b3cbbae08ec52a7fc73d334838e18d17e8512749 INFO discovered end SHA 224be7bdce5a9dd0c2fd0d46b83865648e2fe0ba INFO fetching all commits. This might take a while... INFO starting to process commit 1 of 56 (1.79%): 224be7bdce5a9dd0c2fd0d46b83865648e2fe0ba ... ``` A minor release (no branch needed because the defalut is the `master` branch): ``` > go run cmd/krel/main.go changelog -t $TOKEN --tars $HOME/tars INFO Using branch "master" INFO Using discovery mode "minor-to-minor" ... INFO latest non patch version v1.17.0 INFO previous non patch version v1.16.0 INFO discovered start SHA 2bd9643cee5b3b3a5ecbd3af49d09018f0773c77 INFO discovered end SHA 70132b0f130acc0bed193d9ba59dd186f0e634cf INFO fetching all commits. This might take a while... INFO starting to process commit 1 of 3065 (0.03%): a6f41a46a54c47e4225adbcca251ce8204f9bb1c ... ``` We still have to mangle the outputs together but this will be part of another PR. Signed-off-by: Sascha Grunert --- cmd/krel/cmd/BUILD.bazel | 3 ++ cmd/krel/cmd/changelog.go | 102 +++++++++++++++++++++++++++++++++++++- cmd/krel/cmd/root.go | 5 +- cmd/release-notes/main.go | 1 + pkg/git/git.go | 58 ++++++++++++++++------ pkg/notes/options.go | 12 ++--- 6 files changed, 156 insertions(+), 25 deletions(-) diff --git a/cmd/krel/cmd/BUILD.bazel b/cmd/krel/cmd/BUILD.bazel index ac7525bf20b..2b528c47cfe 100644 --- a/cmd/krel/cmd/BUILD.bazel +++ b/cmd/krel/cmd/BUILD.bazel @@ -12,12 +12,15 @@ go_library( visibility = ["//visibility:public"], deps = [ "//pkg/git:go_default_library", + "//pkg/notes:go_default_library", "//pkg/release:go_default_library", "//pkg/util:go_default_library", + "@com_github_google_go_github_v28//github:go_default_library", "@com_github_pkg_errors//:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", "@com_github_spf13_cobra//:go_default_library", "@com_google_cloud_go//storage:go_default_library", + "@org_golang_x_oauth2//:go_default_library", ], ) diff --git a/cmd/krel/cmd/changelog.go b/cmd/krel/cmd/changelog.go index 85a02bf515d..b5b4434e126 100644 --- a/cmd/krel/cmd/changelog.go +++ b/cmd/krel/cmd/changelog.go @@ -16,7 +16,18 @@ limitations under the License. package cmd -import "github.com/spf13/cobra" +import ( + "context" + + "github.com/google/go-github/v28/github" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "golang.org/x/oauth2" + + "k8s.io/release/pkg/git" + "k8s.io/release/pkg/notes" +) // changelogCmd represents the subcommand for `krel changelog` var changelogCmd = &cobra.Command{ @@ -49,11 +60,98 @@ the golang based 'release-notes' tool: }, } +type changelogOptions struct { + branch string + bucket string + tars string + token string +} + +var changelogOpts = &changelogOptions{} + +const master = "master" + func init() { cobra.OnInitialize(initConfig) + + const ( + tarsFlag = "tars" + tokenFlag = "token" + ) + changelogCmd.PersistentFlags().StringVar(&changelogOpts.branch, "branch", master, "The target release branch. Leave it default for non-patch releases.") + changelogCmd.PersistentFlags().StringVar(&changelogOpts.bucket, "bucket", "kubernetes-release", "Specify gs bucket to point to in generated notes") + changelogCmd.PersistentFlags().StringVar(&changelogOpts.tars, tarsFlag, "", "Directory of tars to sha512 sum for display") + changelogCmd.PersistentFlags().StringVarP(&changelogOpts.token, tokenFlag, "t", "", "GitHub token for release notes retrieval") + + if err := changelogCmd.MarkPersistentFlagRequired(tokenFlag); err != nil { + logrus.Fatal(err) + } + if err := changelogCmd.MarkPersistentFlagRequired(tarsFlag); err != nil { + logrus.Fatal(err) + } + rootCmd.AddCommand(changelogCmd) } -func runChangelog() error { +func runChangelog() (err error) { + branch := master + revisionDiscoveryMode := notes.RevisionDiscoveryModeMinorToMinor + + if changelogOpts.branch != branch { + if !git.IsReleaseBranch(changelogOpts.branch) { + return errors.Wrapf(err, "Branch %q is no release branch", changelogOpts.branch) + } + branch = changelogOpts.branch + revisionDiscoveryMode = notes.RevisionDiscoveryModePatchToPatch + } + logrus.Infof("Using branch %q", branch) + logrus.Infof("Using discovery mode %q", revisionDiscoveryMode) + + notesOptions := notes.NewOptions() + notesOptions.Branch = branch + notesOptions.DiscoverMode = revisionDiscoveryMode + notesOptions.GithubOrg = git.DefaultGithubOrg + notesOptions.GithubRepo = git.DefaultGithubRepo + notesOptions.GithubToken = changelogOpts.token + notesOptions.RepoPath = rootOpts.repoPath + notesOptions.ReleaseBucket = changelogOpts.bucket + notesOptions.ReleaseTars = changelogOpts.tars + notesOptions.Debug = logrus.StandardLogger().Level >= logrus.DebugLevel + if err := notesOptions.ValidateAndFinish(); err != nil { + return err + } + + // Create the GitHub API client + ctx := context.Background() + httpClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: changelogOpts.token}, + )) + githubClient := github.NewClient(httpClient) + + // Fetch a list of fully-contextualized release notes + logrus.Info("fetching all commits. This might take a while...") + + gatherer := ¬es.Gatherer{ + Client: notes.WrapGithubClient(githubClient), + Context: ctx, + Org: git.DefaultGithubOrg, + Repo: git.DefaultGithubRepo, + } + releaseNotes, history, err := gatherer.ListReleaseNotes( + branch, notesOptions.StartSHA, notesOptions.EndSHA, "", "", + ) + if err != nil { + return errors.Wrapf(err, "listing release notes") + } + + // Create the markdown + doc, err := notes.CreateDocument(releaseNotes, history) + if err != nil { + return errors.Wrapf(err, "creating release note document") + } + + // TODO: mangle the documents into the target files + logrus.Infof("doc: %v", doc) + return nil } diff --git a/cmd/krel/cmd/root.go b/cmd/krel/cmd/root.go index ec9cbd339f5..fa575324714 100644 --- a/cmd/krel/cmd/root.go +++ b/cmd/krel/cmd/root.go @@ -17,6 +17,9 @@ limitations under the License. package cmd import ( + "os" + "path/filepath" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -50,7 +53,7 @@ func init() { rootCmd.PersistentFlags().BoolVar(&rootOpts.nomock, "nomock", false, "nomock flag") rootCmd.PersistentFlags().BoolVar(&rootOpts.cleanup, "cleanup", false, "cleanup flag") - rootCmd.PersistentFlags().StringVar(&rootOpts.repoPath, "repo", "", "the local path to the repository to be used") + rootCmd.PersistentFlags().StringVar(&rootOpts.repoPath, "repo", filepath.Join(os.TempDir(), "k8s"), "the local path to the repository to be used") rootCmd.PersistentFlags().StringVar(&rootOpts.logLevel, "log-level", "info", "the logging verbosity, either 'panic', 'fatal', 'error', 'warn', 'warning', 'info', 'debug' or 'trace'") } diff --git a/cmd/release-notes/main.go b/cmd/release-notes/main.go index fcedd3e12c4..ca209b52149 100644 --- a/cmd/release-notes/main.go +++ b/cmd/release-notes/main.go @@ -187,6 +187,7 @@ func init() { notes.RevisionDiscoveryModeNONE, notes.RevisionDiscoveryModeMinorToLatest, notes.RevisionDiscoveryModePatchToPatch, + notes.RevisionDiscoveryModeMinorToMinor, }, ", "), ), ) diff --git a/pkg/git/git.go b/pkg/git/git.go index d5a8e9e7110..621541dd49c 100644 --- a/pkg/git/git.go +++ b/pkg/git/git.go @@ -209,10 +209,11 @@ func (r *Repo) RevParseShort(rev string) (string, error) { // end (release-1.xx or master) revision inside the repository func (r *Repo) LatestNonPatchFinalToLatest() (start, end string, err error) { // Find the last non patch version tag, then resolve its revision - version, err := r.latestNonPatchFinalVersion() + versions, err := r.latestNonPatchFinalVersions() if err != nil { return "", "", err } + version := versions[0] versionTag := addTagPrefix(version.String()) logrus.Infof("latest non patch version %s", versionTag) start, err = r.RevParse(versionTag) @@ -230,15 +231,43 @@ func (r *Repo) LatestNonPatchFinalToLatest() (start, end string, err error) { return start, end, nil } -func (r *Repo) latestNonPatchFinalVersion() (semver.Version, error) { - latestFinalTag := semver.Version{} +func (r *Repo) LatestNonPatchFinalToMinor() (start, end string, err error) { + // Find the last non patch version tag, then resolve its revision + versions, err := r.latestNonPatchFinalVersions() + if err != nil { + return "", "", err + } + if len(versions) < 2 { + return "", "", errors.New("unable to find two latest non patch versions") + } + + latestVersion := versions[0] + latestVersionTag := addTagPrefix(latestVersion.String()) + logrus.Infof("latest non patch version %s", latestVersionTag) + end, err = r.RevParse(latestVersionTag) + if err != nil { + return "", "", err + } + + previousVersion := versions[1] + previousVersionTag := addTagPrefix(previousVersion.String()) + logrus.Infof("previous non patch version %s", previousVersionTag) + start, err = r.RevParse(previousVersionTag) + if err != nil { + return "", "", err + } + + return start, end, nil +} + +func (r *Repo) latestNonPatchFinalVersions() ([]semver.Version, error) { + latestVersions := []semver.Version{} tags, err := r.inner.Tags() if err != nil { - return latestFinalTag, err + return nil, err } - found := false _ = tags.ForEach(func(t *plumbing.Reference) error { // nolint: errcheck tag := trimTagPrefix(t.Name().Short()) ver, err := semver.Make(tag) @@ -246,18 +275,17 @@ func (r *Repo) latestNonPatchFinalVersion() (semver.Version, error) { if err == nil { // We're searching for the latest, non patch final tag if ver.Patch == 0 && len(ver.Pre) == 0 { - if ver.GT(latestFinalTag) { - latestFinalTag = ver - found = true + if len(latestVersions) == 0 || ver.GT(latestVersions[0]) { + latestVersions = append([]semver.Version{ver}, latestVersions...) } } } return nil }) - if !found { - return latestFinalTag, fmt.Errorf("unable to find latest non patch release") + if len(latestVersions) == 0 { + return nil, fmt.Errorf("unable to find latest non patch release") } - return latestFinalTag, nil + return latestVersions, nil } func (r *Repo) releaseBranchOrMasterRev(major, minor uint64) (rev string, err error) { @@ -431,11 +459,9 @@ func (r *Repo) LatestPatchToPatch(branch string) (start, end string, err error) return "", "", err } - if len(latestTag.Pre) > 0 { - return "", "", errors.Errorf( - "found pre-release %v as latest tag on branch %s", - latestTag, branch, - ) + if len(latestTag.Pre) > 0 && latestTag.Patch > 0 { + latestTag.Patch-- + latestTag.Pre = nil } if latestTag.Patch == 0 { diff --git a/pkg/notes/options.go b/pkg/notes/options.go index 5a5ddac8d10..aaf26142e28 100644 --- a/pkg/notes/options.go +++ b/pkg/notes/options.go @@ -49,6 +49,7 @@ const ( RevisionDiscoveryModeNONE = "none" RevisionDiscoveryModeMinorToLatest = "minor-to-latest" RevisionDiscoveryModePatchToPatch = "patch-to-patch" + RevisionDiscoveryModeMinorToMinor = "minor-to-minor" ) // NewOptions creates a new Options instance with the default values @@ -83,14 +84,13 @@ func (o *Options) ValidateAndFinish() error { var start, end string if o.DiscoverMode == RevisionDiscoveryModeMinorToLatest { start, end, err = repo.LatestNonPatchFinalToLatest() - if err != nil { - return err - } } else if o.DiscoverMode == RevisionDiscoveryModePatchToPatch { start, end, err = repo.LatestPatchToPatch(o.Branch) - if err != nil { - return err - } + } else if o.DiscoverMode == RevisionDiscoveryModeMinorToMinor { + start, end, err = repo.LatestNonPatchFinalToMinor() + } + if err != nil { + return err } o.StartSHA = start