1package changes 2 3import ( 4 "bytes" 5 "fmt" 6 "strings" 7 "text/template" 8) 9 10// Release represents a single SDK release, which contains all change metadata and their resulting version bumps. 11type Release struct { 12 ID string 13 SchemaVersion int 14 VersionBumps map[string]VersionBump 15 Changes []Change 16} 17 18type changelogModuleEntry struct { 19 Module string 20 Version string 21 Sections map[ChangeType][]Change 22 TopLevel bool 23 ReleaseID string 24} 25 26func (e changelogModuleEntry) Link() string { 27 anchor := "Release-" + strings.ReplaceAll(e.ReleaseID, " ", "-") 28 return fmt.Sprintf("[%s](%s/CHANGELOG.md#%s)", e.Module, e.Module, anchor) 29} 30 31const changelogModule = `* ` + "`" + `{{.Module}}` + "`" + `{{with .Version}} - {{.}}{{end}} 32{{- range $key, $section := .Sections -}} 33{{range $section}} 34 * {{ $key.ChangelogPrefix }}{{.IndentedDescription " "}} 35{{- end -}} 36{{- end -}}` 37 38var changelogTemplate *template.Template 39var rootChangelogTemplate *template.Template 40 41func init() { 42 var err error 43 44 changelogTemplate, err = template.New("changelog-entry").Parse(changelogModule) 45 if err != nil { 46 panic(err) 47 } 48 49 rootChangelogTemplate, err = template.New("root-changelog").Parse(rootChangelogTemplateContents) 50 if err != nil { 51 panic(err) 52 } 53} 54 55// RenderChangelogForModule returns a new markdown section of a module's CHANGELOG based on the Changes in the Release. 56func (r *Release) RenderChangelogForModule(module string, topLevel bool) (string, error) { 57 sections := map[ChangeType][]Change{} 58 59 for _, c := range r.Changes { 60 if topLevel && c.Module == module { 61 sections[c.Type] = append(sections[c.Type], c) 62 } else if !topLevel && c.matches(module) { 63 sections[c.Type] = append(sections[c.Type], c) 64 } 65 } 66 67 if len(sections) == 0 { 68 return "", nil 69 } 70 71 var version string 72 if bump, ok := r.VersionBumps[module]; ok { 73 version = bump.To 74 } 75 76 buff := new(bytes.Buffer) 77 78 err := changelogTemplate.Execute(buff, changelogModuleEntry{ 79 Module: module, 80 Version: version, 81 Sections: sections, 82 ReleaseID: r.ID, 83 TopLevel: topLevel, 84 }) 85 if err != nil { 86 return "", fmt.Errorf("failed to render module %s's changelog entry: %v", module, err) 87 } 88 89 return buff.String(), nil 90} 91 92const rootChangelogTemplateContents = `# Release {{.ID}} 93{{with .AnnouncementsSection}}## Announcements 94{{range .}}{{.}} 95{{end -}} 96{{end}}{{with .ServiceSection}}## Service Client Highlights 97{{range .}}{{.}} 98{{end -}} 99{{end}}{{with .CoreSection}}## Core SDK Highlights 100{{range .}}{{.}} 101{{end -}} 102{{end}}` 103 104// RenderChangelog generates a top level CHANGELOG.md for the Release r. 105func (r *Release) RenderChangelog() (string, error) { 106 buff := new(bytes.Buffer) 107 108 err := rootChangelogTemplate.Execute(buff, r) 109 if err != nil { 110 return "", err 111 } 112 113 return buff.String(), nil 114} 115 116// AffectedModules returns a sorted list of all modules affected by this Release. A module is considered affected if 117// it is the Module of one or more Changes in the Release. 118func (r *Release) AffectedModules() []string { 119 return AffectedModules(r.Changes) 120} 121 122// wildcards returns a sorted list of wildcards Changes whose Module begin with the given prefix. 123func (r *Release) wildcards() []Change { 124 var changes []Change 125 126 for _, c := range r.Changes { 127 if c.isWildcard() { 128 changes = append(changes, c) 129 } 130 } 131 132 return changes 133} 134 135// splitSections groups entries (including wildcard Changes and module Changelog entries) into three groups: Announcements, 136// Services, and Core SDK modules. 137func (r *Release) splitSections() ([]string, []string, []string, error) { 138 const servicePrefix = "service/" 139 140 var announcements []string 141 var services []string 142 var core []string 143 144 for _, c := range r.wildcards() { 145 if c.Type == AnnouncementChangeType { 146 announcements = append(announcements, c.String()) 147 } else if strings.HasPrefix(c.Module, servicePrefix) { 148 services = append(services, c.String()) 149 } else { 150 core = append(core, c.String()) 151 } 152 } 153 154 mods := r.AffectedModules() 155 156 for _, m := range mods { 157 entry, err := r.RenderChangelogForModule(m, true) 158 if err != nil { 159 return nil, nil, nil, err 160 } 161 if entry == "" { 162 continue 163 } 164 165 if strings.HasPrefix(m, servicePrefix) { 166 services = append(services, entry) 167 } else { 168 core = append(core, entry) 169 } 170 } 171 172 return announcements, services, core, nil 173} 174 175// AnnouncementsSection returns a list of Changelog bullet entries that should be included under the Announcements header. 176func (r *Release) AnnouncementsSection() ([]string, error) { 177 announcements, _, _, err := r.splitSections() 178 return announcements, err 179} 180 181// ServiceSection returns a list of Changelog bullet entries that should be included under the Service Clients header. 182func (r *Release) ServiceSection() ([]string, error) { 183 _, services, _, err := r.splitSections() 184 return services, err 185} 186 187// CoreSection returns a list of Changelog bullet entries that should be included under the Core SDK header. 188func (r *Release) CoreSection() ([]string, error) { 189 _, _, core, err := r.splitSections() 190 return core, err 191} 192