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.
// Package releaser implements a set of utilities and a wrapper around Goreleaser
// to help automate the Hugo release process.
package releaser
import (
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
2017-07-05 19:32:55 -04:00
"strings"
2017-04-13 10:59:05 -04:00
2018-10-03 08:58:09 -04:00
"github.com/pkg/errors"
2017-06-13 12:42:45 -04:00
"github.com/gohugoio/hugo/helpers"
2017-04-13 10:59:05 -04:00
)
const commitPrefix = "releaser:"
2017-09-10 11:14:02 -04:00
type releaseNotesState int
const (
releaseNotesNone = iota
releaseNotesCreated
releaseNotesReady
)
2017-08-02 08:25:05 -04:00
// ReleaseHandler provides functionality to release a new version of Hugo.
2017-04-13 10:59:05 -04:00
type ReleaseHandler struct {
2017-07-05 04:23:07 -04:00
cliVersion string
2017-05-22 09:04:40 -04:00
2017-04-13 10:59:05 -04:00
skipPublish bool
2017-07-05 03:43:47 -04:00
// Just simulate, no actual changes.
try bool
git func ( args ... string ) ( string , error )
2017-04-13 10:59:05 -04:00
}
2017-07-05 04:23:07 -04:00
func ( r ReleaseHandler ) calculateVersions ( ) ( helpers . HugoVersion , helpers . HugoVersion ) {
newVersion := helpers . MustParseHugoVersion ( r . cliVersion )
2017-10-19 01:12:23 -04:00
finalVersion := newVersion . Next ( )
2017-07-05 04:23:07 -04:00
finalVersion . PatchLevel = 0
2017-04-13 10:59:05 -04:00
2017-09-10 11:14:02 -04:00
if newVersion . Suffix != "-test" {
newVersion . Suffix = ""
}
2017-04-13 10:59:05 -04:00
finalVersion . Suffix = "-DEV"
return newVersion , finalVersion
}
2017-08-02 08:25:05 -04:00
// New initialises a ReleaseHandler.
2017-09-10 11:14:02 -04:00
func New ( version string , skipPublish , try bool ) * ReleaseHandler {
// When triggered from CI release branch
version = strings . TrimPrefix ( version , "release-" )
version = strings . TrimPrefix ( version , "v" )
rh := & ReleaseHandler { cliVersion : version , skipPublish : skipPublish , try : try }
2017-07-05 03:43:47 -04:00
if try {
rh . git = func ( args ... string ) ( string , error ) {
2017-07-05 19:32:55 -04:00
fmt . Println ( "git" , strings . Join ( args , " " ) )
2017-07-05 03:43:47 -04:00
return "" , nil
}
} else {
rh . git = git
}
return rh
2017-04-13 10:59:05 -04:00
}
2017-08-02 08:25:05 -04:00
// Run creates a new release.
2017-04-13 10:59:05 -04:00
func ( r * ReleaseHandler ) Run ( ) error {
if os . Getenv ( "GITHUB_TOKEN" ) == "" {
return errors . New ( "GITHUB_TOKEN not set, create one here with the repo scope selected: https://github.com/settings/tokens/new" )
}
2017-07-05 04:23:07 -04:00
newVersion , finalVersion := r . calculateVersions ( )
2017-04-13 10:59:05 -04:00
version := newVersion . String ( )
tag := "v" + version
// Exit early if tag already exists
2017-05-20 03:58:08 -04:00
exists , err := tagExists ( tag )
2017-04-13 10:59:05 -04:00
if err != nil {
return err
}
2017-05-20 03:58:08 -04:00
if exists {
2017-04-13 10:59:05 -04:00
return fmt . Errorf ( "Tag %q already exists" , tag )
}
2017-05-20 03:58:08 -04:00
var changeLogFromTag string
if newVersion . PatchLevel == 0 {
2017-08-07 14:19:24 -04:00
// There may have been patch releases between, so set the tag explicitly.
2017-05-20 03:58:08 -04:00
changeLogFromTag = "v" + newVersion . Prev ( ) . String ( )
exists , _ := tagExists ( changeLogFromTag )
if ! exists {
// fall back to one that exists.
changeLogFromTag = ""
}
}
2017-08-06 04:42:07 -04:00
var (
gitCommits gitInfos
gitCommitsDocs gitInfos
2017-09-10 11:14:02 -04:00
relNotesState releaseNotesState
2017-08-06 04:42:07 -04:00
)
2017-04-13 10:59:05 -04:00
2017-09-10 11:14:02 -04:00
relNotesState , err = r . releaseNotesState ( version )
if err != nil {
return err
}
prepareRelaseNotes := relNotesState == releaseNotesNone
shouldRelease := relNotesState == releaseNotesReady
defer r . gitPush ( ) // TODO(bep)
if prepareRelaseNotes || shouldRelease {
gitCommits , err = getGitInfos ( changeLogFromTag , "hugo" , "" , ! r . try )
2017-08-06 04:42:07 -04:00
if err != nil {
return err
}
2017-09-10 11:14:02 -04:00
// TODO(bep) explicit tag?
gitCommitsDocs , err = getGitInfos ( "" , "hugoDocs" , "../hugoDocs" , ! r . try )
2017-04-13 10:59:05 -04:00
if err != nil {
return err
}
}
2017-09-10 11:14:02 -04:00
if relNotesState == releaseNotesCreated {
fmt . Println ( "Release notes created, but not ready. Reneame to *-ready.md to continue ..." )
return nil
}
if prepareRelaseNotes {
2017-08-06 04:42:07 -04:00
releaseNotesFile , err := r . writeReleaseNotesToTemp ( version , gitCommits , gitCommitsDocs )
2017-04-13 10:59:05 -04:00
if err != nil {
return err
}
2017-07-05 03:43:47 -04:00
if _ , err := r . git ( "add" , releaseNotesFile ) ; err != nil {
2017-04-13 10:59:05 -04:00
return err
}
2017-09-10 11:14:02 -04:00
if _ , err := r . git ( "commit" , "-m" , fmt . Sprintf ( "%s Add release notes draft for %s\n\nRename to *-ready.md to continue. [ci skip]" , commitPrefix , newVersion ) ) ; err != nil {
2017-04-13 10:59:05 -04:00
return err
}
}
2017-09-10 11:14:02 -04:00
if ! shouldRelease {
fmt . Printf ( "Skip release ... " )
return nil
}
2017-04-13 10:59:05 -04:00
2017-09-10 11:14:02 -04:00
// For docs, for now we assume that:
// The /docs subtree is up to date and ready to go.
// The hugoDocs/dev and hugoDocs/master must be merged manually after release.
// TODO(bep) improve this when we see how it works.
2017-06-15 14:36:40 -04:00
2017-09-10 11:14:02 -04:00
if err := r . bumpVersions ( newVersion ) ; err != nil {
return err
2017-05-20 04:11:23 -04:00
}
2017-09-10 11:14:02 -04:00
if _ , err := r . git ( "commit" , "-a" , "-m" , fmt . Sprintf ( "%s Bump versions for release of %s\n\n[ci skip]" , commitPrefix , newVersion ) ) ; err != nil {
return err
2017-05-20 04:11:23 -04:00
}
2017-09-10 11:14:02 -04:00
releaseNotesFile := getReleaseNotesDocsTempFilename ( version , true )
2017-04-13 10:59:05 -04:00
// Write the release notes to the docs site as well.
2017-07-05 03:43:47 -04:00
docFile , err := r . writeReleaseNotesToDocs ( version , releaseNotesFile )
2017-04-13 10:59:05 -04:00
if err != nil {
return err
}
2017-07-05 05:20:48 -04:00
if _ , err := r . git ( "add" , docFile ) ; err != nil {
2017-04-13 10:59:05 -04:00
return err
}
2017-07-05 05:20:48 -04:00
if _ , err := r . git ( "commit" , "-m" , fmt . Sprintf ( "%s Add release notes to /docs for release of %s\n\n[ci skip]" , commitPrefix , newVersion ) ) ; err != nil {
2017-04-13 10:59:05 -04:00
return err
}
2017-09-10 11:14:02 -04:00
if _ , err := r . git ( "tag" , "-a" , tag , "-m" , fmt . Sprintf ( "%s %s [ci skip]" , commitPrefix , newVersion ) ) ; err != nil {
2017-07-05 05:20:48 -04:00
return err
2017-05-03 03:29:59 -04:00
}
2017-09-10 11:14:02 -04:00
if ! r . skipPublish {
if _ , err := r . git ( "push" , "origin" , tag ) ; err != nil {
return err
}
2017-07-05 05:20:48 -04:00
}
2017-06-16 04:11:02 -04:00
2017-04-13 10:59:05 -04:00
if err := r . release ( releaseNotesFile ) ; err != nil {
return err
}
2017-07-05 03:43:47 -04:00
if err := r . bumpVersions ( finalVersion ) ; err != nil {
2017-04-13 10:59:05 -04:00
return err
}
2017-07-05 03:43:47 -04:00
if ! r . try {
// No longer needed.
if err := os . Remove ( releaseNotesFile ) ; err != nil {
return err
}
2017-04-13 10:59:05 -04:00
}
2017-07-05 05:20:48 -04:00
if _ , err := r . git ( "commit" , "-a" , "-m" , fmt . Sprintf ( "%s Prepare repository for %s\n\n[ci skip]" , commitPrefix , finalVersion ) ) ; err != nil {
return err
2017-04-13 10:59:05 -04:00
}
return nil
}
2017-09-10 11:14:02 -04:00
func ( r * ReleaseHandler ) gitPush ( ) {
if r . skipPublish {
return
}
if _ , err := r . git ( "push" , "origin" , "HEAD" ) ; err != nil {
log . Fatal ( "push failed:" , err )
}
}
2017-04-13 10:59:05 -04:00
func ( r * ReleaseHandler ) release ( releaseNotesFile string ) error {
2017-07-05 03:43:47 -04:00
if r . try {
fmt . Println ( "Skip goreleaser..." )
return nil
}
2018-08-17 04:58:18 -04:00
args := [ ] string { "--rm-dist" , "--release-notes" , releaseNotesFile }
if r . skipPublish {
args = append ( args , "--skip-publish" )
}
cmd := exec . Command ( "goreleaser" , args ... )
2017-04-13 10:59:05 -04:00
cmd . Stdout = os . Stdout
cmd . Stderr = os . Stderr
err := cmd . Run ( )
if err != nil {
2018-10-03 08:58:09 -04:00
return errors . Wrap ( err , "goreleaser failed" )
2017-04-13 10:59:05 -04:00
}
return nil
}
2017-07-05 03:43:47 -04:00
func ( r * ReleaseHandler ) bumpVersions ( ver helpers . HugoVersion ) error {
2017-04-13 10:59:05 -04:00
toDev := ""
if ver . Suffix != "" {
2017-09-10 11:14:02 -04:00
toDev = ver . Suffix
2017-04-13 10:59:05 -04:00
}
2017-07-05 03:43:47 -04:00
if err := r . replaceInFile ( "helpers/hugo.go" ,
2017-04-13 10:59:05 -04:00
` Number:(\s { 4,})(.*), ` , fmt . Sprintf ( ` Number:$ { 1}%.2f, ` , ver . Number ) ,
` PatchLevel:(\s*)(.*), ` , fmt . Sprintf ( ` PatchLevel:$ { 1}%d, ` , ver . PatchLevel ) ,
2017-09-10 11:14:02 -04:00
` Suffix:(\s { 4,})".*", ` , fmt . Sprintf ( ` Suffix:$ { 1}"%s", ` , toDev ) ) ; err != nil {
2017-04-13 10:59:05 -04:00
return err
}
snapcraftGrade := "stable"
if ver . Suffix != "" {
snapcraftGrade = "devel"
}
2018-09-25 09:21:09 -04:00
if err := r . replaceInFile ( "snap/snapcraft.yaml" ,
2017-04-13 10:59:05 -04:00
` version: "(.*)" ` , fmt . Sprintf ( ` version: "%s" ` , ver ) ,
` grade: (.*) # ` , fmt . Sprintf ( ` grade: %s # ` , snapcraftGrade ) ) ; err != nil {
return err
}
var minVersion string
if ver . Suffix != "" {
// People use the DEV version in daily use, and we cannot create new themes
// with the next version before it is released.
minVersion = ver . Prev ( ) . String ( )
} else {
minVersion = ver . String ( )
}
2017-07-05 03:43:47 -04:00
if err := r . replaceInFile ( "commands/new.go" ,
2017-04-13 10:59:05 -04:00
` min_version = "(.*)" ` , fmt . Sprintf ( ` min_version = "%s" ` , minVersion ) ) ; err != nil {
return err
}
// docs/config.toml
2017-07-05 03:43:47 -04:00
if err := r . replaceInFile ( "docs/config.toml" ,
2017-04-13 10:59:05 -04:00
` release = "(.*)" ` , fmt . Sprintf ( ` release = "%s" ` , ver ) ) ; err != nil {
return err
}
return nil
}
2017-07-05 03:43:47 -04:00
func ( r * ReleaseHandler ) replaceInFile ( filename string , oldNew ... string ) error {
2017-04-13 10:59:05 -04:00
fullFilename := hugoFilepath ( filename )
fi , err := os . Stat ( fullFilename )
if err != nil {
return err
}
2017-07-05 03:43:47 -04:00
if r . try {
fmt . Printf ( "Replace in %q: %q\n" , filename , oldNew )
return nil
}
2017-04-13 10:59:05 -04:00
b , err := ioutil . ReadFile ( fullFilename )
if err != nil {
return err
}
newContent := string ( b )
for i := 0 ; i < len ( oldNew ) ; i += 2 {
re := regexp . MustCompile ( oldNew [ i ] )
newContent = re . ReplaceAllString ( newContent , oldNew [ i + 1 ] )
}
return ioutil . WriteFile ( fullFilename , [ ] byte ( newContent ) , fi . Mode ( ) )
}
func hugoFilepath ( filename string ) string {
pwd , err := os . Getwd ( )
if err != nil {
log . Fatal ( err )
}
return filepath . Join ( pwd , filename )
}
2017-09-10 11:14:02 -04:00
func isCI ( ) bool {
return os . Getenv ( "CI" ) != ""
}