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  }