2017-04-13 10:59:05 -04:00
// Copyright 2017-present The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
2017-08-02 08:25:05 -04:00
// Package releaser implements a set of utilities and a wrapper around Goreleaser
2017-04-13 10:59:05 -04:00
// to help automate the Hugo release process.
package releaser
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"text/template"
"time"
)
const (
2017-06-13 12:47:17 -04:00
issueLinkTemplate = "[#%d](https://github.com/gohugoio/hugo/issues/%d)"
2017-04-13 10:59:05 -04:00
linkTemplate = "[%s](%s)"
releaseNotesMarkdownTemplate = `
{ { - $ patchRelease := isPatch . - } }
{ { - $ contribsPerAuthor := . All . ContribCountPerAuthor - } }
2017-08-06 04:42:07 -04:00
{ { - $ docsContribsPerAuthor := . Docs . ContribCountPerAuthor - } }
2017-04-13 10:59:05 -04:00
{ { - if $ patchRelease } }
{ { if eq ( len . All ) 1 } }
This is a bug - fix release with one important fix .
{ { else } }
2017-06-21 15:17:50 -04:00
This is a bug - fix release with a couple of important fixes .
2017-04-13 10:59:05 -04:00
{ { end } }
{ { else } }
This release represents * * { { len . All } } contributions by { { len $ contribsPerAuthor } } contributors * * to the main Hugo code base .
{ { end - } }
{ { - if gt ( len $ contribsPerAuthor ) 3 - } }
{ { - $ u1 := index $ contribsPerAuthor 0 - } }
{ { - $ u2 := index $ contribsPerAuthor 1 - } }
{ { - $ u3 := index $ contribsPerAuthor 2 - } }
{ { - $ u4 := index $ contribsPerAuthor 3 - } }
{ { - $ 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 .
2018-03-16 15:56:28 -04:00
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.
2017-04-13 10:59:05 -04:00
{ { end } }
2017-08-06 04:42:07 -04:00
{ { - if not $ patchRelease } }
Many have also been busy writing and fixing the documentation in [ hugoDocs ] ( https : //github.com/gohugoio/hugoDocs),
which has received * * { { len . Docs } } contributions by { { len $ docsContribsPerAuthor } } contributors * * .
{ { - if gt ( len $ docsContribsPerAuthor ) 3 - } }
{ { - $ u1 := index $ docsContribsPerAuthor 0 - } }
{ { - $ u2 := index $ docsContribsPerAuthor 1 - } }
{ { - $ u3 := index $ docsContribsPerAuthor 2 - } }
{ { - $ u4 := index $ docsContribsPerAuthor 3 } } A special thanks to { { $ u1 . AuthorLink } } , { { $ u2 . AuthorLink } } , { { $ u3 . AuthorLink } } , and { { $ u4 . AuthorLink } } for their work on the documentation site .
{ { end } }
{ { end } }
2017-04-13 10:59:05 -04:00
Hugo now has :
{ { with . Repo - } }
2017-06-13 12:47:17 -04:00
* { { . Stars } } + [ stars ] ( https : //github.com/gohugoio/hugo/stargazers)
* { { len . Contributors } } + [ contributors ] ( https : //github.com/gohugoio/hugo/graphs/contributors)
2017-04-13 10:59:05 -04:00
{ { - end - } }
{ { with . ThemeCount } }
2017-07-06 11:31:04 -04:00
* { { . } } + [ themes ] ( http : //themes.gohugo.io/)
2017-08-06 04:42:07 -04:00
{ { end } }
2017-05-20 03:58:08 -04:00
{ { with . Notes } }
# # Notes
{ { template "change-section" . } }
{ { - end - } }
2017-04-13 10:59:05 -04:00
# # Enhancements
{ { template "change-headers" . Enhancements - } }
# # Fixes
{ { template "change-headers" . Fixes - } }
{ { define "change-headers" } }
{ { $ tmplChanges := index . "templateChanges" - } }
{ { - $ outChanges := index . "outChanges" - } }
{ { - $ coreChanges := index . "coreChanges" - } }
{ { - $ otherChanges := index . "otherChanges" - } }
{ { - with $ tmplChanges - } }
# # # Templates
{ { template "change-section" . } }
{ { - end - } }
{ { - with $ outChanges - } }
# # # Output
2017-05-20 03:58:08 -04:00
{ { template "change-section" . } }
2017-04-13 10:59:05 -04:00
{ { - end - } }
{ { - with $ coreChanges - } }
# # # Core
{ { template "change-section" . } }
{ { - end - } }
{ { - with $ otherChanges - } }
# # # Other
{ { template "change-section" . } }
{ { - end - } }
{ { end } }
{ { define "change-section" } }
{ { range . } }
{ { - if . GitHubCommit - } }
2017-06-25 10:29:58 -04:00
* { { . Subject } } { { . | commitURL } } { { . | authorURL } } { { range . Issues } } { { . | issue } } { { end } }
2017-04-13 10:59:05 -04:00
{ { else - } }
2017-06-25 10:29:58 -04:00
* { { . Subject } } { { range . Issues } } { { . | issue } } { { end } }
2017-04-13 10:59:05 -04:00
{ { end - } }
{ { - end } }
{ { end } }
`
)
var templateFuncs = template . FuncMap {
"isPatch" : func ( c changeLog ) bool {
return strings . Count ( c . Version , "." ) > 1
} ,
"issue" : func ( id int ) string {
return fmt . Sprintf ( issueLinkTemplate , id , id )
} ,
"commitURL" : func ( info gitInfo ) string {
if info . GitHubCommit . HtmlURL == "" {
return ""
}
return fmt . Sprintf ( linkTemplate , info . Hash , info . GitHubCommit . HtmlURL )
} ,
"authorURL" : func ( info gitInfo ) string {
if info . GitHubCommit . Author . Login == "" {
return ""
}
return fmt . Sprintf ( linkTemplate , "@" + info . GitHubCommit . Author . Login , info . GitHubCommit . Author . HtmlURL )
} ,
}
2017-08-06 04:42:07 -04:00
func writeReleaseNotes ( version string , infosMain , infosDocs gitInfos , to io . Writer ) error {
2017-09-10 11:14:02 -04:00
client := newGitHubAPI ( "hugo" )
2017-08-06 04:42:07 -04:00
changes := gitInfosToChangeLog ( infosMain , infosDocs )
2017-04-13 10:59:05 -04:00
changes . Version = version
2017-09-10 11:14:02 -04:00
repo , err := client . fetchRepo ( )
2017-04-13 10:59:05 -04:00
if err == nil {
changes . Repo = & repo
}
themeCount , err := fetchThemeCount ( )
if err == nil {
changes . ThemeCount = themeCount
}
tmpl , err := template . New ( "" ) . Funcs ( templateFuncs ) . Parse ( releaseNotesMarkdownTemplate )
if err != nil {
return err
}
err = tmpl . Execute ( to , changes )
if err != nil {
return err
}
return nil
}
func fetchThemeCount ( ) ( int , error ) {
2017-06-15 14:36:40 -04:00
resp , err := http . Get ( "https://raw.githubusercontent.com/gohugoio/hugoThemes/master/.gitmodules" )
2017-04-13 10:59:05 -04:00
if err != nil {
return 0 , err
}
defer resp . Body . Close ( )
b , _ := ioutil . ReadAll ( resp . Body )
return bytes . Count ( b , [ ] byte ( "submodule" ) ) , nil
}
2017-08-06 04:42:07 -04:00
func writeReleaseNotesToTmpFile ( version string , infosMain , infosDocs gitInfos ) ( string , error ) {
2017-04-13 10:59:05 -04:00
f , err := ioutil . TempFile ( "" , "hugorelease" )
if err != nil {
return "" , err
}
defer f . Close ( )
2017-08-06 04:42:07 -04:00
if err := writeReleaseNotes ( version , infosMain , infosDocs , f ) ; err != nil {
2017-04-13 10:59:05 -04:00
return "" , err
}
return f . Name ( ) , nil
}
2017-09-10 11:14:02 -04:00
func getReleaseNotesDocsTempDirAndName ( version string , final bool ) ( string , string ) {
if final {
return hugoFilepath ( "temp" ) , fmt . Sprintf ( "%s-relnotes-ready.md" , version )
}
2017-06-15 14:36:40 -04:00
return hugoFilepath ( "temp" ) , fmt . Sprintf ( "%s-relnotes.md" , version )
2017-04-13 10:59:05 -04:00
}
2017-09-10 11:14:02 -04:00
func getReleaseNotesDocsTempFilename ( version string , final bool ) string {
return filepath . Join ( getReleaseNotesDocsTempDirAndName ( version , final ) )
}
func ( r * ReleaseHandler ) releaseNotesState ( version string ) ( releaseNotesState , error ) {
docsTempPath , name := getReleaseNotesDocsTempDirAndName ( version , false )
_ , err := os . Stat ( filepath . Join ( docsTempPath , name ) )
if err == nil {
return releaseNotesCreated , nil
}
docsTempPath , name = getReleaseNotesDocsTempDirAndName ( version , true )
_ , err = os . Stat ( filepath . Join ( docsTempPath , name ) )
if err == nil {
return releaseNotesReady , nil
}
if ! os . IsNotExist ( err ) {
return releaseNotesNone , err
}
return releaseNotesNone , nil
2017-04-13 10:59:05 -04:00
}
2017-08-06 04:42:07 -04:00
func ( r * ReleaseHandler ) writeReleaseNotesToTemp ( version string , infosMain , infosDocs gitInfos ) ( string , error ) {
2017-07-05 03:43:47 -04:00
2017-09-10 11:14:02 -04:00
docsTempPath , name := getReleaseNotesDocsTempDirAndName ( version , false )
2017-04-13 10:59:05 -04:00
2017-07-05 03:43:47 -04:00
var (
w io . WriteCloser
)
2017-04-13 10:59:05 -04:00
2017-07-05 03:43:47 -04:00
if ! r . try {
os . Mkdir ( docsTempPath , os . ModePerm )
2017-04-13 10:59:05 -04:00
2017-07-05 03:43:47 -04:00
f , err := os . Create ( filepath . Join ( docsTempPath , name ) )
if err != nil {
return "" , err
}
name = f . Name ( )
defer f . Close ( )
w = f
} else {
w = os . Stdout
}
2017-08-06 04:42:07 -04:00
if err := writeReleaseNotes ( version , infosMain , infosDocs , w ) ; err != nil {
2017-04-13 10:59:05 -04:00
return "" , err
}
2017-07-05 03:43:47 -04:00
return name , nil
2017-04-13 10:59:05 -04:00
}
2017-07-05 03:43:47 -04:00
func ( r * ReleaseHandler ) writeReleaseNotesToDocs ( title , sourceFilename string ) ( string , error ) {
2018-02-21 04:22:08 -05:00
targetFilename := "index.md"
bundleDir := strings . TrimSuffix ( filepath . Base ( sourceFilename ) , "-ready.md" )
contentDir := hugoFilepath ( "docs/content/news/" + bundleDir )
2017-04-13 10:59:05 -04:00
targetFullFilename := filepath . Join ( contentDir , targetFilename )
2017-07-05 03:43:47 -04:00
if r . try {
2018-02-21 04:22:08 -05:00
fmt . Printf ( "Write release notes to /docs: Bundle %q Dir: %q\n" , bundleDir , contentDir )
2017-07-05 03:43:47 -04:00
return targetFullFilename , nil
}
2018-02-21 04:22:08 -05:00
if err := os . MkdirAll ( contentDir , os . ModePerm ) ; err != nil {
return "" , nil
}
2017-04-13 10:59:05 -04:00
b , err := ioutil . ReadFile ( sourceFilename )
if err != nil {
return "" , err
}
f , err := os . Create ( targetFullFilename )
if err != nil {
return "" , err
}
defer f . Close ( )
2017-10-19 06:34:03 -04:00
fmTail := ""
if strings . Count ( title , "." ) > 1 {
// Bug fix release
fmTail = `
images :
- images / blog / hugo - bug - poster . png
`
}
2017-04-13 10:59:05 -04:00
if _ , err := f . WriteString ( fmt . Sprintf ( `
-- -
date : % s
2017-08-07 02:54:40 -04:00
title : % q
description : % q
2017-10-19 06:34:03 -04:00
categories : [ "Releases" ] % s
2017-04-13 10:59:05 -04:00
-- -
2018-02-27 04:33:35 -05:00
` , time . Now ( ) . Format ( "2006-01-02" ) , title , title , fmTail ) ) ; err != nil {
2017-04-13 10:59:05 -04:00
return "" , err
}
if _ , err := f . Write ( b ) ; err != nil {
return "" , err
}
return targetFullFilename , nil
}