diff --git a/changelog/fragments/1761335496-show-subsections.yaml b/changelog/fragments/1761335496-show-subsections.yaml new file mode 100644 index 0000000..459767a --- /dev/null +++ b/changelog/fragments/1761335496-show-subsections.yaml @@ -0,0 +1,46 @@ +# REQUIRED +# Kind can be one of: +# - breaking-change: a change to previously-documented behavior +# - deprecation: functionality that is being removed in a later release +# - bug-fix: fixes a problem in a previous version +# - enhancement: extends functionality but does not break or fix existing behavior +# - feature: new functionality +# - known-issue: problems that we are aware of in a given version +# - security: impacts on the security of a product or a user’s deployment. +# - upgrade: important information for someone upgrading from a prior version +# - other: does not fit into any of the other categories +kind: feature + +# REQUIRED for all kinds +# Change summary; a 80ish characters long description of the change. +summary: Add a new `subsections` configuration option that specifies whether to show `component`s as subsections in rendered release notes. + +# REQUIRED for breaking-change, deprecation, known-issue +# Long description; in case the summary is not enough to describe the change +# this field accommodate a description without length limits. +description: This should be set to `true` in repos with multiple `component`s. For example, the Beats repo, which contains changelog entries for multiple `component`s like `filebeat`, `metricbeat`, etc. + +# REQUIRED for breaking-change, deprecation, known-issue +# impact: + +# REQUIRED for breaking-change, deprecation, known-issue +# action: + +# REQUIRED for all kinds +# Affected component; usually one of "elastic-agent", "fleet-server", "filebeat", "metricbeat", "auditbeat", "all", etc. +component: + +# AUTOMATED +# OPTIONAL to manually add other PR URLs +# PR URL: A link the PR that added the changeset. +# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added. +# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number. +# Please provide it if you are adding a fragment for a different PR. +pr: + - https://github.com/elastic/elastic-agent-changelog-tool/pull/221 + +# AUTOMATED +# OPTIONAL to manually add other issue URLs +# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of). +# If not present is automatically filled by the tooling with the issue linked to the PR number. +# issue: https://github.com/owner/repo/1234 diff --git a/cmd/render.go b/cmd/render.go index 51399f5..781071a 100644 --- a/cmd/render.go +++ b/cmd/render.go @@ -23,7 +23,7 @@ var RenderLongDescription = `Use this command to render the consolidated changel func RenderCmd(fs afero.Fs) *cobra.Command { renderCmd := &cobra.Command{ Use: "render", - Short: "Render a changelog in an asciidoc file", + Short: "Render a changelog in an AsciiDoc or Markdown file", Long: RenderLongDescription, Args: func(cmd *cobra.Command, args []string) error { return nil @@ -32,6 +32,7 @@ func RenderCmd(fs afero.Fs) *cobra.Command { dest := viper.GetString("changelog_destination") renderedDest := viper.GetString("rendered_changelog_destination") repo := viper.GetString("repo") + subsections := viper.GetBool("subsections") version, err := cmd.Flags().GetString("version") if err != nil { @@ -54,25 +55,25 @@ func RenderCmd(fs afero.Fs) *cobra.Command { } if file_type == "asciidoc" { - r := changelog.NewRenderer(fs, c, renderedDest, "asciidoc-embedded", repo) + r := changelog.NewRenderer(fs, c, renderedDest, "asciidoc-embedded", repo, subsections) if err := r.Render(); err != nil { return fmt.Errorf("cannot build asciidoc file: %w", err) } } else if file_type == "markdown" { - r_index := changelog.NewRenderer(fs, c, renderedDest, "markdown-index", repo) + r_index := changelog.NewRenderer(fs, c, renderedDest, "markdown-index", repo, subsections) if err := r_index.Render(); err != nil { return fmt.Errorf("cannot build markdown file: %w", err) } - r_breaking := changelog.NewRenderer(fs, c, renderedDest, "markdown-breaking", repo) + r_breaking := changelog.NewRenderer(fs, c, renderedDest, "markdown-breaking", repo, subsections) if err := r_breaking.Render(); err != nil { return fmt.Errorf("cannot build markdown file: %w", err) } - r_deprecations := changelog.NewRenderer(fs, c, renderedDest, "markdown-deprecations", repo) + r_deprecations := changelog.NewRenderer(fs, c, renderedDest, "markdown-deprecations", repo, subsections) if err := r_deprecations.Render(); err != nil { return fmt.Errorf("cannot build markdown file: %w", err) } } else { - r := changelog.NewRenderer(fs, c, renderedDest, template, repo) + r := changelog.NewRenderer(fs, c, renderedDest, template, repo, subsections) if err := r.Render(); err != nil { return fmt.Errorf("cannot build file: %w", err) } diff --git a/docs/configuration.md b/docs/configuration.md index f108bf7..cd88b47 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -24,6 +24,7 @@ When generating Markdown files, at a minimum you should set the following settin | `owner` (required) | `elastic` | The owner of the GitHub repo. | | `repo` (required) | ‒ | The name of the GitHub repo. | | `rendered_changelog_destination` | `changelog` | The directory where you want to put the generated files.

