github.com/olliephillips/hugo@v0.42.2/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 releaseNotesMarkdownTemplate = ` 35 {{- $patchRelease := isPatch . -}} 36 {{- $contribsPerAuthor := .All.ContribCountPerAuthor -}} 37 {{- $docsContribsPerAuthor := .Docs.ContribCountPerAuthor -}} 38 {{- if $patchRelease }} 39 {{ if eq (len .All) 1 }} 40 This is a bug-fix release with one important fix. 41 {{ else }} 42 This is a bug-fix release with a couple of important fixes. 43 {{ end }} 44 {{ else }} 45 This release represents **{{ len .All }} contributions by {{ len $contribsPerAuthor }} contributors** to the main Hugo code base. 46 {{ end -}} 47 48 {{- if gt (len $contribsPerAuthor) 3 -}} 49 {{- $u1 := index $contribsPerAuthor 0 -}} 50 {{- $u2 := index $contribsPerAuthor 1 -}} 51 {{- $u3 := index $contribsPerAuthor 2 -}} 52 {{- $u4 := index $contribsPerAuthor 3 -}} 53 {{- $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. 54 And a big thanks to [@digitalcraftsman](https://github.com/digitalcraftsman) for his relentless work on keeping the themes site in pristine condition and to [@kaushalmodi](https://github.com/kaushalmodi) for his great work on the documentation site. 55 {{ end }} 56 {{- if not $patchRelease }} 57 Many have also been busy writing and fixing the documentation in [hugoDocs](https://github.com/gohugoio/hugoDocs), 58 which has received **{{ len .Docs }} contributions by {{ len $docsContribsPerAuthor }} contributors**. 59 {{- if gt (len $docsContribsPerAuthor) 3 -}} 60 {{- $u1 := index $docsContribsPerAuthor 0 -}} 61 {{- $u2 := index $docsContribsPerAuthor 1 -}} 62 {{- $u3 := index $docsContribsPerAuthor 2 -}} 63 {{- $u4 := index $docsContribsPerAuthor 3 }} A special thanks to {{ $u1.AuthorLink }}, {{ $u2.AuthorLink }}, {{ $u3.AuthorLink }}, and {{ $u4.AuthorLink }} for their work on the documentation site. 64 {{ end }} 65 {{ end }} 66 Hugo now has: 67 68 {{ with .Repo -}} 69 * {{ .Stars }}+ [stars](https://github.com/gohugoio/hugo/stargazers) 70 * {{ len .Contributors }}+ [contributors](https://github.com/gohugoio/hugo/graphs/contributors) 71 {{- end -}} 72 {{ with .ThemeCount }} 73 * {{ . }}+ [themes](http://themes.gohugo.io/) 74 {{ end }} 75 {{ with .Notes }} 76 ## Notes 77 {{ template "change-section" . }} 78 {{- end -}} 79 ## Enhancements 80 {{ template "change-headers" .Enhancements -}} 81 ## Fixes 82 {{ template "change-headers" .Fixes -}} 83 84 {{ define "change-headers" }} 85 {{ $tmplChanges := index . "templateChanges" -}} 86 {{- $outChanges := index . "outChanges" -}} 87 {{- $coreChanges := index . "coreChanges" -}} 88 {{- $otherChanges := index . "otherChanges" -}} 89 {{- with $tmplChanges -}} 90 ### Templates 91 {{ template "change-section" . }} 92 {{- end -}} 93 {{- with $outChanges -}} 94 ### Output 95 {{ template "change-section" . }} 96 {{- end -}} 97 {{- with $coreChanges -}} 98 ### Core 99 {{ template "change-section" . }} 100 {{- end -}} 101 {{- with $otherChanges -}} 102 ### Other 103 {{ template "change-section" . }} 104 {{- end -}} 105 {{ end }} 106 107 108 {{ define "change-section" }} 109 {{ range . }} 110 {{- if .GitHubCommit -}} 111 * {{ .Subject }} {{ . | commitURL }} {{ . | authorURL }} {{ range .Issues }}{{ . | issue }}{{ end }} 112 {{ else -}} 113 * {{ .Subject }} {{ range .Issues }}{{ . | issue }}{{ end }} 114 {{ end -}} 115 {{- end }} 116 {{ end }} 117 ` 118 ) 119 120 var templateFuncs = template.FuncMap{ 121 "isPatch": func(c changeLog) bool { 122 return strings.Count(c.Version, ".") > 1 123 }, 124 "issue": func(id int) string { 125 return fmt.Sprintf(issueLinkTemplate, id, id) 126 }, 127 "commitURL": func(info gitInfo) string { 128 if info.GitHubCommit.HtmlURL == "" { 129 return "" 130 } 131 return fmt.Sprintf(linkTemplate, info.Hash, info.GitHubCommit.HtmlURL) 132 }, 133 "authorURL": func(info gitInfo) string { 134 if info.GitHubCommit.Author.Login == "" { 135 return "" 136 } 137 return fmt.Sprintf(linkTemplate, "@"+info.GitHubCommit.Author.Login, info.GitHubCommit.Author.HtmlURL) 138 }, 139 } 140 141 func writeReleaseNotes(version string, infosMain, infosDocs gitInfos, to io.Writer) error { 142 client := newGitHubAPI("hugo") 143 changes := gitInfosToChangeLog(infosMain, infosDocs) 144 changes.Version = version 145 repo, err := client.fetchRepo() 146 if err == nil { 147 changes.Repo = &repo 148 } 149 themeCount, err := fetchThemeCount() 150 if err == nil { 151 changes.ThemeCount = themeCount 152 } 153 154 tmpl, err := template.New("").Funcs(templateFuncs).Parse(releaseNotesMarkdownTemplate) 155 if err != nil { 156 return err 157 } 158 159 err = tmpl.Execute(to, changes) 160 if err != nil { 161 return err 162 } 163 164 return nil 165 166 } 167 168 func fetchThemeCount() (int, error) { 169 resp, err := http.Get("https://raw.githubusercontent.com/gohugoio/hugoThemes/master/.gitmodules") 170 if err != nil { 171 return 0, err 172 } 173 defer resp.Body.Close() 174 175 b, _ := ioutil.ReadAll(resp.Body) 176 return bytes.Count(b, []byte("submodule")), nil 177 } 178 179 func writeReleaseNotesToTmpFile(version string, infosMain, infosDocs gitInfos) (string, error) { 180 f, err := ioutil.TempFile("", "hugorelease") 181 if err != nil { 182 return "", err 183 } 184 185 defer f.Close() 186 187 if err := writeReleaseNotes(version, infosMain, infosDocs, f); err != nil { 188 return "", err 189 } 190 191 return f.Name(), nil 192 } 193 194 func getReleaseNotesDocsTempDirAndName(version string, final bool) (string, string) { 195 if final { 196 return hugoFilepath("temp"), fmt.Sprintf("%s-relnotes-ready.md", version) 197 } 198 return hugoFilepath("temp"), fmt.Sprintf("%s-relnotes.md", version) 199 } 200 201 func getReleaseNotesDocsTempFilename(version string, final bool) string { 202 return filepath.Join(getReleaseNotesDocsTempDirAndName(version, final)) 203 } 204 205 func (r *ReleaseHandler) releaseNotesState(version string) (releaseNotesState, error) { 206 docsTempPath, name := getReleaseNotesDocsTempDirAndName(version, false) 207 _, err := os.Stat(filepath.Join(docsTempPath, name)) 208 209 if err == nil { 210 return releaseNotesCreated, nil 211 } 212 213 docsTempPath, name = getReleaseNotesDocsTempDirAndName(version, true) 214 _, err = os.Stat(filepath.Join(docsTempPath, name)) 215 216 if err == nil { 217 return releaseNotesReady, nil 218 } 219 220 if !os.IsNotExist(err) { 221 return releaseNotesNone, err 222 } 223 224 return releaseNotesNone, nil 225 226 } 227 228 func (r *ReleaseHandler) writeReleaseNotesToTemp(version string, infosMain, infosDocs gitInfos) (string, error) { 229 230 docsTempPath, name := getReleaseNotesDocsTempDirAndName(version, false) 231 232 var ( 233 w io.WriteCloser 234 ) 235 236 if !r.try { 237 os.Mkdir(docsTempPath, os.ModePerm) 238 239 f, err := os.Create(filepath.Join(docsTempPath, name)) 240 if err != nil { 241 return "", err 242 } 243 244 name = f.Name() 245 246 defer f.Close() 247 248 w = f 249 250 } else { 251 w = os.Stdout 252 } 253 254 if err := writeReleaseNotes(version, infosMain, infosDocs, w); err != nil { 255 return "", err 256 } 257 258 return name, nil 259 260 } 261 262 func (r *ReleaseHandler) writeReleaseNotesToDocs(title, sourceFilename string) (string, error) { 263 targetFilename := "index.md" 264 bundleDir := strings.TrimSuffix(filepath.Base(sourceFilename), "-ready.md") 265 contentDir := hugoFilepath("docs/content/en/news/" + bundleDir) 266 targetFullFilename := filepath.Join(contentDir, targetFilename) 267 268 if r.try { 269 fmt.Printf("Write release notes to /docs: Bundle %q Dir: %q\n", bundleDir, contentDir) 270 return targetFullFilename, nil 271 } 272 273 if err := os.MkdirAll(contentDir, os.ModePerm); err != nil { 274 return "", nil 275 } 276 277 b, err := ioutil.ReadFile(sourceFilename) 278 if err != nil { 279 return "", err 280 } 281 282 f, err := os.Create(targetFullFilename) 283 if err != nil { 284 return "", err 285 } 286 defer f.Close() 287 288 fmTail := "" 289 if strings.Count(title, ".") > 1 { 290 // Bug fix release 291 fmTail = ` 292 images: 293 - images/blog/hugo-bug-poster.png 294 ` 295 } 296 297 if _, err := f.WriteString(fmt.Sprintf(` 298 --- 299 date: %s 300 title: %q 301 description: %q 302 categories: ["Releases"]%s 303 --- 304 305 `, time.Now().Format("2006-01-02"), title, title, fmTail)); err != nil { 306 return "", err 307 } 308 309 if _, err := f.Write(b); err != nil { 310 return "", err 311 } 312 313 return targetFullFilename, nil 314 315 }