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")
}