github.com/gohugoio/hugo@v0.88.1/releaser/releasenotes_writer.go (about) 1 // Copyright 2017-present The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 // Package releaser implements a set of utilities and a wrapper around Goreleaser 15 // to help automate the Hugo release process. 16 package releaser 17 18 import ( 19 "bytes" 20 "fmt" 21 "io" 22 "io/ioutil" 23 "net/http" 24 "os" 25 "path/filepath" 26 "strings" 27 "text/template" 28 "time" 29 ) 30 31 const ( 32 issueLinkTemplate = "[#%d](https://github.com/gohugoio/hugo/issues/%d)" 33 linkTemplate = "[%s](%s)" 34 releaseNotesMarkdownTemplatePatchRelease = ` 35 {{ if eq (len .All) 1 }} 36 This is a bug-fix release with one important fix. 37 {{ else }} 38 This is a bug-fix release with a couple of important fixes. 39 {{ end }} 40 {{ range .All }} 41 {{- if .GitHubCommit -}} 42 * {{ .Subject }} {{ . | commitURL }} {{ . | authorURL }} {{ range .Issues }}{{ . | issue }}{{ end }} 43 {{ else -}} 44 * {{ .Subject }} {{ range .Issues }}{{ . | issue }}{{ end }} 45 {{ end -}} 46 {{- end }} 47 48 49 ` 50 releaseNotesMarkdownTemplate = ` 51 {{- $contribsPerAuthor := .All.ContribCountPerAuthor -}} 52 {{- $docsContribsPerAuthor := .Docs.ContribCountPerAuthor -}} 53 54 This release represents **{{ len .All }} contributions by {{ len $contribsPerAuthor }} contributors** to the main Hugo code base. 55 56 {{- if gt (len $contribsPerAuthor) 3 -}} 57 {{- $u1 := index $contribsPerAuthor 0 -}} 58 {{- $u2 := index $contribsPerAuthor 1 -}} 59 {{- $u3 := index $contribsPerAuthor 2 -}} 60 {{- $u4 := index $contribsPerAuthor 3 -}} 61 {{- $u1.AuthorLink }} leads the Hugo development with a significant amount of contributions, but also a big shoutout to {{ $u2.AuthorLink }}, {{ $u3.AuthorLink }}, and {{ $u4.AuthorLink }} for their ongoing contributions. 62 {{ end }} 63 Many have also been busy writing and fixing the documentation in [hugoDocs](https://github.com/gohugoio/hugoDocs), 64 which has received **{{ len .Docs }} contributions by {{ len $docsContribsPerAuthor }} contributors**. 65 {{- if gt (len $docsContribsPerAuthor) 3 -}} 66 {{- $u1 := index $docsContribsPerAuthor 0 -}} 67 {{- $u2 := index $docsContribsPerAuthor 1 -}} 68 {{- $u3 := index $docsContribsPerAuthor 2 -}} 69 {{- $u4 := index $docsContribsPerAuthor 3 }} A special thanks to {{ $u1.AuthorLink }}, {{ $u2.AuthorLink }}, {{ $u3.AuthorLink }}, and {{ $u4.AuthorLink }} for their work on the documentation site. 70 {{ end }} 71 72 Hugo now has: 73 74 {{ with .Repo -}} 75 * {{ .Stars }}+ [stars](https://github.com/gohugoio/hugo/stargazers) 76 * {{ len .Contributors }}+ [contributors](https://github.com/gohugoio/hugo/graphs/contributors) 77 {{- end -}} 78 {{ with .ThemeCount }} 79 * {{ . }}+ [themes](http://themes.gohugo.io/) 80 {{ end }} 81 {{ with .Notes }} 82 ## Notes 83 {{ template "change-section" . }} 84 {{- end -}} 85 ## Enhancements 86 {{ template "change-headers" .Enhancements -}} 87 ## Fixes 88 {{ template "change-headers" .Fixes -}} 89 90 {{ define "change-headers" }} 91 {{ $tmplChanges := index . "templateChanges" -}} 92 {{- $outChanges := index . "outChanges" -}} 93 {{- $coreChanges := index . "coreChanges" -}} 94 {{- $otherChanges := index . "otherChanges" -}} 95 {{- with $tmplChanges -}} 96 ### Templates 97 {{ template "change-section" . }} 98 {{- end -}} 99 {{- with $outChanges -}} 100 ### Output 101 {{ template "change-section" . }} 102 {{- end -}} 103 {{- with $coreChanges -}} 104 ### Core 105 {{ template "change-section" . }} 106 {{- end -}} 107 {{- with $otherChanges -}} 108 ### Other 109 {{ template "change-section" . }} 110 {{- end -}} 111 {{ end }} 112 113 114 {{ define "change-section" }} 115 {{ range . }} 116 {{- if .GitHubCommit -}} 117 * {{ .Subject }} {{ . | commitURL }} {{ . | authorURL }} {{ range .Issues }}{{ . | issue }}{{ end }} 118 {{ else -}} 119 * {{ .Subject }} {{ range .Issues }}{{ . | issue }}{{ end }} 120 {{ end -}} 121 {{- end }} 122 {{ end }} 123 ` 124 ) 125 126 var templateFuncs = template.FuncMap{ 127 "isPatch": func(c changeLog) bool { 128 return !strings.HasSuffix(c.Version, "0") 129 }, 130 "issue": func(id int) string { 131 return fmt.Sprintf(issueLinkTemplate, id, id) 132 }, 133 "commitURL": func(info gitInfo) string { 134 if info.GitHubCommit.HTMLURL == "" { 135 return "" 136 } 137 return fmt.Sprintf(linkTemplate, info.Hash, info.GitHubCommit.HTMLURL) 138 }, 139 "authorURL": func(info gitInfo) string { 140 if info.GitHubCommit.Author.Login == "" { 141 return "" 142 } 143 return fmt.Sprintf(linkTemplate, "@"+info.GitHubCommit.Author.Login, info.GitHubCommit.Author.HTMLURL) 144 }, 145 } 146 147 func writeReleaseNotes(version string, infosMain, infosDocs gitInfos, to io.Writer) error { 148 client := newGitHubAPI("hugo") 149 changes := gitInfosToChangeLog(infosMain, infosDocs) 150 changes.Version = version 151 repo, err := client.fetchRepo() 152 if err == nil { 153 changes.Repo = &repo 154 } 155 themeCount, err := fetchThemeCount() 156 if err == nil { 157 changes.ThemeCount = themeCount 158 } 159 160 mtempl := releaseNotesMarkdownTemplate 161 162 if !strings.HasSuffix(version, "0") { 163 mtempl = releaseNotesMarkdownTemplatePatchRelease 164 } 165 166 tmpl, err := template.New("").Funcs(templateFuncs).Parse(mtempl) 167 if err != nil { 168 return err 169 } 170 171 err = tmpl.Execute(to, changes) 172 if err != nil { 173 return err 174 } 175 176 return nil 177 } 178 179 func fetchThemeCount() (int, error) { 180 resp, err := http.Get("https://raw.githubusercontent.com/gohugoio/hugoThemesSiteBuilder/main/themes.txt") 181 if err != nil { 182 return 0, err 183 } 184 defer resp.Body.Close() 185 186 b, _ := ioutil.ReadAll(resp.Body) 187 return bytes.Count(b, []byte("\n")) - bytes.Count(b, []byte("#")), nil 188 } 189 190 func writeReleaseNotesToTmpFile(version string, infosMain, infosDocs gitInfos) (string, error) { 191 f, err := ioutil.TempFile("", "hugorelease") 192 if err != nil { 193 return "", err 194 } 195 196 defer f.Close() 197 198 if err := writeReleaseNotes(version, infosMain, infosDocs, f); err != nil { 199 return "", err 200 } 201 202 return f.Name(), nil 203 } 204 205 func getReleaseNotesDocsTempDirAndName(version string, final bool) (string, string) { 206 if final { 207 return hugoFilepath("temp"), fmt.Sprintf("%s-relnotes-ready.md", version) 208 } 209 return hugoFilepath("temp"), fmt.Sprintf("%s-relnotes.md", version) 210 } 211 212 func getReleaseNotesDocsTempFilename(version string, final bool) string { 213 return filepath.Join(getReleaseNotesDocsTempDirAndName(version, final)) 214 } 215 216 func (r *ReleaseHandler) releaseNotesState(version string) (releaseNotesState, error) { 217 docsTempPath, name := getReleaseNotesDocsTempDirAndName(version, false) 218 _, err := os.Stat(filepath.Join(docsTempPath, name)) 219 220 if err == nil { 221 return releaseNotesCreated, nil 222 } 223 224 docsTempPath, name = getReleaseNotesDocsTempDirAndName(version, true) 225 _, err = os.Stat(filepath.Join(docsTempPath, name)) 226 227 if err == nil { 228 return releaseNotesReady, nil 229 } 230 231 if !os.IsNotExist(err) { 232 return releaseNotesNone, err 233 } 234 235 return releaseNotesNone, nil 236 } 237 238 func (r *ReleaseHandler) writeReleaseNotesToTemp(version string, isPatch bool, infosMain, infosDocs gitInfos) (string, error) { 239 docsTempPath, name := getReleaseNotesDocsTempDirAndName(version, isPatch) 240 241 var w io.WriteCloser 242 243 if !r.try { 244 os.Mkdir(docsTempPath, os.ModePerm) 245 246 f, err := os.Create(filepath.Join(docsTempPath, name)) 247 if err != nil { 248 return "", err 249 } 250 251 name = f.Name() 252 253 defer f.Close() 254 255 w = f 256 257 } else { 258 w = os.Stdout 259 } 260 261 if err := writeReleaseNotes(version, infosMain, infosDocs, w); err != nil { 262 return "", err 263 } 264 265 return name, nil 266 } 267 268 func (r *ReleaseHandler) writeReleaseNotesToDocs(title, description, sourceFilename string) (string, error) { 269 targetFilename := "index.md" 270 bundleDir := strings.TrimSuffix(filepath.Base(sourceFilename), "-ready.md") 271 contentDir := hugoFilepath("docs/content/en/news/" + bundleDir) 272 targetFullFilename := filepath.Join(contentDir, targetFilename) 273 274 if r.try { 275 fmt.Printf("Write release notes to /docs: Bundle %q Dir: %q\n", bundleDir, contentDir) 276 return targetFullFilename, nil 277 } 278 279 if err := os.MkdirAll(contentDir, os.ModePerm); err != nil { 280 return "", nil 281 } 282 283 b, err := ioutil.ReadFile(sourceFilename) 284 if err != nil { 285 return "", err 286 } 287 288 f, err := os.Create(targetFullFilename) 289 if err != nil { 290 return "", err 291 } 292 defer f.Close() 293 294 fmTail := "" 295 if !strings.HasSuffix(title, ".0") { 296 // Bug fix release 297 fmTail = ` 298 images: 299 - images/blog/hugo-bug-poster.png 300 ` 301 } 302 303 if _, err := f.WriteString(fmt.Sprintf(` 304 --- 305 date: %s 306 title: %q 307 description: %q 308 categories: ["Releases"]%s 309 --- 310 311 `, time.Now().Format("2006-01-02"), title, description, fmTail)); err != nil { 312 return "", err 313 } 314 315 if _, err := f.Write(b); err != nil { 316 return "", err 317 } 318 319 return targetFullFilename, nil 320 }