diff --git a/pkg/anago/stage.go b/pkg/anago/stage.go index 1fff237bca1..ee5ad98a3da 100644 --- a/pkg/anago/stage.go +++ b/pkg/anago/stage.go @@ -443,6 +443,7 @@ func (d *DefaultStage) GenerateChangelog() error { Branch: branch, Bucket: d.options.Bucket(), HTMLFile: filepath.Join(workspaceDir, "src/release-notes.html"), + JSONFile: filepath.Join(workspaceDir, "src/release-notes.json"), Dependencies: true, Tars: filepath.Join( gitRoot, diff --git a/pkg/changelog/changelog.go b/pkg/changelog/changelog.go index 52d6587ef47..0ca1ac02aac 100644 --- a/pkg/changelog/changelog.go +++ b/pkg/changelog/changelog.go @@ -19,6 +19,7 @@ package changelog import ( "bufio" "bytes" + "encoding/json" "fmt" "os" "path/filepath" @@ -41,6 +42,7 @@ type Options struct { Bucket string Tars string HTMLFile string + JSONFile string RecordDir string ReplayDir string Dependencies bool @@ -97,7 +99,7 @@ func (c *Changelog) Run() error { } logrus.Infof("Found latest %s commit %s", remoteBranch, head) - var markdown, startRev, endRev string + var markdown, jsonStr, startRev, endRev string if tag.Patch == 0 { if len(tag.Pre) == 0 { // Still create the downloads table @@ -132,7 +134,7 @@ func (c *Changelog) Run() error { // the current HEAD as end revision. endRev = head - markdown, err = c.generateReleaseNotes(branch, startRev, endRev) + markdown, jsonStr, err = c.generateReleaseNotes(branch, startRev, endRev) } else { // New minor alpha, beta and rc releases get generated notes latestTags, tErr := c.impl.LatestGitHubTagsPerBranch() @@ -148,7 +150,7 @@ func (c *Changelog) Run() error { startRev = startTag endRev = head - markdown, err = c.generateReleaseNotes(branch, startRev, endRev) + markdown, jsonStr, err = c.generateReleaseNotes(branch, startRev, endRev) } else { return errors.Errorf( "no latest tag available for branch %s", branch, @@ -164,7 +166,7 @@ func (c *Changelog) Run() error { startRev = startTag endRev = head - markdown, err = c.generateReleaseNotes(branch, startTag, endRev) + markdown, jsonStr, err = c.generateReleaseNotes(branch, startTag, endRev) } if err != nil { return errors.Wrap(err, "generate release notes") @@ -213,6 +215,11 @@ func (c *Changelog) Run() error { return errors.Wrap(err, "write HTML") } + logrus.Info("Writing JSON") + if err := c.writeJSON(tag, jsonStr); err != nil { + return errors.Wrap(err, "write JSON") + } + logrus.Info("Committing changes") return errors.Wrap( c.commitChanges(repo, branch, tag), @@ -220,7 +227,9 @@ func (c *Changelog) Run() error { ) } -func (c *Changelog) generateReleaseNotes(branch, startRev, endRev string) (string, error) { +func (c *Changelog) generateReleaseNotes( + branch, startRev, endRev string, +) (markdown, jsonStr string, err error) { logrus.Info("Generating release notes") notesOptions := options.New() @@ -236,25 +245,33 @@ func (c *Changelog) generateReleaseNotes(branch, startRev, endRev string) (strin notesOptions.Pull = false if err := c.impl.ValidateAndFinish(notesOptions); err != nil { - return "", errors.Wrap(err, "validating notes options") + return "", "", errors.Wrap(err, "validating notes options") } - doc, err := c.impl.GatherReleaseNotesDocument( - notesOptions, startRev, c.options.Tag, - ) + releaseNotes, err := c.impl.GatherReleaseNotes(notesOptions) + if err != nil { + return "", "", errors.Wrapf(err, "gather release notes") + } + + doc, err := c.impl.NewDocument(releaseNotes, startRev, c.options.Tag) if err != nil { - return "", errors.Wrap(err, "gather release notes") + return "", "", errors.Wrapf(err, "create release note document") } - markdown, err := c.impl.RenderMarkdownTemplate( + releaseNotesJSON, err := json.MarshalIndent(releaseNotes.ByPR(), "", " ") + if err != nil { + return "", "", errors.Wrapf(err, "build release notes JSON") + } + + markdown, err = c.impl.RenderMarkdownTemplate( doc, c.options.Bucket, c.options.Tars, options.GoTemplateInline+releaseNotesTemplate, ) if err != nil { - return "", errors.Wrapf(err, "render release notes to markdown") + return "", "", errors.Wrapf(err, "render release notes to markdown") } - return markdown, nil + return markdown, string(releaseNotesJSON), nil } func (c *Changelog) writeMarkdown( @@ -316,6 +333,13 @@ func (c *Changelog) htmlChangelogFilename(tag semver.Version) string { return changelogFilename(tag, "html") } +func (c *Changelog) jsonChangelogFilename(tag semver.Version) string { + if c.options.JSONFile != "" { + return c.options.JSONFile + } + return changelogFilename(tag, "json") +} + func markdownChangelogReadme() string { return filepath.Join(RepoChangelogDir, "README.md") } @@ -361,6 +385,18 @@ func (c *Changelog) writeHTML(tag semver.Version, markdown string) error { ) } +func (c *Changelog) writeJSON(tag semver.Version, jsonStr string) error { + absOutputPath, err := c.impl.Abs(c.jsonChangelogFilename(tag)) + if err != nil { + return errors.Wrap(err, "get absolute file path") + } + logrus.Infof("Writing JSON file to %s", absOutputPath) + return errors.Wrap( + c.impl.WriteFile(absOutputPath, []byte(jsonStr), os.FileMode(0o644)), + "write JSON", + ) +} + func (c *Changelog) lookupRemoteReleaseNotes(branch string) (string, error) { logrus.Info("Assuming new minor release") diff --git a/pkg/changelog/changelog_test.go b/pkg/changelog/changelog_test.go index 1c65083e8f6..4790a7a6e99 100644 --- a/pkg/changelog/changelog_test.go +++ b/pkg/changelog/changelog_test.go @@ -26,6 +26,7 @@ import ( "k8s.io/release/pkg/changelog" "k8s.io/release/pkg/changelog/changelogfakes" "k8s.io/release/pkg/github" + "k8s.io/release/pkg/notes" ) func TestRun(t *testing.T) { @@ -49,6 +50,7 @@ func TestRun(t *testing.T) { }, }, nil) mock.ReadFileReturns([]byte(changelog.TocEnd), nil) + mock.GatherReleaseNotesReturns(¬es.ReleaseNotes{}, nil) }, shouldErr: false, }, @@ -60,6 +62,7 @@ func TestRun(t *testing.T) { Patch: 3, }, nil) mock.ReadFileReturns([]byte(changelog.TocEnd), nil) + mock.GatherReleaseNotesReturns(¬es.ReleaseNotes{}, nil) }, shouldErr: false, }, @@ -77,6 +80,7 @@ func TestRun(t *testing.T) { "release-1.19": "v1.19.0-beta.0", }, nil) mock.ReadFileReturns([]byte(changelog.TocEnd), nil) + mock.GatherReleaseNotesReturns(¬es.ReleaseNotes{}, nil) }, shouldErr: false, }, @@ -234,6 +238,33 @@ func TestRun(t *testing.T) { }, shouldErr: true, }, + { // GatherReleaseNotes failed + prepare: func(mock *changelogfakes.FakeImpl, _ *changelog.Options) { + mock.TagStringToSemverReturns(semver.Version{ + Pre: []semver.PRVersion{ + {VersionStr: "alpha"}, + {VersionNum: 1}, + }, + }, nil) + mock.ReadFileReturns([]byte(changelog.TocEnd), nil) + mock.GatherReleaseNotesReturns(nil, err) + }, + shouldErr: true, + }, + { // NewDocumentReturns failed + prepare: func(mock *changelogfakes.FakeImpl, _ *changelog.Options) { + mock.TagStringToSemverReturns(semver.Version{ + Pre: []semver.PRVersion{ + {VersionStr: "alpha"}, + {VersionNum: 1}, + }, + }, nil) + mock.ReadFileReturns([]byte(changelog.TocEnd), nil) + mock.GatherReleaseNotesReturns(¬es.ReleaseNotes{}, nil) + mock.NewDocumentReturns(nil, err) + }, + shouldErr: true, + }, } { options := &changelog.Options{} sut := changelog.New(options) diff --git a/pkg/changelog/changelogfakes/fake_impl.go b/pkg/changelog/changelogfakes/fake_impl.go index 574670fa94a..85500e9944c 100644 --- a/pkg/changelog/changelogfakes/fake_impl.go +++ b/pkg/changelog/changelogfakes/fake_impl.go @@ -27,6 +27,7 @@ import ( "github.com/yuin/goldmark/parser" "k8s.io/release/pkg/git" "k8s.io/release/pkg/github" + "k8s.io/release/pkg/notes" "k8s.io/release/pkg/notes/document" "k8s.io/release/pkg/notes/options" ) @@ -124,19 +125,17 @@ type FakeImpl struct { result1 string result2 error } - GatherReleaseNotesDocumentStub func(*options.Options, string, string) (*document.Document, error) - gatherReleaseNotesDocumentMutex sync.RWMutex - gatherReleaseNotesDocumentArgsForCall []struct { + GatherReleaseNotesStub func(*options.Options) (*notes.ReleaseNotes, error) + gatherReleaseNotesMutex sync.RWMutex + gatherReleaseNotesArgsForCall []struct { arg1 *options.Options - arg2 string - arg3 string } - gatherReleaseNotesDocumentReturns struct { - result1 *document.Document + gatherReleaseNotesReturns struct { + result1 *notes.ReleaseNotes result2 error } - gatherReleaseNotesDocumentReturnsOnCall map[int]struct { - result1 *document.Document + gatherReleaseNotesReturnsOnCall map[int]struct { + result1 *notes.ReleaseNotes result2 error } GenerateTOCStub func(string) (string, error) @@ -191,6 +190,21 @@ type FakeImpl struct { markdownToHTMLReturnsOnCall map[int]struct { result1 error } + NewDocumentStub func(*notes.ReleaseNotes, string, string) (*document.Document, error) + newDocumentMutex sync.RWMutex + newDocumentArgsForCall []struct { + arg1 *notes.ReleaseNotes + arg2 string + arg3 string + } + newDocumentReturns struct { + result1 *document.Document + result2 error + } + newDocumentReturnsOnCall map[int]struct { + result1 *document.Document + result2 error + } OpenRepoStub func(string) (*git.Repo, error) openRepoMutex sync.RWMutex openRepoArgsForCall []struct { @@ -796,20 +810,18 @@ func (fake *FakeImpl) DependencyChangesReturnsOnCall(i int, result1 string, resu }{result1, result2} } -func (fake *FakeImpl) GatherReleaseNotesDocument(arg1 *options.Options, arg2 string, arg3 string) (*document.Document, error) { - fake.gatherReleaseNotesDocumentMutex.Lock() - ret, specificReturn := fake.gatherReleaseNotesDocumentReturnsOnCall[len(fake.gatherReleaseNotesDocumentArgsForCall)] - fake.gatherReleaseNotesDocumentArgsForCall = append(fake.gatherReleaseNotesDocumentArgsForCall, struct { +func (fake *FakeImpl) GatherReleaseNotes(arg1 *options.Options) (*notes.ReleaseNotes, error) { + fake.gatherReleaseNotesMutex.Lock() + ret, specificReturn := fake.gatherReleaseNotesReturnsOnCall[len(fake.gatherReleaseNotesArgsForCall)] + fake.gatherReleaseNotesArgsForCall = append(fake.gatherReleaseNotesArgsForCall, struct { arg1 *options.Options - arg2 string - arg3 string - }{arg1, arg2, arg3}) - stub := fake.GatherReleaseNotesDocumentStub - fakeReturns := fake.gatherReleaseNotesDocumentReturns - fake.recordInvocation("GatherReleaseNotesDocument", []interface{}{arg1, arg2, arg3}) - fake.gatherReleaseNotesDocumentMutex.Unlock() + }{arg1}) + stub := fake.GatherReleaseNotesStub + fakeReturns := fake.gatherReleaseNotesReturns + fake.recordInvocation("GatherReleaseNotes", []interface{}{arg1}) + fake.gatherReleaseNotesMutex.Unlock() if stub != nil { - return stub(arg1, arg2, arg3) + return stub(arg1) } if specificReturn { return ret.result1, ret.result2 @@ -817,47 +829,47 @@ func (fake *FakeImpl) GatherReleaseNotesDocument(arg1 *options.Options, arg2 str return fakeReturns.result1, fakeReturns.result2 } -func (fake *FakeImpl) GatherReleaseNotesDocumentCallCount() int { - fake.gatherReleaseNotesDocumentMutex.RLock() - defer fake.gatherReleaseNotesDocumentMutex.RUnlock() - return len(fake.gatherReleaseNotesDocumentArgsForCall) +func (fake *FakeImpl) GatherReleaseNotesCallCount() int { + fake.gatherReleaseNotesMutex.RLock() + defer fake.gatherReleaseNotesMutex.RUnlock() + return len(fake.gatherReleaseNotesArgsForCall) } -func (fake *FakeImpl) GatherReleaseNotesDocumentCalls(stub func(*options.Options, string, string) (*document.Document, error)) { - fake.gatherReleaseNotesDocumentMutex.Lock() - defer fake.gatherReleaseNotesDocumentMutex.Unlock() - fake.GatherReleaseNotesDocumentStub = stub +func (fake *FakeImpl) GatherReleaseNotesCalls(stub func(*options.Options) (*notes.ReleaseNotes, error)) { + fake.gatherReleaseNotesMutex.Lock() + defer fake.gatherReleaseNotesMutex.Unlock() + fake.GatherReleaseNotesStub = stub } -func (fake *FakeImpl) GatherReleaseNotesDocumentArgsForCall(i int) (*options.Options, string, string) { - fake.gatherReleaseNotesDocumentMutex.RLock() - defer fake.gatherReleaseNotesDocumentMutex.RUnlock() - argsForCall := fake.gatherReleaseNotesDocumentArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +func (fake *FakeImpl) GatherReleaseNotesArgsForCall(i int) *options.Options { + fake.gatherReleaseNotesMutex.RLock() + defer fake.gatherReleaseNotesMutex.RUnlock() + argsForCall := fake.gatherReleaseNotesArgsForCall[i] + return argsForCall.arg1 } -func (fake *FakeImpl) GatherReleaseNotesDocumentReturns(result1 *document.Document, result2 error) { - fake.gatherReleaseNotesDocumentMutex.Lock() - defer fake.gatherReleaseNotesDocumentMutex.Unlock() - fake.GatherReleaseNotesDocumentStub = nil - fake.gatherReleaseNotesDocumentReturns = struct { - result1 *document.Document +func (fake *FakeImpl) GatherReleaseNotesReturns(result1 *notes.ReleaseNotes, result2 error) { + fake.gatherReleaseNotesMutex.Lock() + defer fake.gatherReleaseNotesMutex.Unlock() + fake.GatherReleaseNotesStub = nil + fake.gatherReleaseNotesReturns = struct { + result1 *notes.ReleaseNotes result2 error }{result1, result2} } -func (fake *FakeImpl) GatherReleaseNotesDocumentReturnsOnCall(i int, result1 *document.Document, result2 error) { - fake.gatherReleaseNotesDocumentMutex.Lock() - defer fake.gatherReleaseNotesDocumentMutex.Unlock() - fake.GatherReleaseNotesDocumentStub = nil - if fake.gatherReleaseNotesDocumentReturnsOnCall == nil { - fake.gatherReleaseNotesDocumentReturnsOnCall = make(map[int]struct { - result1 *document.Document +func (fake *FakeImpl) GatherReleaseNotesReturnsOnCall(i int, result1 *notes.ReleaseNotes, result2 error) { + fake.gatherReleaseNotesMutex.Lock() + defer fake.gatherReleaseNotesMutex.Unlock() + fake.GatherReleaseNotesStub = nil + if fake.gatherReleaseNotesReturnsOnCall == nil { + fake.gatherReleaseNotesReturnsOnCall = make(map[int]struct { + result1 *notes.ReleaseNotes result2 error }) } - fake.gatherReleaseNotesDocumentReturnsOnCall[i] = struct { - result1 *document.Document + fake.gatherReleaseNotesReturnsOnCall[i] = struct { + result1 *notes.ReleaseNotes result2 error }{result1, result2} } @@ -1110,6 +1122,72 @@ func (fake *FakeImpl) MarkdownToHTMLReturnsOnCall(i int, result1 error) { }{result1} } +func (fake *FakeImpl) NewDocument(arg1 *notes.ReleaseNotes, arg2 string, arg3 string) (*document.Document, error) { + fake.newDocumentMutex.Lock() + ret, specificReturn := fake.newDocumentReturnsOnCall[len(fake.newDocumentArgsForCall)] + fake.newDocumentArgsForCall = append(fake.newDocumentArgsForCall, struct { + arg1 *notes.ReleaseNotes + arg2 string + arg3 string + }{arg1, arg2, arg3}) + stub := fake.NewDocumentStub + fakeReturns := fake.newDocumentReturns + fake.recordInvocation("NewDocument", []interface{}{arg1, arg2, arg3}) + fake.newDocumentMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImpl) NewDocumentCallCount() int { + fake.newDocumentMutex.RLock() + defer fake.newDocumentMutex.RUnlock() + return len(fake.newDocumentArgsForCall) +} + +func (fake *FakeImpl) NewDocumentCalls(stub func(*notes.ReleaseNotes, string, string) (*document.Document, error)) { + fake.newDocumentMutex.Lock() + defer fake.newDocumentMutex.Unlock() + fake.NewDocumentStub = stub +} + +func (fake *FakeImpl) NewDocumentArgsForCall(i int) (*notes.ReleaseNotes, string, string) { + fake.newDocumentMutex.RLock() + defer fake.newDocumentMutex.RUnlock() + argsForCall := fake.newDocumentArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeImpl) NewDocumentReturns(result1 *document.Document, result2 error) { + fake.newDocumentMutex.Lock() + defer fake.newDocumentMutex.Unlock() + fake.NewDocumentStub = nil + fake.newDocumentReturns = struct { + result1 *document.Document + result2 error + }{result1, result2} +} + +func (fake *FakeImpl) NewDocumentReturnsOnCall(i int, result1 *document.Document, result2 error) { + fake.newDocumentMutex.Lock() + defer fake.newDocumentMutex.Unlock() + fake.NewDocumentStub = nil + if fake.newDocumentReturnsOnCall == nil { + fake.newDocumentReturnsOnCall = make(map[int]struct { + result1 *document.Document + result2 error + }) + } + fake.newDocumentReturnsOnCall[i] = struct { + result1 *document.Document + result2 error + }{result1, result2} +} + func (fake *FakeImpl) OpenRepo(arg1 string) (*git.Repo, error) { fake.openRepoMutex.Lock() ret, specificReturn := fake.openRepoReturnsOnCall[len(fake.openRepoArgsForCall)] @@ -1895,8 +1973,8 @@ func (fake *FakeImpl) Invocations() map[string][][]interface{} { defer fake.currentBranchMutex.RUnlock() fake.dependencyChangesMutex.RLock() defer fake.dependencyChangesMutex.RUnlock() - fake.gatherReleaseNotesDocumentMutex.RLock() - defer fake.gatherReleaseNotesDocumentMutex.RUnlock() + fake.gatherReleaseNotesMutex.RLock() + defer fake.gatherReleaseNotesMutex.RUnlock() fake.generateTOCMutex.RLock() defer fake.generateTOCMutex.RUnlock() fake.getURLResponseMutex.RLock() @@ -1905,6 +1983,8 @@ func (fake *FakeImpl) Invocations() map[string][][]interface{} { defer fake.latestGitHubTagsPerBranchMutex.RUnlock() fake.markdownToHTMLMutex.RLock() defer fake.markdownToHTMLMutex.RUnlock() + fake.newDocumentMutex.RLock() + defer fake.newDocumentMutex.RUnlock() fake.openRepoMutex.RLock() defer fake.openRepoMutex.RUnlock() fake.parseHTMLTemplateMutex.RLock() diff --git a/pkg/changelog/impl.go b/pkg/changelog/impl.go index 784ce7abdd1..469b91f4da1 100644 --- a/pkg/changelog/impl.go +++ b/pkg/changelog/impl.go @@ -57,8 +57,9 @@ type impl interface { // Used in `generateReleaseNotes()` ValidateAndFinish(opts *options.Options) error - GatherReleaseNotesDocument( - opts *options.Options, previousRev, currentRev string, + GatherReleaseNotes(opts *options.Options) (*notes.ReleaseNotes, error) + NewDocument( + releaseNotes *notes.ReleaseNotes, previousRev, currentRev string, ) (*document.Document, error) RenderMarkdownTemplate( document *document.Document, bucket, fileDir, templateSpec string, @@ -133,10 +134,16 @@ func (*defaultImpl) ValidateAndFinish(opts *options.Options) error { return opts.ValidateAndFinish() } -func (*defaultImpl) GatherReleaseNotesDocument( - opts *options.Options, previousRev, currentRev string, +func (*defaultImpl) GatherReleaseNotes( + opts *options.Options, +) (*notes.ReleaseNotes, error) { + return notes.GatherReleaseNotes(opts) +} + +func (*defaultImpl) NewDocument( + releaseNotes *notes.ReleaseNotes, previousRev, currentRev string, ) (*document.Document, error) { - return document.GatherReleaseNotesDocument(opts, previousRev, currentRev) + return document.New(releaseNotes, previousRev, currentRev) } func (*defaultImpl) RenderMarkdownTemplate( diff --git a/pkg/object/objectfakes/fake_store.go b/pkg/object/objectfakes/fake_store.go index 6de825ea3bc..3ab0bf34233 100644 --- a/pkg/object/objectfakes/fake_store.go +++ b/pkg/object/objectfakes/fake_store.go @@ -1,3 +1,19 @@ +/* +Copyright 2020 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. +*/ + // Code generated by counterfeiter. DO NOT EDIT. package objectfakes