internal/changelog/renderer.go (189 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package changelog
import (
"bytes"
"fmt"
"html/template"
"log"
"path"
"strings"
"github.com/elastic/elastic-agent-changelog-tool/internal/assets"
"github.com/spf13/afero"
"github.com/spf13/viper"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
type Renderer struct {
changelog Changelog
fs afero.Fs
// dest is the destination location where the changelog is written to
dest string
templ string
}
func NewRenderer(fs afero.Fs, c Changelog, dest string, templ string) *Renderer {
return &Renderer{
changelog: c,
fs: fs,
dest: dest,
templ: templ,
}
}
func (r Renderer) Render() error {
log.Printf("render changelog for version: %s\n", r.changelog.Version)
tpl, err := r.Template()
if err != nil {
log.Fatal(err)
}
type TemplateData struct {
Component string
Version string
Changelog Changelog
Kinds map[Kind]bool
BreakingChange map[string][]Entry
Deprecation map[string][]Entry
BugFix map[string][]Entry
Enhancement map[string][]Entry
Feature map[string][]Entry
KnownIssue map[string][]Entry
Security map[string][]Entry
Upgrade map[string][]Entry
Other map[string][]Entry
}
td := TemplateData{
buildTitleByComponents(r.changelog.Entries), r.changelog.Version, r.changelog,
collectKinds(r.changelog.Entries),
collectByKindMap(r.changelog.Entries, BreakingChange),
collectByKindMap(r.changelog.Entries, Deprecation),
collectByKindMap(r.changelog.Entries, BugFix),
collectByKindMap(r.changelog.Entries, Enhancement),
collectByKindMap(r.changelog.Entries, Feature),
collectByKindMap(r.changelog.Entries, KnownIssue),
collectByKindMap(r.changelog.Entries, Security),
collectByKindMap(r.changelog.Entries, Upgrade),
collectByKindMap(r.changelog.Entries, Other),
}
tmpl, err := template.New("release-notes").
Funcs(template.FuncMap{
"crossreferenceList": func(ids []string) string {
return strings.Join(ids, "-")
},
// nolint:staticcheck // ignoring for now, supports for multiple component is not implemented
"linkPRSource": func(component string, ids []string) string {
res := make([]string, len(ids))
for i, id := range ids {
res[i] = fmt.Sprintf("{%s-pull}%v[#%v]", component, id, id)
}
return strings.Join(res, " ")
},
// nolint:staticcheck // ignoring for now, supports for multiple component is not implemented
"linkIssueSource": func(component string, ids []string) string {
res := make([]string, len(ids))
for i, id := range ids {
res[i] = fmt.Sprintf("{%s-issue}%v[#%v]", component, id, id)
}
return strings.Join(res, " ")
},
// Capitalize sentence and ensure ends with .
"beautify": func(s1 string) string {
s2 := strings.Builder{}
s2.WriteString(cases.Title(language.English).String(s1))
if !strings.HasSuffix(s1, ".") {
s2.WriteString(".")
}
return s2.String()
},
// Ensure components have section styling
"header2": func(s1 string) string {
s2 := strings.Builder{}
s2.WriteString(s1)
if !strings.HasSuffix(s1, "::") && s1 != "" {
s2.WriteString("::")
}
return s2.String()
},
}).
Parse(string(tpl))
if err != nil {
panic(err)
}
var data bytes.Buffer
err = tmpl.Execute(&data, td)
if err != nil {
panic(err)
}
outFile := path.Join(r.dest, fmt.Sprintf("%s.asciidoc", r.changelog.Version))
log.Printf("saving changelog in %s\n", outFile)
return afero.WriteFile(r.fs, outFile, data.Bytes(), changelogFilePerm)
}
func (r Renderer) Template() ([]byte, error) {
var data []byte
var err error
if embeddedFileName, ok := assets.GetEmbeddedTemplates()[r.templ]; ok {
data, err = assets.AsciidocTemplate.ReadFile(embeddedFileName)
if err != nil {
return []byte{}, fmt.Errorf("cannot read embedded template: %s %w", embeddedFileName, err)
}
return data, nil
}
data, err = afero.ReadFile(r.fs, r.templ)
if err != nil {
return []byte{}, fmt.Errorf("cannot read custom template: %w", err)
}
return data, nil
}
func collectKinds(items []Entry) map[Kind]bool {
// NOTE: collect kinds in a set-like map to avoid duplicates
kinds := map[Kind]bool{}
for _, e := range items {
kinds[e.Kind] = true
}
return kinds
}
func collectByKindMap(entries []Entry, k Kind) map[string][]Entry {
componentEntries := map[string][]Entry{}
for _, e := range entries {
if e.Kind == k {
if len(e.Component) > 0 {
componentEntries[e.Component] = append(componentEntries[e.Component], e)
} else {
componentEntries[""] = append(componentEntries[""], e)
}
}
}
return componentEntries
}
func collectByKind(items []Entry, k Kind) []Entry {
entries := []Entry{}
for _, e := range items {
if e.Kind == k {
entries = append(entries, e)
}
}
return entries
}
func buildTitleByComponents(entries []Entry) string {
configComponents := viper.GetStringSlice("components")
switch len(configComponents) {
case 0:
return ""
case 1:
c := configComponents[0]
for _, e := range entries {
if c != e.Component && len(e.Component) > 0 {
log.Fatalf("Component [%s] not found in config", e.Component)
}
}
return c
default:
var match string
for _, e := range entries {
if e.Component == "" {
log.Fatalf("Component cannot be assumed, choose it from config values: %s", e.File.Name)
}
match = ""
for _, c := range configComponents {
if e.Component != c {
continue
}
match = e.Component
}
if match == "" {
log.Fatalf("Component [%s] not found in config", e.Component)
}
}
return match
}
}