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-07-05 19:32:55 -04:00
|
|
|
"strings"
|
2017-04-13 10:59:05 -04:00
|
|
|
|
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-07-05 05:20:48 -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 {
|
2017-07-05 05:20:48 -04:00
|
|
|
return r.step < 1 || r.step == 2 || r.step > 3
|
2017-05-22 09:04:40 -04:00
|
|
|
}
|
|
|
|
|
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) {
|
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
|
|
|
}
|
|
|
|
|
|
|
|
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-07-05 05:20:48 -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-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
|
|
|
|
2017-07-05 05:20:48 -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-06-15 14:36:40 -04:00
|
|
|
}
|
2017-05-20 04:11:23 -04:00
|
|
|
}
|
|
|
|
|
2017-05-22 09:04:40 -04:00
|
|
|
if !r.shouldRelease() {
|
2017-07-05 05:20:48 -04:00
|
|
|
fmt.Printf("Skip release ... Use --state=%d for next or --state=4 to finish\n", r.step+1)
|
2017-05-22 09:04:40 -04:00
|
|
|
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 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-07-05 05:20:48 -04:00
|
|
|
if _, err := r.git("tag", "-a", tag, "-m", fmt.Sprintf("%s %s [ci deploy]", commitPrefix, newVersion)); err != nil {
|
|
|
|
return err
|
2017-05-03 03:29:59 -04:00
|
|
|
}
|
|
|
|
|
2017-07-07 03:28:35 -04:00
|
|
|
if _, err := r.git("push", "origin", tag); err != nil {
|
2017-07-05 05:20:48 -04:00
|
|
|
return err
|
|
|
|
}
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
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-07-10 02:26:26 -04:00
|
|
|
cmd := exec.Command("goreleaser", "--rm-dist", "--release-notes", releaseNotesFile, "--skip-publish="+fmt.Sprint(r.skipPublish))
|
2017-04-13 10:59:05 -04:00
|
|
|
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)
|
|
|
|
}
|