Skip to content

Implement clone, commit and push of release notes draft to user's fork #1102

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

Merged
merged 1 commit into from
Feb 21, 2020
Merged
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
175 changes: 172 additions & 3 deletions cmd/krel/cmd/release_notes.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"

"github.com/blang/semver"
Expand All @@ -34,6 +37,11 @@ import (
"k8s.io/release/pkg/util"
)

const (
// draftFilename filename for the release notes draft
draftFilename = "release-notes-draft.md"
)

// releaseNotesCmd represents the subcommand for `krel release-notes`
var releaseNotesCmd = &cobra.Command{
Use: "release-notes",
Expand Down Expand Up @@ -66,7 +74,13 @@ permissions to your fork of k/sig-release and k-sigs/release-notes.`,
}

type releaseNotesOptions struct {
tag string
tag string
draftOrg string
draftRepo string
createDraftPR bool
outputDir string
sigreleaseForkPath string
Format string
}

type releaseNotesResult struct {
Expand All @@ -85,6 +99,49 @@ func init() {
"version tag for the notes",
)

releaseNotesCmd.PersistentFlags().StringVar(
&releaseNotesOpts.draftOrg,
"draft-org",
"",
"a Github organization ownwer of the fork of k/sig-release where the Release Notes Draft PR will be created",
)

releaseNotesCmd.PersistentFlags().StringVar(
&releaseNotesOpts.draftRepo,
"draft-repo",
"",
"the name of the fork of k/sig-release, the Release Notes Draft PR will be created from this repository",
)

releaseNotesCmd.PersistentFlags().BoolVar(
&releaseNotesOpts.createDraftPR,
"create-draft-pr",
false,
"create the Release Notes Draft PR. --draft-org and --draft-repo must be set along with this option",
)

releaseNotesCmd.PersistentFlags().StringVarP(
&releaseNotesOpts.outputDir,
"output-dir",
"o",
".",
"output a copy of the release notes to this directory",
)

releaseNotesCmd.PersistentFlags().StringVar(
&releaseNotesOpts.Format,
"format",
util.EnvDefault("FORMAT", "markdown"),
"The format for notes output (options: markdown, json)",
)

releaseNotesCmd.PersistentFlags().StringVar(
&releaseNotesOpts.sigreleaseForkPath,
"sigrelease-fork-path",
filepath.Join(os.TempDir(), "k8s-sigrelease"),
"output a copy of the release notes to this directory",
)

rootCmd.AddCommand(releaseNotesCmd)
}

Expand Down Expand Up @@ -113,12 +170,124 @@ func runReleaseNotes() (err error) {
logrus.Infof("Using start tag %v", start)
logrus.Infof("Using end tag %v", tag)

_, err = releaseNotesFrom(start)
if releaseNotesOpts.createDraftPR {
if err = validateDraftPROptions(); err != nil {
return errors.Wrap(err, "validating PR command line options")
}
}

result, err := releaseNotesFrom(start)
if err != nil {
return errors.Wrapf(err, "generating release notes")
}

//TODO: implement PR creation for k-sigs/release-notes and k/sig-release
// Create RN draft PR
if releaseNotesOpts.createDraftPR {
if err := createDraftPR(tag, result); err != nil {
return errors.Wrap(err, "failed to create release notes draft PR")
}
return nil
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If releaseNotesOpts.createDraftPR is false, because --create-draft-pr was not given, should we log out to say we're skipping it and why? Might help users understand why it's not working if they missed that flag.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if do not specify an output option for the release notes we could print out the help screen. There are and will be more options in addition to --create-draft-pr in the subcommand. For example just writing the release notes to files or generating the json version.


switch releaseNotesOpts.Format {
case "json":
err = ioutil.WriteFile(filepath.Join(releaseNotesOpts.outputDir, "release-notes.json"), []byte(result.json), 0644)
if err != nil {
return errors.Wrap(err, "writing release notes JSON file")
}
case "markdown":
err = ioutil.WriteFile(filepath.Join(releaseNotesOpts.outputDir, "release-notes.md"), []byte(result.json), 0644)
if err != nil {
return errors.Wrap(err, "writing release notes markdown file")
}
default:
return errors.Errorf("%q is an unsupported format", releaseNotesOpts.Format)
}

// TODO: implement PR creation for k-sigs/release-notes
return nil
}

// validateDraftPROptions checks if we have all needed parameters to create the Release Notes PR
func validateDraftPROptions() error {
if releaseNotesOpts.createDraftPR {
// Check if --draft-org is set
if releaseNotesOpts.draftOrg == "" {
logrus.Warn("cannot generate the Release Notes draft PR without --draft-org")
}

// Check if --draft-repo is set
if releaseNotesOpts.draftRepo == "" {
logrus.Warn("cannot generate the Release Notes draft PR without --draft-repo")
}

if releaseNotesOpts.draftOrg == "" || releaseNotesOpts.draftRepo == "" {
return errors.New("To generate the release notes draft you must define both --draft-org and --draft-repo")
}
}
return nil
}

// createDraftPR pushes the release notes draft to the users fork
func createDraftPR(tag string, result *releaseNotesResult) error {
s, err := util.TagStringToSemver(tag)
if err != nil {
return errors.Wrapf(err, "no valid tag: %v", tag)
}

branchname := "release-notes-draft-" + tag

// checkout kubernetes/sig-release
sigReleaseRepo, err := git.CloneOrOpenGitHubRepo(releaseNotesOpts.sigreleaseForkPath, git.DefaultGithubOrg, git.DefaultGithubReleaseRepo, true)
if err != nil {
return errors.Wrap(err, "cloning k/sig-release")
}

// add the user's fork as a remote
err = sigReleaseRepo.AddRemote("userfork", releaseNotesOpts.draftOrg, releaseNotesOpts.draftRepo)
if err != nil {
return errors.Wrap(err, "adding users fork as remote repository")
}

// verify the branch doesn't already exist on the user's fork
err = sigReleaseRepo.HasRemoteBranch(branchname)
if err == nil {
return errors.Errorf("remote repo already has a branch named %s", branchname)
}

// checkout the new branch
err = sigReleaseRepo.Checkout("-b", branchname)
if err != nil {
return errors.Wrapf(err, "creating new branch %s", branchname)
}

// generate the notes
targetdir := filepath.Join(sigReleaseRepo.Dir(), "releases", fmt.Sprintf("release-%d.%d", s.Major, s.Minor))
logrus.Debugf("Release notes markdown will be written to %s", targetdir)
err = ioutil.WriteFile(filepath.Join(targetdir, draftFilename), []byte(result.markdown), 0644)
if err != nil {
return errors.Wrapf(err, "writing release notes draft")
}

// commit the results
err = sigReleaseRepo.Add(filepath.Join("releases", fmt.Sprintf("release-%d.%d", s.Major, s.Minor), draftFilename))
if err != nil {
return errors.Wrap(err, "adding release notes draft to staging area")
}

err = sigReleaseRepo.Commit("Release Notes draft for k/k " + tag)
if err != nil {
return errors.Wrapf(err, "Error creating commit in %s/%s", releaseNotesOpts.draftOrg, releaseNotesOpts.draftRepo)
}

// push to fork
logrus.Infof("Pushing release notes draft to %s/%s", releaseNotesOpts.draftOrg, releaseNotesOpts.draftRepo)
err = sigReleaseRepo.PushToRemote("userfork", branchname)
if err != nil {
return errors.Wrapf(err, "pushing changes to %s/%s", releaseNotesOpts.draftOrg, releaseNotesOpts.draftRepo)
}

// TODO: Call github API and create PR against k/sig-release
return nil
}

Expand Down
36 changes: 30 additions & 6 deletions pkg/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ import (
)

const (
DefaultGithubOrg = "kubernetes"
DefaultGithubRepo = "kubernetes"
DefaultGithubURLBase = "https://github.com"
DefaultRemote = "origin"
DefaultMasterRef = "HEAD"
Master = "master"
DefaultGithubOrg = "kubernetes"
DefaultGithubRepo = "kubernetes"
DefaultGithubReleaseRepo = "sig-release"
DefaultGithubURLBase = "https://github.com"
DefaultRemote = "origin"
DefaultMasterRef = "HEAD"
Master = "master"

branchRE = `master|release-([0-9]{1,})\.([0-9]{1,})(\.([0-9]{1,}))*$`
defaultGithubAuthRoot = "[email protected]:"
Expand Down Expand Up @@ -747,3 +748,26 @@ func (r *Repo) Rm(force bool, files ...string) error {
NewWithWorkDir(r.Dir(), gitExecutable, args...).
RunSilentSuccess()
}

// AddRemote adds a new remote to the current working tree
func (r *Repo) AddRemote(name, owner, repo string) error {
args := []string{"remote", "add"}
args = append(args, name, fmt.Sprintf("%s%s/%s.git", defaultGithubAuthRoot, owner, repo))

return command.
NewWithWorkDir(r.Dir(), gitExecutable, args...).
RunSilentSuccess()
}

// PushToRemote push the current branch to a spcified remote, but only if the
// repository is not in dry run mode
func (r *Repo) PushToRemote(remote, remoteBranch string) error {
args := []string{"push"}
if r.dryRun {
logrus.Infof("Won't push due to dry run repository")
args = append(args, "--dry-run")
}
args = append(args, remote, remoteBranch)

return command.NewWithWorkDir(r.Dir(), gitExecutable, args...).RunSuccess()
}