From d28450ccc202a70903a0e17850a4446249af08e6 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Fri, 16 Jul 2021 08:35:14 +0200 Subject: [PATCH] Extract ACTIONS_REQUIRED blocks from releases and PRs. (#40) --- pkg/markdown/helpers.go | 56 +++ pkg/{utils => markdown}/markdown.go | 92 +---- pkg/{utils => markdown}/markdown_test.go | 4 +- pkg/markdown/section.go | 76 ++++ pkg/webhooks/github/actions/common.go | 3 +- .../github/actions/release_drafter.go | 353 +++++++++++------- .../github/actions/release_drafter_test.go | 150 ++++++-- 7 files changed, 473 insertions(+), 261 deletions(-) create mode 100644 pkg/markdown/helpers.go rename pkg/{utils => markdown}/markdown.go (56%) rename pkg/{utils => markdown}/markdown_test.go (99%) create mode 100644 pkg/markdown/section.go diff --git a/pkg/markdown/helpers.go b/pkg/markdown/helpers.go new file mode 100644 index 0000000..763f8da --- /dev/null +++ b/pkg/markdown/helpers.go @@ -0,0 +1,56 @@ +package markdown + +import ( + "fmt" + "strings" +) + +var NoSuchBlockError = fmt.Errorf("no such block") + +func isHeading(l string) bool { + return strings.HasPrefix(l, "#") +} + +func headingLevel(l string) int { + level := 0 + for _, char := range l { + if char != '#' { + break + } + level++ + } + return level +} + +func ExtractAnnotatedBlock(annotation string, s string) (string, error) { + parts := strings.SplitN(s, "```"+annotation, 2) + if len(parts) != 2 { + return "", NoSuchBlockError + } + + parts = strings.SplitN(parts[1], "```", 2) + if len(parts) != 2 { + return "", NoSuchBlockError + } + + return strings.TrimSpace(parts[0]), nil +} + +func ToListItem(lines string) []string { + var result []string + + for i, line := range SplitLines(lines) { + if i == 0 { + result = append(result, "* "+line) + continue + } + + result = append(result, " "+line) + } + + return result +} + +func SplitLines(s string) []string { + return strings.Split(strings.Replace(s, `\r\n`, "\n", -1), "\n") +} diff --git a/pkg/utils/markdown.go b/pkg/markdown/markdown.go similarity index 56% rename from pkg/utils/markdown.go rename to pkg/markdown/markdown.go index 85895f4..755ac6e 100644 --- a/pkg/utils/markdown.go +++ b/pkg/markdown/markdown.go @@ -1,7 +1,6 @@ -package utils +package markdown import ( - "fmt" "strings" ) @@ -9,36 +8,7 @@ type Markdown struct { sections []*MarkdownSection } -func (m *Markdown) allSections() []*MarkdownSection { - var result []*MarkdownSection - - for _, s := range m.sections { - result = append(result, s.allSections()...) - } - - return result -} - -func (m *MarkdownSection) allSections() []*MarkdownSection { - var result []*MarkdownSection - - result = append(result, m) - - for _, s := range m.SubSections { - result = append(result, s.allSections()...) - } - - return result -} - -type MarkdownSection struct { - Level int - Heading string - ContentLines []string - SubSections []*MarkdownSection -} - -func ParseMarkdown(content string) *Markdown { +func Parse(content string) *Markdown { m := &Markdown{} lines := strings.Split(content, "\n") @@ -82,19 +52,14 @@ func ParseMarkdown(content string) *Markdown { return m } -func isHeading(l string) bool { - return strings.HasPrefix(l, "#") -} +func (m *Markdown) allSections() []*MarkdownSection { + var result []*MarkdownSection -func headingLevel(l string) int { - level := 0 - for _, char := range l { - if char != '#' { - break - } - level++ + for _, s := range m.sections { + result = append(result, s.allSections()...) } - return level + + return result } func (m *Markdown) AppendSection(s *MarkdownSection) { @@ -105,22 +70,6 @@ func (m *Markdown) PrependSection(s *MarkdownSection) { m.sections = append([]*MarkdownSection{s}, m.sections...) } -func (m *MarkdownSection) AppendContent(contentLines []string) { - m.ContentLines = append(m.ContentLines, contentLines...) -} - -func (m *MarkdownSection) PrependContent(contentLines []string) { - m.ContentLines = append(contentLines, m.ContentLines...) -} - -func (m *MarkdownSection) AppendChild(child *MarkdownSection) { - m.SubSections = append(m.SubSections, child) -} - -func (m *MarkdownSection) PrependChild(child *MarkdownSection) { - m.SubSections = append([]*MarkdownSection{child}, m.SubSections...) -} - func (m *Markdown) FindSectionByHeading(level int, headline string) *MarkdownSection { for _, s := range m.allSections() { if s.Level == level { @@ -148,28 +97,9 @@ func (m *Markdown) FindSectionByHeadingPrefix(level int, headlinePrefix string) func (m *Markdown) String() string { var result string for _, s := range m.sections { - result += s.String() - } - return strings.TrimSpace(result) -} - -func (m *MarkdownSection) String() string { - var result string - - if m.Level > 0 { - for i := 0; i < m.Level; i++ { - result += "#" - } - result += " " + m.Heading + "\n" + result += "\n" + s.String() + result = strings.Trim(result, "\n") } - for _, l := range m.ContentLines { - result += fmt.Sprintf("%s\n", l) - } - - for _, sub := range m.SubSections { - result += sub.String() - } - - return result + return strings.TrimSpace(result) } diff --git a/pkg/utils/markdown_test.go b/pkg/markdown/markdown_test.go similarity index 99% rename from pkg/utils/markdown_test.go rename to pkg/markdown/markdown_test.go index 3811a57..5d9b004 100644 --- a/pkg/utils/markdown_test.go +++ b/pkg/markdown/markdown_test.go @@ -1,4 +1,4 @@ -package utils +package markdown import ( "testing" @@ -173,7 +173,7 @@ content 1b`, for _, tt := range tests { // regex := regexp.MustCompile("\n\n") t.Run(tt.name, func(t *testing.T) { - m := ParseMarkdown(tt.content) + m := Parse(tt.content) if diff := cmp.Diff(m.sections, tt.want); diff != "" { t.Errorf("parseMarkdown(), differs in sections: %v", diff) } diff --git a/pkg/markdown/section.go b/pkg/markdown/section.go new file mode 100644 index 0000000..bfed22a --- /dev/null +++ b/pkg/markdown/section.go @@ -0,0 +1,76 @@ +package markdown + +import ( + "fmt" + "strings" +) + +type MarkdownSection struct { + Level int + Heading string + ContentLines []string + SubSections []*MarkdownSection +} + +func (m *MarkdownSection) allSections() []*MarkdownSection { + var result []*MarkdownSection + + result = append(result, m) + + for _, s := range m.SubSections { + result = append(result, s.allSections()...) + } + + return result +} + +func (m *MarkdownSection) FindSectionByHeading(level int, headline string) *MarkdownSection { + for _, s := range m.allSections() { + if s.Level == level { + if headline == s.Heading { + return s + } + } + } + + return nil +} + +func (m *MarkdownSection) AppendContent(contentLines []string) { + m.ContentLines = append(m.ContentLines, contentLines...) +} + +func (m *MarkdownSection) PrependContent(contentLines []string) { + m.ContentLines = append(contentLines, m.ContentLines...) +} + +func (m *MarkdownSection) AppendChild(child *MarkdownSection) { + m.SubSections = append(m.SubSections, child) +} + +func (m *MarkdownSection) PrependChild(child *MarkdownSection) { + m.SubSections = append([]*MarkdownSection{child}, m.SubSections...) +} + +func (m *MarkdownSection) String() string { + var result string + + if m.Level > 0 { + for i := 0; i < m.Level; i++ { + result += "#" + } + result += " " + m.Heading + "\n" + } + + for _, l := range m.ContentLines { + result += fmt.Sprintf("%s\n", l) + } + + result = strings.Trim(result, "\n") + + for _, sub := range m.SubSections { + result += "\n" + sub.String() + } + + return result +} diff --git a/pkg/webhooks/github/actions/common.go b/pkg/webhooks/github/actions/common.go index 460c87e..d01d1bf 100644 --- a/pkg/webhooks/github/actions/common.go +++ b/pkg/webhooks/github/actions/common.go @@ -205,7 +205,8 @@ func (w *WebhookActions) ProcessPullRequestEvent(payload *ghwebhooks.PullRequest } params := &releaseDrafterParams{ - RepositoryName: payload.Repository.Name, + RepositoryName: payload.Repository.Name, + ComponentReleaseInfo: &payload.PullRequest.Body, } err := a.appendMergedPR(ctx, payload.PullRequest.Title, payload.PullRequest.Number, payload.PullRequest.User.Login, params) if err != nil { diff --git a/pkg/webhooks/github/actions/release_drafter.go b/pkg/webhooks/github/actions/release_drafter.go index d90d464..9cba8f9 100644 --- a/pkg/webhooks/github/actions/release_drafter.go +++ b/pkg/webhooks/github/actions/release_drafter.go @@ -10,6 +10,7 @@ import ( "github.com/google/go-github/v32/github" "github.com/metal-stack/metal-robot/pkg/clients" "github.com/metal-stack/metal-robot/pkg/config" + "github.com/metal-stack/metal-robot/pkg/markdown" "github.com/metal-stack/metal-robot/pkg/utils" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" @@ -93,6 +94,7 @@ func (r *releaseDrafter) draft(ctx context.Context, p *releaseDrafterParams) err r.logger.Debugw("skip adding release draft because not a release vector repo", "repo", p.RepositoryName, "release", p.TagName) return nil } + componentTag := p.TagName if !strings.HasPrefix(componentTag, "v") { r.logger.Debugw("skip adding release draft because tag not starting with v", "repo", p.RepositoryName, "release", componentTag) @@ -105,115 +107,36 @@ func (r *releaseDrafter) draft(ctx context.Context, p *releaseDrafterParams) err return nil } - existingDraft, err := findExistingReleaseDraft(ctx, r.client, r.repoName) + infos, err := r.releaseInfos(ctx) if err != nil { return err } - var releaseTag string - if existingDraft != nil && existingDraft.TagName != nil { - releaseTag = *existingDraft.TagName - } else { - releaseTag, err = r.guessNextVersionFromLatestRelease(ctx) - if err != nil { - return err - } - } - - var priorBody string - if existingDraft != nil && existingDraft.Body != nil { - priorBody = *existingDraft.Body - } - - body := r.updateReleaseBody(r.draftHeadline, r.client.Organization(), priorBody, p.RepositoryName, componentSemver, p.ComponentReleaseInfo) - - if existingDraft != nil { - existingDraft.Body = &body - _, _, err := r.client.GetV3Client().Repositories.EditRelease(ctx, r.client.Organization(), r.repoName, existingDraft.GetID(), existingDraft) - if err != nil { - return errors.Wrap(err, "unable to update release draft") - } - r.logger.Infow("release draft updated", "repository", r.repoName, "trigger-component", p.RepositoryName, "version", p.TagName) - } else { - newDraft := &github.RepositoryRelease{ - TagName: v3.String(releaseTag), - Name: v3.String(fmt.Sprintf(r.titleTemplate, releaseTag)), - Body: &body, - Draft: v3.Bool(true), - } - _, _, err := r.client.GetV3Client().Repositories.CreateRelease(ctx, r.client.Organization(), r.repoName, newDraft) - if err != nil { - return errors.Wrap(err, "unable to create release draft") - } - r.logger.Infow("new release draft created", "repository", r.repoName, "trigger-component", p.RepositoryName, "version", p.TagName) - } + body := r.updateReleaseBody(r.client.Organization(), infos.body, p.RepositoryName, componentSemver, p.ComponentReleaseInfo) - return nil + return r.createOrUpdateRelease(ctx, infos, body, p) } -func findExistingReleaseDraft(ctx context.Context, client *clients.Github, repoName string) (*github.RepositoryRelease, error) { - opt := &github.ListOptions{ - PerPage: 100, - } +func (r *releaseDrafter) updateReleaseBody(org string, priorBody string, component string, componentVersion semver.Version, componentBody *string) string { + m := markdown.Parse(priorBody) - for { - releases, resp, err := client.GetV3Client().Repositories.ListReleases(ctx, client.Organization(), repoName, opt) - if err != nil { - return nil, errors.Wrap(err, "error retrieving releases") - } + releaseSection := ensureReleaseSection(m, r.draftHeadline) - for _, release := range releases { - if release.Draft != nil && *release.Draft { - return release, nil - } + componentSection := m.FindSectionByHeading(2, "Component Releases") + if componentSection == nil { + componentSection = &markdown.MarkdownSection{ + Level: 2, + Heading: "Component Releases", } - - if resp.NextPage == 0 { - break - } - opt.Page = resp.NextPage - } - - return nil, nil -} - -func (r *releaseDrafter) guessNextVersionFromLatestRelease(ctx context.Context) (string, error) { - latest, _, err := r.client.GetV3Client().Repositories.GetLatestRelease(ctx, r.client.Organization(), r.repoName) - if err != nil { - return "", errors.Wrap(err, "unable to find latest release") - } - if latest != nil && latest.TagName != nil { - groups := utils.RegexCapture(utils.SemanticVersionMatcher, *latest.TagName) - t := groups["full_match"] - t = strings.TrimPrefix(t, "v") - latestTag, err := semver.Parse(t) - if err != nil { - r.logger.Warnw("latest release of repository was not a semver tag", "repository", r.repoName, "latest-tag", *latest.TagName) - } else { - latestTag.Patch = latestTag.Patch + 1 - return "v" + latestTag.String(), nil - } - } - return "v0.0.1", nil -} - -func (r *releaseDrafter) updateReleaseBody(headline string, org string, priorBody string, component string, componentVersion semver.Version, componentBody *string) string { - m := utils.ParseMarkdown(priorBody) - - releaseSection := m.FindSectionByHeading(1, headline) - if releaseSection == nil { - releaseSection = &utils.MarkdownSection{ - Level: 1, - Heading: headline, - } - m.PrependSection(releaseSection) + releaseSection.AppendChild(componentSection) } // ensure component section var body []string if componentBody != nil { - lines := strings.Split(strings.Replace(*componentBody, `\r\n`, "\n", -1), "\n") - for _, l := range lines { + for _, l := range markdown.SplitLines(*componentBody) { + l := strings.TrimSpace(l) + // TODO: we only add lines from bullet point list for now, but certainly we want to support more in the future. if !strings.HasPrefix(l, "-") && !strings.HasPrefix(l, "*") { continue @@ -227,12 +150,15 @@ func (r *releaseDrafter) updateReleaseBody(headline string, org string, priorBod body = append(body, l) } + + _ = r.prependActionsRequired(m, *componentBody, org, nil) } + heading := fmt.Sprintf("%s v%s", component, componentVersion.String()) - section := m.FindSectionByHeadingPrefix(2, component) + section := m.FindSectionByHeadingPrefix(3, component) if section == nil { - releaseSection.AppendChild(&utils.MarkdownSection{ - Level: 2, + componentSection.AppendChild(&markdown.MarkdownSection{ + Level: 3, Heading: heading, ContentLines: body, }) @@ -249,20 +175,148 @@ func (r *releaseDrafter) updateReleaseBody(headline string, org string, priorBod } } - return strings.Trim(strings.TrimSpace(m.String()), "\n") + return m.String() } // appends a merged pull request to the release draft func (r *releaseDrafter) appendMergedPR(ctx context.Context, title string, number int64, author string, p *releaseDrafterParams) error { _, ok := r.repoMap[p.RepositoryName] if ok { - r.logger.Debugw("skip adding merged pull request to release draft because of special handling in release vector", "repo", p.RepositoryName) + // if there is an ACTIONS_REQUIRED block, we want to add it (even when it's a release vector handled repository) + + if p.ComponentReleaseInfo == nil { + r.logger.Debugw("skip adding merged pull request to release draft because of special handling in release vector", "repo", p.RepositoryName) + return nil + } + + infos, err := r.releaseInfos(ctx) + if err != nil { + return err + } + + m := markdown.Parse(infos.body) + + issueSuffix := fmt.Sprintf("(%s/%s#%d)", r.client.Organization(), p.RepositoryName, number) + err = r.prependActionsRequired(m, *p.ComponentReleaseInfo, r.client.Organization(), &issueSuffix) + if err != nil { + r.logger.Debugw("skip adding merged pull request to release draft", "reason", err, "repo", p.RepositoryName) + return nil + } + + body := m.String() + + return r.createOrUpdateRelease(ctx, infos, body, p) + } + + infos, err := r.releaseInfos(ctx) + if err != nil { + return err + } + + body := r.appendPullRequest(r.client.Organization(), infos.body, p.RepositoryName, title, number, author, p.ComponentReleaseInfo) + + return r.createOrUpdateRelease(ctx, infos, body, p) +} + +func (r *releaseDrafter) appendPullRequest(org string, priorBody string, repo string, title string, number int64, author string, prBody *string) string { + m := markdown.Parse(priorBody) + + l := fmt.Sprintf("* %s (%s/%s#%d) @%s", title, org, repo, number, author) + + body := []string{l} + + section := m.FindSectionByHeading(1, r.prHeadline) + if section == nil { + if r.prDescription != nil { + body = append([]string{*r.prDescription}, body...) + } + + m.AppendSection(&markdown.MarkdownSection{ + Level: 1, + Heading: r.prHeadline, + ContentLines: body, + }) + } else { + alreadyPresent := false + for _, existingLine := range section.ContentLines { + if existingLine == l { + alreadyPresent = true + } + } + if !alreadyPresent { + section.AppendContent(body) + } + } + + if prBody != nil { + issueSuffix := fmt.Sprintf("(%s/%s#%d)", org, repo, number) + _ = r.prependActionsRequired(m, *prBody, org, &issueSuffix) + } + + return m.String() +} + +func (r *releaseDrafter) prependActionsRequired(m *markdown.Markdown, body string, org string, issueSuffix *string) error { + actionBlock, err := markdown.ExtractAnnotatedBlock("ACTIONS_REQUIRED", body) + if err != nil { + return err + } + + if issueSuffix != nil { + actionBlock += " " + *issueSuffix + } + + actionBody := markdown.ToListItem(actionBlock) + if len(body) == 0 { + return err + } + + headline := "Required Actions" + + releaseSection := ensureReleaseSection(m, r.draftHeadline) + + section := releaseSection.FindSectionByHeading(2, headline) + if section != nil { + if strings.Contains(strings.Join(section.ContentLines, ""), strings.Join(actionBody, "")) { + // idempotence check: hint was already added + return nil + } + section.AppendContent(actionBody) return nil } + releaseSection.PrependChild(&markdown.MarkdownSection{ + Level: 2, + Heading: headline, + ContentLines: actionBody, + }) + + return nil +} + +func ensureReleaseSection(m *markdown.Markdown, headline string) *markdown.MarkdownSection { + releaseSection := m.FindSectionByHeading(1, headline) + if releaseSection == nil { + releaseSection = &markdown.MarkdownSection{ + Level: 1, + Heading: headline, + } + m.PrependSection(releaseSection) + } + + return releaseSection +} + +type releaseInfos struct { + existing *v3.RepositoryRelease + releaseTag string + body string +} + +func (r *releaseDrafter) releaseInfos(ctx context.Context) (*releaseInfos, error) { existingDraft, err := findExistingReleaseDraft(ctx, r.client, r.repoName) if err != nil { - return err + return nil, err } var releaseTag string @@ -271,28 +325,80 @@ func (r *releaseDrafter) appendMergedPR(ctx context.Context, title string, numbe } else { releaseTag, err = r.guessNextVersionFromLatestRelease(ctx) if err != nil { - return err + return nil, err } } - var priorBody string + var body string if existingDraft != nil && existingDraft.Body != nil { - priorBody = *existingDraft.Body + body = *existingDraft.Body + } + + return &releaseInfos{ + existing: existingDraft, + releaseTag: releaseTag, + body: body, + }, nil +} + +func (r *releaseDrafter) guessNextVersionFromLatestRelease(ctx context.Context) (string, error) { + latest, _, err := r.client.GetV3Client().Repositories.GetLatestRelease(ctx, r.client.Organization(), r.repoName) + if err != nil { + return "", errors.Wrap(err, "unable to find latest release") + } + if latest != nil && latest.TagName != nil { + groups := utils.RegexCapture(utils.SemanticVersionMatcher, *latest.TagName) + t := groups["full_match"] + t = strings.TrimPrefix(t, "v") + latestTag, err := semver.Parse(t) + if err != nil { + r.logger.Warnw("latest release of repository was not a semver tag", "repository", r.repoName, "latest-tag", *latest.TagName) + } else { + latestTag.Patch = latestTag.Patch + 1 + return "v" + latestTag.String(), nil + } } + return "v0.0.1", nil +} - body := r.appendPullRequest(r.prHeadline, r.client.Organization(), priorBody, p.RepositoryName, title, number, author) +func findExistingReleaseDraft(ctx context.Context, client *clients.Github, repoName string) (*github.RepositoryRelease, error) { + opt := &github.ListOptions{ + PerPage: 100, + } - if existingDraft != nil { - existingDraft.Body = &body - _, _, err := r.client.GetV3Client().Repositories.EditRelease(ctx, r.client.Organization(), r.repoName, existingDraft.GetID(), existingDraft) + for { + releases, resp, err := client.GetV3Client().Repositories.ListReleases(ctx, client.Organization(), repoName, opt) + if err != nil { + return nil, errors.Wrap(err, "error retrieving releases") + } + + for _, release := range releases { + if release.Draft != nil && *release.Draft { + return release, nil + } + } + + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + return nil, nil +} + +func (r *releaseDrafter) createOrUpdateRelease(ctx context.Context, infos *releaseInfos, body string, p *releaseDrafterParams) error { + if infos.existing != nil { + infos.existing.Body = &body + _, _, err := r.client.GetV3Client().Repositories.EditRelease(ctx, r.client.Organization(), r.repoName, infos.existing.GetID(), infos.existing) if err != nil { return errors.Wrap(err, "unable to update release draft") } - r.logger.Infow("release draft updated", "repository", r.repoName, "trigger-component", p.RepositoryName, "pull-request", title) + r.logger.Infow("release draft updated", "repository", r.repoName, "trigger-component", p.RepositoryName, "version", p.TagName) } else { newDraft := &github.RepositoryRelease{ - TagName: v3.String(releaseTag), - Name: v3.String(fmt.Sprintf(r.titleTemplate, releaseTag)), + TagName: v3.String(infos.releaseTag), + Name: v3.String(fmt.Sprintf(r.titleTemplate, infos.releaseTag)), Body: &body, Draft: v3.Bool(true), } @@ -300,33 +406,8 @@ func (r *releaseDrafter) appendMergedPR(ctx context.Context, title string, numbe if err != nil { return errors.Wrap(err, "unable to create release draft") } - r.logger.Infow("new release draft created", "repository", r.repoName, "trigger-component", p.RepositoryName, "pull-request", title) + r.logger.Infow("new release draft created", "repository", r.repoName, "trigger-component", p.RepositoryName, "version", p.TagName) } return nil } - -func (r *releaseDrafter) appendPullRequest(headline string, org string, priorBody string, repo string, title string, number int64, author string) string { - m := utils.ParseMarkdown(priorBody) - - l := fmt.Sprintf("* %s (%s/%s#%d) @%s", title, org, repo, number, author) - - body := []string{l} - - section := m.FindSectionByHeading(1, headline) - if section == nil { - if r.prDescription != nil { - body = append([]string{*r.prDescription}, body...) - } - - m.AppendSection(&utils.MarkdownSection{ - Level: 1, - Heading: headline, - ContentLines: body, - }) - } else { - section.AppendContent(body) - } - - return strings.Trim(strings.TrimSpace(m.String()), "\n") -} diff --git a/pkg/webhooks/github/actions/release_drafter_test.go b/pkg/webhooks/github/actions/release_drafter_test.go index 6f0d0dd..9b518dd 100644 --- a/pkg/webhooks/github/actions/release_drafter_test.go +++ b/pkg/webhooks/github/actions/release_drafter_test.go @@ -13,7 +13,7 @@ func TestReleaseDrafter_updateReleaseBody(t *testing.T) { tests := []struct { name string org string - version string + headline string priorBody string component string componentVersion semver.Version @@ -23,114 +23,164 @@ func TestReleaseDrafter_updateReleaseBody(t *testing.T) { }{ { name: "creating fresh release draft", - version: "v0.1.0", + headline: "General", org: "metal-stack", component: "metal-robot", componentVersion: semver.MustParse("0.2.4"), componentBody: v3.String(`- Adding new feature - Fixed a bug`), priorBody: "", - want: `# v0.1.0 -## metal-robot v0.2.4 + want: `# General +## Component Releases +### metal-robot v0.2.4 - Adding new feature - Fixed a bug`, }, { name: "creating fresh release draft, no release body", - version: "v0.1.0", + headline: "General", org: "metal-stack", component: "metal-robot", componentVersion: semver.MustParse("0.2.4"), componentBody: nil, priorBody: "", - want: `# v0.1.0 -## metal-robot v0.2.4`, + want: `# General +## Component Releases +### metal-robot v0.2.4`, }, { name: "creating fresh release draft, empty release body", - version: "v0.1.0", + headline: "General", org: "metal-stack", component: "metal-robot", componentVersion: semver.MustParse("0.2.4"), componentBody: v3.String(""), priorBody: "", - want: `# v0.1.0 -## metal-robot v0.2.4`, + want: `# General +## Component Releases +### metal-robot v0.2.4`, }, { name: "adding new section to existing release draft", - version: "v0.1.0", + headline: "General", org: "metal-stack", component: "metal-robot", componentVersion: semver.MustParse("0.2.4"), componentBody: v3.String(`- Adding new feature - Fixed a bug`), - priorBody: `# v0.1.0 -## metal-test v0.1.0 + priorBody: `# General +## Component Releases +### metal-test v0.1.0 - 42`, - want: `# v0.1.0 -## metal-test v0.1.0 + want: `# General +## Component Releases +### metal-test v0.1.0 - 42 -## metal-robot v0.2.4 +### metal-robot v0.2.4 - Adding new feature - Fixed a bug`, }, { name: "updating release draft with another component release", - version: "v0.1.0", + headline: "General", org: "metal-stack", component: "metal-robot", componentVersion: semver.MustParse("0.2.5"), componentBody: v3.String(`## General Changes\r\n\r\n* Fix (#123) @Gerrit91\r\n`), - priorBody: `# v0.1.0 -## metal-test v0.1.0 + priorBody: `# General +## Component Releases +### metal-test v0.1.0 - 42 -## metal-robot v0.2.4 +### metal-robot v0.2.4 - Adding new feature - Fixed a bug`, - want: `# v0.1.0 -## metal-test v0.1.0 + want: `# General +## Component Releases +### metal-test v0.1.0 - 42 -## metal-robot v0.2.5 +### metal-robot v0.2.5 - Adding new feature - Fixed a bug * Fix (metal-stack/metal-robot#123) @Gerrit91`, }, { name: "updating release draft when there is a pull request summary", - version: "v0.1.0", + headline: "General", org: "metal-stack", component: "metal-robot", componentVersion: semver.MustParse("0.2.5"), componentBody: v3.String(`## General Changes\r\n\r\n* Fix (#123) @Gerrit91\r\n`), - priorBody: `# v0.1.0 -## metal-test v0.1.0 + priorBody: `# General +## Component Releases +### metal-test v0.1.0 - 42 # Merged Pull Requests Some description * Some new feature (metal-stack/metal-robot#11) @metal-robot`, - want: `# v0.1.0 -## metal-test v0.1.0 + want: `# General +## Component Releases +### metal-test v0.1.0 - 42 -## metal-robot v0.2.5 +### metal-robot v0.2.5 * Fix (metal-stack/metal-robot#123) @Gerrit91 # Merged Pull Requests Some description * Some new feature (metal-stack/metal-robot#11) @metal-robot`, }, + { + name: "extracting required actions", + headline: "General", + org: "metal-stack", + component: "metal-robot", + componentVersion: semver.MustParse("0.2.5"), + componentBody: v3.String("## General Changes\r\n\r\n* Fix (#123) @Gerrit91\r\n```ACTIONS_REQUIRED\r\nAPI has changed\r\n```"), + priorBody: `# General +## Component Releases +### metal-test v0.1.0 +- 42 +# Merged Pull Requests +Some description +* Some new feature (metal-stack/metal-robot#11) @metal-robot`, + want: `# General +## Required Actions +* API has changed +## Component Releases +### metal-test v0.1.0 +- 42 +### metal-robot v0.2.5 +* Fix (metal-stack/metal-robot#123) @Gerrit91 +# Merged Pull Requests +Some description +* Some new feature (metal-stack/metal-robot#11) @metal-robot`, + }, + { + name: "extracting required actions, empty release bidy", + headline: "General", + org: "metal-stack", + component: "metal-robot", + componentVersion: semver.MustParse("0.2.5"), + componentBody: v3.String("## General Changes\r\n\r\n* Fix (#123) @Gerrit91\r\n```ACTIONS_REQUIRED\r\nAPI has changed\r\n```"), + want: `# General +## Required Actions +* API has changed +## Component Releases +### metal-robot v0.2.5 +* Fix (metal-stack/metal-robot#123) @Gerrit91`, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &releaseDrafter{ - logger: zaptest.NewLogger(t).Sugar(), - client: nil, + logger: zaptest.NewLogger(t).Sugar(), + client: nil, + draftHeadline: tt.headline, } - res := r.updateReleaseBody(tt.version, tt.org, tt.priorBody, tt.component, tt.componentVersion, tt.componentBody) + res := r.updateReleaseBody(tt.org, tt.priorBody, tt.component, tt.componentVersion, tt.componentBody) if diff := cmp.Diff(tt.want, res); diff != "" { t.Errorf("ReleaseDrafter.updateReleaseBody(), diff: %v", diff) t.Logf("want\n=====\n%s\n\ngot\n=====\n%s", tt.want, res) } - idempotent := r.updateReleaseBody(tt.version, tt.org, res, tt.component, tt.componentVersion, tt.componentBody) + idempotent := r.updateReleaseBody(tt.org, res, tt.component, tt.componentVersion, tt.componentBody) if diff := cmp.Diff(tt.want, idempotent); diff != "" { t.Errorf("not idempotent: %v", diff) t.Logf("want\n=====\n%s\n\ngot\n=====\n%s", tt.want, res) @@ -147,6 +197,7 @@ func Test_releaseDrafter_appendPullRequest(t *testing.T) { title string number int64 author string + prBody *string priorBody string description string @@ -171,13 +222,13 @@ func Test_releaseDrafter_appendPullRequest(t *testing.T) { number: 11, author: "metal-robot", description: "Some description", - priorBody: `# v0.1.0 + priorBody: `# General ## metal-test v0.1.0 - 42 ## metal-robot v0.2.4 - Adding new feature - Fixed a bug`, - want: `# v0.1.0 + want: `# General ## metal-test v0.1.0 - 42 ## metal-robot v0.2.4 @@ -194,7 +245,7 @@ Some description title: "Second PR", number: 12, author: "metal-robot", - priorBody: `# v0.1.0 + priorBody: `# General ## metal-test v0.1.0 - 42 ## metal-robot v0.2.4 @@ -202,7 +253,7 @@ Some description - Fixed a bug # Merged Pull Requests * Some new feature (metal-stack/metal-robot#11) @metal-robot`, - want: `# v0.1.0 + want: `# General ## metal-test v0.1.0 - 42 ## metal-robot v0.2.4 @@ -212,23 +263,40 @@ Some description * Some new feature (metal-stack/metal-robot#11) @metal-robot * Second PR (metal-stack/metal-robot#12) @metal-robot`, }, + { + name: "creating fresh release draft with actions required", + org: "metal-stack", + repo: "metal-robot", + title: "Some new feature", + number: 11, + author: "metal-robot", + priorBody: "", + prBody: v3.String("This is a new feature\r\n```ACTIONS_REQUIRED\r\nAPI has changed\r\n```"), + want: `# General +## Required Actions +* API has changed (metal-stack/metal-robot#11) +# Merged Pull Requests +* Some new feature (metal-stack/metal-robot#11) @metal-robot`, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := &releaseDrafter{ - logger: zaptest.NewLogger(t).Sugar(), - client: nil, + logger: zaptest.NewLogger(t).Sugar(), + client: nil, + prHeadline: "Merged Pull Requests", + draftHeadline: "General", } if tt.description != "" { r.prDescription = &tt.description } - res := r.appendPullRequest("Merged Pull Requests", tt.org, tt.priorBody, tt.repo, tt.title, tt.number, tt.author) + res := r.appendPullRequest(tt.org, tt.priorBody, tt.repo, tt.title, tt.number, tt.author, tt.prBody) if diff := cmp.Diff(tt.want, res); diff != "" { t.Errorf("ReleaseDrafter.appendPullRequest(), diff: %v", diff) t.Logf("want\n=====\n%s\n\ngot\n=====\n%s", tt.want, res) } - idempotent := r.appendPullRequest("Merged Pull Requests", tt.org, tt.priorBody, tt.repo, tt.title, tt.number, tt.author) + idempotent := r.appendPullRequest(tt.org, tt.priorBody, tt.repo, tt.title, tt.number, tt.author, tt.prBody) if diff := cmp.Diff(tt.want, idempotent); diff != "" { t.Errorf("not idempotent: %v", diff) t.Logf("want\n=====\n%s\n\ngot\n=====\n%s", tt.want, res)