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 (
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
2017-06-13 12:42:45 -04:00
"github.com/gohugoio/hugo/helpers"
2017-04-13 10:59:05 -04:00
)
const commitPrefix = "releaser:"
type ReleaseHandler struct {
2017-07-05 04:23:07 -04:00
cliVersion string
2017-05-22 09:04:40 -04:00
2017-06-21 15:17:50 -04:00
// If set, we do the releases in 3 steps:
2017-05-22 09:04:40 -04:00
// 1: Create and write a draft release notes
// 2: Prepare files for new version.
// 3: Release
2017-04-13 10:59:05 -04:00
step int
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
}
func ( r ReleaseHandler ) shouldRelease ( ) bool {
return r . step < 1 || r . shouldContinue ( )
}
func ( r ReleaseHandler ) shouldContinue ( ) bool {
2017-05-22 09:04:40 -04:00
return r . step == 3
2017-04-13 10:59:05 -04:00
}
2017-05-22 09:04:40 -04:00
func ( r ReleaseHandler ) shouldPrepareReleasenotes ( ) bool {
2017-04-13 10:59:05 -04:00
return r . step < 1 || r . step == 1
}
2017-05-22 09:04:40 -04:00
func ( r ReleaseHandler ) shouldPrepareVersions ( ) bool {
return r . step < 1 || r . step == 2
}
2017-07-05 04:23:07 -04:00
func ( r ReleaseHandler ) calculateVersions ( ) ( helpers . HugoVersion , helpers . HugoVersion ) {
newVersion := helpers . MustParseHugoVersion ( r . cliVersion )
finalVersion := newVersion
finalVersion . PatchLevel = 0
2017-04-13 10:59:05 -04:00
newVersion . Suffix = ""
2017-07-05 04:23:07 -04:00
if newVersion . PatchLevel == 0 {
finalVersion = finalVersion . Next ( )
2017-04-13 10:59:05 -04:00
}
finalVersion . Suffix = "-DEV"
return newVersion , finalVersion
}
2017-07-05 04:23:07 -04:00
func New ( version string , step int , skipPublish , try bool ) * ReleaseHandler {
rh := & ReleaseHandler { cliVersion : version , step : step , skipPublish : skipPublish , try : try }
2017-07-05 03:43:47 -04:00
if try {
rh . git = func ( args ... string ) ( string , error ) {
fmt . Println ( "git" , args )
return "" , nil
}
} else {
rh . git = git
}
return rh
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 {
// There may have been patch releases inbetween, so set the tag explicitly.
changeLogFromTag = "v" + newVersion . Prev ( ) . String ( )
exists , _ := tagExists ( changeLogFromTag )
if ! exists {
// fall back to one that exists.
changeLogFromTag = ""
}
}
2017-04-13 10:59:05 -04:00
var gitCommits gitInfos
2017-05-22 09:04:40 -04:00
if r . shouldPrepareReleasenotes ( ) || r . shouldRelease ( ) {
2017-07-05 03:43:47 -04:00
gitCommits , err = getGitInfos ( changeLogFromTag , ! r . try )
2017-04-13 10:59:05 -04:00
if err != nil {
return err
}
}
2017-05-22 09:04:40 -04:00
if r . shouldPrepareReleasenotes ( ) {
2017-07-05 03:43:47 -04:00
releaseNotesFile , err := r . writeReleaseNotesToTemp ( version , gitCommits )
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-07-05 03:43:47 -04:00
if _ , err := r . git ( "commit" , "-m" , fmt . Sprintf ( "%s Add release notes draft for %s\n\n[ci skip]" , commitPrefix , newVersion ) ) ; err != nil {
2017-04-13 10:59:05 -04:00
return err
}
}
2017-05-22 09:04:40 -04:00
if r . shouldPrepareVersions ( ) {
2017-06-24 03:18:57 -04:00
if newVersion . PatchLevel == 0 {
// Make sure the docs submodule is up to date.
// TODO(bep) improve this. Maybe it was not such a good idea to do
// this in the sobmodule directly.
2017-07-05 03:43:47 -04:00
if _ , err := r . git ( "submodule" , "update" , "--init" ) ; err != nil {
2017-06-24 03:18:57 -04:00
return err
}
//git submodule update
2017-07-05 03:43:47 -04:00
if _ , err := r . git ( "submodule" , "update" , "--remote" , "--merge" ) ; err != nil {
2017-06-24 03:18:57 -04:00
return err
}
2017-06-24 03:49:57 -04:00
// TODO(bep) the above may not have changed anything.
2017-07-05 03:43:47 -04:00
if _ , err := r . git ( "commit" , "-a" , "-m" , fmt . Sprintf ( "%s Update /docs [ci skip]" , commitPrefix ) ) ; err != nil {
2017-06-24 03:49:57 -04:00
return err
}
2017-05-22 09:04:40 -04:00
}
2017-04-13 10:59:05 -04:00
2017-07-05 03:43:47 -04:00
if err := r . bumpVersions ( newVersion ) ; err != nil {
2017-05-22 09:04:40 -04:00
return err
}
2017-06-15 14:36:40 -04:00
for _ , repo := range [ ] string { "docs" , "." } {
2017-07-05 03:43:47 -04:00
if _ , err := r . git ( "-C" , repo , "commit" , "-a" , "-m" , fmt . Sprintf ( "%s Bump versions for release of %s\n\n[ci skip]" , commitPrefix , newVersion ) ) ; err != nil {
2017-06-15 14:36:40 -04:00
return err
}
}
2017-05-20 04:11:23 -04:00
}
2017-05-22 09:04:40 -04:00
if ! r . shouldRelease ( ) {
fmt . Println ( "Skip release ... Use --state=3 to continue." )
return nil
2017-05-20 04:11:23 -04:00
}
2017-06-21 15:17:50 -04:00
releaseNotesFile := getReleaseNotesDocsTempFilename ( version )
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 03:43:47 -04:00
if _ , err := r . git ( "-C" , "docs" , "add" , docFile ) ; err != nil {
2017-04-13 10:59:05 -04:00
return err
}
2017-07-05 03:43:47 -04:00
if _ , err := r . git ( "-C" , "docs" , "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-06-15 14:36:40 -04:00
for i , repo := range [ ] string { "docs" , "." } {
if i == 1 {
2017-07-05 03:43:47 -04:00
if _ , err := r . git ( "add" , "docs" ) ; err != nil {
2017-06-15 14:36:40 -04:00
return err
}
2017-07-05 03:43:47 -04:00
if _ , err := r . git ( "commit" , "-m" , fmt . Sprintf ( "%s Update /docs to %s [ci skip]" , commitPrefix , newVersion ) ) ; err != nil {
2017-06-15 14:36:40 -04:00
return err
}
}
2017-07-05 03:43:47 -04:00
if _ , err := r . git ( "-C" , repo , "tag" , "-a" , tag , "-m" , fmt . Sprintf ( "%s %s [ci deploy]" , commitPrefix , newVersion ) ) ; err != nil {
2017-06-15 14:36:40 -04:00
return err
}
2017-04-13 10:59:05 -04:00
2017-06-16 03:21:44 -04:00
repoURL := "git@github.com:gohugoio/hugo.git"
if i == 0 {
repoURL = "git@github.com:gohugoio/hugoDocs.git"
}
2017-07-05 03:43:47 -04:00
if _ , err := r . git ( "-C" , repo , "push" , repoURL , "origin/master" , tag ) ; err != nil {
2017-06-15 14:36:40 -04:00
return err
}
2017-05-03 03:29:59 -04:00
}
2017-06-16 04:11:02 -04:00
// We make changes to the submodule, which is in detached state. Reconsider this
// to get changes pushed to both.
// TODO(bep) git fetch git@github.com:gohugoio/hugoDocs.git -- master
// git branch -f master 8c9359b
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-06-15 14:36:40 -04:00
for _ , repo := range [ ] string { "docs" , "." } {
2017-07-05 03:43:47 -04:00
if _ , err := r . git ( "-C" , repo , "commit" , "-a" , "-m" , fmt . Sprintf ( "%s Prepare repository for %s\n\n[ci skip]" , commitPrefix , finalVersion ) ) ; err != nil {
2017-06-15 14:36:40 -04:00
return err
}
2017-04-13 10:59:05 -04:00
}
return nil
}
func ( r * ReleaseHandler ) release ( releaseNotesFile string ) error {
2017-07-05 03:43:47 -04:00
if r . try {
fmt . Println ( "Skip goreleaser..." )
return nil
}
2017-04-13 10:59:05 -04:00
cmd := exec . Command ( "goreleaser" , "--release-notes" , releaseNotesFile , "--skip-publish=" + fmt . Sprint ( r . skipPublish ) )
cmd . Stdout = os . Stdout
cmd . Stderr = os . Stderr
err := cmd . Run ( )
if err != nil {
return fmt . Errorf ( "goreleaser failed: %s" , err )
}
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
fromDev := ""
toDev := ""
if ver . Suffix != "" {
toDev = "-DEV"
} else {
fromDev = "-DEV"
}
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 ) ,
fmt . Sprintf ( ` Suffix:(\s { 4,})"%s", ` , fromDev ) , fmt . Sprintf ( ` Suffix:$ { 1}"%s", ` , toDev ) ) ; err != nil {
return err
}
snapcraftGrade := "stable"
if ver . Suffix != "" {
snapcraftGrade = "devel"
}
2017-07-05 03:43:47 -04:00
if err := r . replaceInFile ( "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 )
}