When generating Markdown files, this should probably be `docs/release-notes/_snippets`. | +| `subsections` | `false` | Whether to show `component`s as subsections in rendered release notes. This should be set to `true` in repos with multiple `component`s (for example, Beats). | ## Supported Environment Variables diff --git a/internal/assets/markdown-breaking-template.md.tmpl b/internal/assets/markdown-breaking-template.md.tmpl index 5a2db49..ad1b298 100644 --- a/internal/assets/markdown-breaking-template.md.tmpl +++ b/internal/assets/markdown-breaking-template.md.tmpl @@ -1,7 +1,8 @@ ## {{.Version}} [{{.Repo}}-{{.Version}}-breaking-changes] -{{ if .BreakingChange -}}{{ range $k, $v := .BreakingChange }}{{ range $item := $v }} +{{ if .BreakingChange -}}{{ range $k, $v := .BreakingChange }}{{ $k | header2 }}{{ range $item := $v }} + ::::{dropdown} {{ $item.Summary | beautify }} -{{ if $item.Description }}{{ $item.Description }}{{ end }} +{{ if $item.Description }}{{ $item.Description }}{{ else }}% Describe the functionality that changed{{ end }} For more information, check {{ linkPRSource $item.Component $item.LinkedPR }}{{ linkIssueSource $item.Component $item.LinkedIssue }}. diff --git a/internal/assets/markdown-deprecations-template.md.tmpl b/internal/assets/markdown-deprecations-template.md.tmpl index cf5d066..bdd0219 100644 --- a/internal/assets/markdown-deprecations-template.md.tmpl +++ b/internal/assets/markdown-deprecations-template.md.tmpl @@ -1,8 +1,8 @@ ## {{.Version}} [{{.Repo}}-{{.Version}}-deprecations] -{{ if .Deprecation -}}{{ range $k, $v := .Deprecation }}{{ range $item := $v }} +{{ if .Deprecation -}}{{ range $k, $v := .Deprecation }}{{ $k | header2 }}{{ range $item := $v }} ::::{dropdown} {{ $item.Summary | beautify }} -{{ if $item.Description }}{{ $item.Description }}{{ end }} +{{ if $item.Description }}{{ $item.Description }}{{ else }}% Describe the functionality that was deprecated{{ end }} For more information, check {{ linkPRSource $item.Component $item.LinkedPR }}{{ linkIssueSource $item.Component $item.LinkedIssue }}. diff --git a/internal/assets/markdown-index-template.md.tmpl b/internal/assets/markdown-index-template.md.tmpl index 3ea38be..be5dbc0 100644 --- a/internal/assets/markdown-index-template.md.tmpl +++ b/internal/assets/markdown-index-template.md.tmpl @@ -1,30 +1,24 @@ ## {{.Version}} [{{.Repo}}-release-notes-{{.Version}}] {{ if or .KnownIssue .BreakingChange .Deprecation }} {{ other_links }}{{- end }} -{{ if or .Feature .Enhancement .Security .BugFix }}{{ if or .Feature .Enhancement }} +{{ if or .Feature .Enhancement .Security .BugFix }} +{{ if or .Feature .Enhancement }} ### Features and enhancements [{{.Repo}}-{{.Version}}-features-enhancements] -{{ if .Feature }}{{ range $k, $v := .Feature }}{{ range $item := $v }} -* {{ $item.Summary | beautify }} {{ linkPRSource $item.Component $item.LinkedPR }} {{ linkIssueSource $item.Component $item.LinkedIssue }}{{ if $item.Description }} -{{ $item.Description | indent }} -{{- end }} -{{- end }}{{- end }}{{- end }}{{ if .Enhancement }}{{ range $k, $v := .Enhancement }}{{ range $item := $v }} +{{ $additions := combine .Feature .Enhancement }}{{ range $k, $v := $additions }}{{ header2 $k }} +{{ range $item := $v }} * {{ $item.Summary | beautify }} {{ linkPRSource $item.Component $item.LinkedPR }} {{ linkIssueSource $item.Component $item.LinkedIssue }}{{ if $item.Description }} {{ $item.Description | indent }} {{- end }} {{- end }}{{- end }}{{- end }} -{{- end }} {{ if or .Security .BugFix }} ### Fixes [{{.Repo}}-{{.Version}}-fixes] -{{ if .Security }}{{ range $k, $v := .Security }}{{ range $item := $v }} +{{ $fixes := combine .Security .BugFix }}{{ range $k, $v := $fixes }}{{ header2 $k }} +{{ range $item := $v }} * {{ $item.Summary | beautify }} {{ linkPRSource $item.Component $item.LinkedPR }} {{ linkIssueSource $item.Component $item.LinkedIssue }}{{ if $item.Description }} {{ $item.Description | indent }} {{- end }} -{{- end }}{{- end }}{{- end }}{{ if .BugFix }}{{ range $k, $v := .BugFix }}{{ range $item := $v }} -* {{ $item.Summary | beautify }} {{ linkPRSource $item.Component $item.LinkedPR }} {{ linkIssueSource $item.Component $item.LinkedIssue }}{{ if $item.Description }} -{{ $item.Description | indent }} -{{- end }} -{{- end }}{{- end }}{{- end }}{{- end }} +{{- end }}{{- end }}{{- end }} {{ else }} _No new features, enhancements, or fixes._ {{- end }} diff --git a/internal/changelog/renderer.go b/internal/changelog/renderer.go index bfa7d43..fcb25ef 100644 --- a/internal/changelog/renderer.go +++ b/internal/changelog/renderer.go @@ -23,18 +23,20 @@ type Renderer struct { changelog Changelog fs afero.Fs // dest is the destination location where the changelog is written to - dest string - templ string - repo string + dest string + templ string + repo string + subsections bool } -func NewRenderer(fs afero.Fs, c Changelog, dest string, templ string, repo string) *Renderer { +func NewRenderer(fs afero.Fs, c Changelog, dest string, templ string, repo string, subsections bool) *Renderer { return &Renderer{ - changelog: c, - fs: fs, - dest: dest, - templ: templ, - repo: repo, + changelog: c, + fs: fs, + dest: dest, + templ: templ, + repo: repo, + subsections: subsections, } } @@ -47,11 +49,12 @@ func (r Renderer) Render() error { } type TemplateData struct { - Component string - Version string - Repo string - Changelog Changelog - Kinds map[Kind]bool + Component string + Version string + Repo string + Changelog Changelog + Kinds map[Kind]bool + Subsections bool // In Markdown, this goes to release notes Enhancement map[string][]Entry @@ -75,6 +78,7 @@ func (r Renderer) Render() error { r.repo, r.changelog, collectKinds(r.changelog.Entries), + r.subsections, // In Markdown, this goes to release notes collectByKindMap(r.changelog.Entries, Enhancement), collectByKindMap(r.changelog.Entries, Feature), @@ -159,15 +163,33 @@ func (r Renderer) Render() error { if len(links) > 0 { return fmt.Sprintf( "_This release also includes: %s._", - strings.Join(links, " and"), + strings.Join(links, " and "), ) } else { return "" } }, // Ensure components have section styling - "header2": func(s1 string) string { - return fmt.Sprintf("**%s**", s1) + "header2": func(s string) string { + if r.subsections { + s = strings.ToUpper(string(s[0])) + s[1:] + s = strings.ReplaceAll(s, "-", " ") + return fmt.Sprintf("\n\n**%s**", s) + } else { + return "" + } + }, + "combine": func(map1 map[string][]Entry, map2 map[string][]Entry) map[string][]Entry { + combinedMap := make(map[string][]Entry) + // Start with a copy of map1 entries + for k, v := range map1 { + combinedMap[k] = append([]Entry{}, v...) + } + // Merge entries from map2, appending to any existing entries + for k, v := range map2 { + combinedMap[k] = append(combinedMap[k], v...) + } + return combinedMap }, }). Parse(string(tpl)) diff --git a/internal/changelog/renderer_test.go b/internal/changelog/renderer_test.go index a9f5f81..5b06fb3 100644 --- a/internal/changelog/renderer_test.go +++ b/internal/changelog/renderer_test.go @@ -23,6 +23,7 @@ func TestRenderer(t *testing.T) { filename := "0.0.0.yaml" src := "testdata" dest := viper.GetString("changelog_destination") + subsections := viper.GetBool("subsections") t.Log("building changelog from test fragments") builder := changelog.NewBuilder(fs, filename, "0.0.0", src, dest) @@ -34,7 +35,7 @@ func TestRenderer(t *testing.T) { c, err := changelog.FromFile(fs, inFile) require.NoError(t, err) - r := changelog.NewRenderer(fs, c, dest, "asciidoc-embedded", "elastic-agent") + r := changelog.NewRenderer(fs, c, dest, "asciidoc-embedded", "elastic-agent", subsections) err = r.Render() require.Nil(t, err) diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 4dc335c..5f47821 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -61,6 +61,7 @@ func setDefaults() { viper.SetDefault("changelog_destination", "changelog") viper.SetDefault("rendered_changelog_destination", "changelog") viper.SetDefault("file_type", "markdown") + viper.SetDefault("subsections", false) viper.SetDefault("template", "asciidoc-embedded") }