Skip to content

Commit edb33af

Browse files
committed
[krel] add options for release notes draft PR automation
1 parent cadfbd5 commit edb33af

File tree

2 files changed

+167
-3
lines changed

2 files changed

+167
-3
lines changed

cmd/krel/cmd/release_notes.go

Lines changed: 144 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"encoding/json"
2222
"fmt"
23+
"io/ioutil"
2324
"strings"
2425

2526
"github.com/blang/semver"
@@ -66,7 +67,11 @@ permissions to your fork of k/sig-release and k-sigs/release-notes.`,
6667
}
6768

6869
type releaseNotesOptions struct {
69-
tag string
70+
tag string
71+
draftOrg string
72+
draftRepo string
73+
createDraftPR bool
74+
outputDir string
7075
}
7176

7277
type releaseNotesResult struct {
@@ -85,6 +90,38 @@ func init() {
8590
"version tag for the notes",
8691
)
8792

93+
releaseNotesCmd.PersistentFlags().StringVarP(
94+
&releaseNotesOpts.draftOrg,
95+
"draft-org",
96+
"",
97+
"",
98+
"a Github organization or user where the Release Notes PR will be created",
99+
)
100+
101+
releaseNotesCmd.PersistentFlags().StringVarP(
102+
&releaseNotesOpts.draftRepo,
103+
"draft-repo",
104+
"",
105+
"",
106+
"the name of a Github repository where the Release Notes PR will be created",
107+
)
108+
109+
releaseNotesCmd.PersistentFlags().BoolVarP(
110+
&releaseNotesOpts.createDraftPR,
111+
"create-draft-pr",
112+
"",
113+
false,
114+
"create the Release Notes draft PR. --draft-org and --draft-repo muste be set along with this option",
115+
)
116+
117+
releaseNotesCmd.PersistentFlags().StringVarP(
118+
&releaseNotesOpts.outputDir,
119+
"output-dir",
120+
"o",
121+
"",
122+
"output a copy of the release notes to this directory",
123+
)
124+
88125
rootCmd.AddCommand(releaseNotesCmd)
89126
}
90127

@@ -113,12 +150,116 @@ func runReleaseNotes() (err error) {
113150
logrus.Infof("Using start tag %v", start)
114151
logrus.Infof("Using end tag %v", tag)
115152

116-
_, err = releaseNotesFrom(start)
153+
if releaseNotesOpts.createDraftPR {
154+
err = validateDraftPROptions()
155+
if err != nil {
156+
return errors.Wrap(err, "validating PR command line options")
157+
}
158+
}
159+
160+
result, err := releaseNotesFrom(start)
117161
if err != nil {
118162
return errors.Wrapf(err, "generating release notes")
119163
}
120164

121-
//TODO: implement PR creation for k-sigs/release-notes and k/sig-release
165+
// Create RN draft PR
166+
if releaseNotesOpts.createDraftPR {
167+
err = createDraftPR(tag, result)
168+
if err != nil {
169+
return errors.Wrap(err, "Failed to create release notes draft PR")
170+
}
171+
}
172+
173+
if releaseNotesOpts.outputDir != "" {
174+
err = ioutil.WriteFile(releaseNotesOpts.outputDir+"release-notes.json", []byte(result.json), 0644)
175+
if err != nil {
176+
return errors.Wrap(err, "writing release notes JSON file")
177+
}
178+
179+
err = ioutil.WriteFile(releaseNotesOpts.outputDir+"release-notes.md", []byte(result.json), 0644)
180+
if err != nil {
181+
return errors.Wrap(err, "writing release notes markdown file")
182+
}
183+
}
184+
185+
// TODO: implement PR creation for k-sigs/release-notes
186+
return nil
187+
}
188+
189+
// validateDraftPROptions checks if we have all needed parameters to create the Release Notes PR
190+
func validateDraftPROptions() error {
191+
if releaseNotesOpts.createDraftPR {
192+
// Check if --draft-org is set
193+
if releaseNotesOpts.draftOrg == "" {
194+
return errors.New("cannot generate Release Notes draft PR without --draft-org")
195+
}
196+
197+
// Check if --draft-repo is set
198+
if releaseNotesOpts.draftRepo == "" {
199+
return errors.New("cannot generate Release Notes draft PR without --draft-repo")
200+
}
201+
}
202+
return nil
203+
}
204+
205+
// createDraftPR pushes the release notes draft to the users fork
206+
func createDraftPR(tag string, result *releaseNotesResult) error {
207+
s, err := util.TagStringToSemver(tag)
208+
if err != nil {
209+
return errors.Wrapf(err, "no valid tag: %v", tag)
210+
}
211+
212+
// checkout kubernetes/sig-release
213+
sigReleaseRepo, err := git.CloneOrOpenGitHubRepo("", "kubernetes", "sig-release", true)
214+
if err != nil {
215+
return errors.Wrap(err, "cloning k/sig-release")
216+
}
217+
218+
// add the user's fork as a remote
219+
err = sigReleaseRepo.AddRemote("userfork", releaseNotesOpts.draftOrg, releaseNotesOpts.draftRepo)
220+
if err != nil {
221+
return errors.Wrap(err, "adding users fork as remote repository")
222+
}
223+
224+
// verify the branch doesn't already exist on the user's fork
225+
err = sigReleaseRepo.HasRemoteBranch("release-notes-draft-" + tag)
226+
if err == nil {
227+
return errors.New(fmt.Sprintf("Remote repo already has a branch named release-notes-draft-%s", tag))
228+
}
229+
230+
// checkout the new branch
231+
err = sigReleaseRepo.Checkout("-b", "release-notes-draft-"+tag)
232+
if err != nil {
233+
return errors.Wrapf(err, "creating new branch %s", "release-notes-draft-"+tag)
234+
}
235+
236+
// generate the notes
237+
targetdir := sigReleaseRepo.Dir() + fmt.Sprintf("/releases/release-%d.%d", s.Major, s.Minor)
238+
logrus.Debugf("Release notes markdown will be written to %s", targetdir)
239+
err = ioutil.WriteFile(targetdir+"/release-notes-draft.md", []byte(result.markdown), 0644)
240+
if err != nil {
241+
return errors.Wrapf(err, "writing release notes draft")
242+
}
243+
244+
// commit the results
245+
err = sigReleaseRepo.Add(fmt.Sprintf("releases/release-%d.%d", s.Major, s.Minor) + "/release-notes-draft.md")
246+
if err != nil {
247+
return errors.Wrap(err, "adding release notes draft to staging area")
248+
}
249+
250+
err = sigReleaseRepo.Commit("Release Notes draft for k/k " + tag)
251+
if err != nil {
252+
return errors.Wrapf(err, "Error creating commit in %s/%s", releaseNotesOpts.draftOrg, releaseNotesOpts.draftRepo)
253+
}
254+
255+
// push to fork
256+
logrus.Infof("Pushing release notes draft to %s/%s", releaseNotesOpts.draftOrg, releaseNotesOpts.draftRepo)
257+
err = sigReleaseRepo.PushToRemote("userfork", "release-notes-draft-"+tag)
258+
if err != nil {
259+
return errors.Wrapf(err, "pushing changes to %s/%s", releaseNotesOpts.draftOrg, releaseNotesOpts.draftRepo)
260+
}
261+
262+
// TODO: Call github API and create PR against k/sig-release
122263
return nil
123264
}
124265

pkg/git/git.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,19 @@ func (r *Repo) Push(remoteBranch string) error {
535535
return command.NewWithWorkDir(r.Dir(), gitExecutable, args...).RunSuccess()
536536
}
537537

538+
// PushToRemote push the current branch to a spcified remote, but only if the
539+
// repository is not in dry run mode
540+
func (r *Repo) PushToRemote(remote, remoteBranch string) error {
541+
args := []string{"push"}
542+
if r.dryRun {
543+
logrus.Infof("Won't push due to dry run repository")
544+
args = append(args, "--dry-run")
545+
}
546+
args = append(args, remote, remoteBranch)
547+
548+
return command.NewWithWorkDir(r.Dir(), gitExecutable, args...).RunSuccess()
549+
}
550+
538551
// Head retrieves the current repository HEAD as a string
539552
func (r *Repo) Head() (string, error) {
540553
ref, err := r.inner.Head()
@@ -711,3 +724,13 @@ func (r *Repo) Rm(force bool, files ...string) error {
711724
NewWithWorkDir(r.Dir(), gitExecutable, args...).
712725
RunSilentSuccess()
713726
}
727+
728+
// AddRemote adds a new remote to the current working tree
729+
func (r *Repo) AddRemote(name, owner, repo string) error {
730+
args := []string{"remote", "add"}
731+
args = append(args, name, fmt.Sprintf("%s%s/%s.git", defaultGithubAuthRoot, owner, repo))
732+
733+
return command.
734+
NewWithWorkDir(r.Dir(), gitExecutable, args...).
735+
RunSilentSuccess()
736+
}

0 commit comments

Comments
 (0)