shithub: hugo

ref: 30e14cc31678ddc204b082ab362f86b6b8063881
dir: /releaser/releaser.go/

View raw version
// 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"

	"github.com/gohugoio/hugo/helpers"
)

const commitPrefix = "releaser:"

type ReleaseHandler struct {
	patch int

	// If set, we do the releases in 3 steps:
	// 1: Create and write a draft release notes
	// 2: Prepare files for new version.
	// 3: Release
	step        int
	skipPublish bool
}

func (r ReleaseHandler) shouldRelease() bool {
	return r.step < 1 || r.shouldContinue()
}

func (r ReleaseHandler) shouldContinue() bool {
	return r.step == 3
}

func (r ReleaseHandler) shouldPrepareReleasenotes() bool {
	return r.step < 1 || r.step == 1
}

func (r ReleaseHandler) shouldPrepareVersions() bool {
	return r.step < 1 || r.step == 2
}

func (r ReleaseHandler) calculateVersions(current helpers.HugoVersion) (helpers.HugoVersion, helpers.HugoVersion) {
	var (
		newVersion   = current
		finalVersion = current
	)

	newVersion.Suffix = ""

	if r.shouldContinue() {
		// The version in the current code base is in the state we want for
		// the release.
		if r.patch == 0 {
			finalVersion = newVersion.Next()
		}
	} else if r.patch > 0 {
		newVersion = helpers.CurrentHugoVersion.NextPatchLevel(r.patch)
	} else {
		finalVersion = newVersion.Next()
	}

	finalVersion.Suffix = "-DEV"

	return newVersion, finalVersion
}

func New(patch, step int, skipPublish bool) *ReleaseHandler {
	return &ReleaseHandler{patch: patch, step: step, skipPublish: skipPublish}
}

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")
	}

	newVersion, finalVersion := r.calculateVersions(helpers.CurrentHugoVersion)

	version := newVersion.String()
	tag := "v" + version

	// Exit early if tag already exists
	exists, err := tagExists(tag)
	if err != nil {
		return err
	}

	if exists {
		return fmt.Errorf("Tag %q already exists", tag)
	}

	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 = ""
		}
	}

	var gitCommits gitInfos

	if r.shouldPrepareReleasenotes() || r.shouldRelease() {
		gitCommits, err = getGitInfos(changeLogFromTag, true)
		if err != nil {
			return err
		}
	}

	if r.shouldPrepareReleasenotes() {
		releaseNotesFile, err := writeReleaseNotesToTemp(version, gitCommits)
		if err != nil {
			return err
		}

		if _, err := git("add", releaseNotesFile); err != nil {
			return err
		}
		if _, err := git("commit", "-m", fmt.Sprintf("%s Add release notes draft for %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil {
			return err
		}
	}

	if r.shouldPrepareVersions() {
		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.
			if _, err := git("submodule", "update", "--init"); err != nil {
				return err
			}
			//git submodule update
			if _, err := git("submodule", "update", "--remote", "--merge"); err != nil {
				return err
			}

			// TODO(bep) the above may not have changed anything.
			if _, err := git("commit", "-a", "-m", fmt.Sprintf("%s Update /docs [ci skip]", commitPrefix)); err != nil {
				return err
			}
		}

		if err := bumpVersions(newVersion); err != nil {
			return err
		}

		for _, repo := range []string{"docs", "."} {
			if _, err := git("-C", repo, "commit", "-a", "-m", fmt.Sprintf("%s Bump versions for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil {
				return err
			}
		}
	}

	if !r.shouldRelease() {
		fmt.Println("Skip release ... Use --state=3 to continue.")
		return nil
	}

	releaseNotesFile := getReleaseNotesDocsTempFilename(version)

	// Write the release notes to the docs site as well.
	docFile, err := writeReleaseNotesToDocs(version, releaseNotesFile)
	if err != nil {
		return err
	}

	if _, err := git("-C", "docs", "add", docFile); err != nil {
		return err
	}
	if _, err := 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 {
		return err
	}

	for i, repo := range []string{"docs", "."} {
		if i == 1 {
			if _, err := git("add", "docs"); err != nil {
				return err
			}
			if _, err := git("commit", "-m", fmt.Sprintf("%s Update /docs to %s [ci skip]", commitPrefix, newVersion)); err != nil {
				return err
			}
		}
		if _, err := git("-C", repo, "tag", "-a", tag, "-m", fmt.Sprintf("%s %s [ci deploy]", commitPrefix, newVersion)); err != nil {
			return err
		}

		repoURL := "[email protected]:gohugoio/hugo.git"
		if i == 0 {
			repoURL = "[email protected]:gohugoio/hugoDocs.git"
		}
		if _, err := git("-C", repo, "push", repoURL, "origin/master", tag); err != nil {
			return err
		}
	}

	// We make changes to the submodule, which is in detached state. Reconsider this
	// to get changes pushed to both.
	// TODO(bep) git fetch [email protected]:gohugoio/hugoDocs.git -- master
	// git branch -f master 8c9359b

	if err := r.release(releaseNotesFile); err != nil {
		return err
	}

	if err := bumpVersions(finalVersion); err != nil {
		return err
	}

	// No longer needed.
	if err := os.Remove(releaseNotesFile); err != nil {
		return err
	}

	for _, repo := range []string{"docs", "."} {
		if _, err := git("-C", repo, "commit", "-a", "-m", fmt.Sprintf("%s Prepare repository for %s\n\n[ci skip]", commitPrefix, finalVersion)); err != nil {
			return err
		}
	}

	return nil
}

func (r *ReleaseHandler) release(releaseNotesFile string) error {
	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
}

func bumpVersions(ver helpers.HugoVersion) error {
	fromDev := ""
	toDev := ""

	if ver.Suffix != "" {
		toDev = "-DEV"
	} else {
		fromDev = "-DEV"
	}

	if err := replaceInFile("helpers/hugo.go",
		`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"
	}
	if err := replaceInFile("snapcraft.yaml",
		`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()
	}

	if err := replaceInFile("commands/new.go",
		`min_version = "(.*)"`, fmt.Sprintf(`min_version = "%s"`, minVersion)); err != nil {
		return err
	}

	// docs/config.toml
	if err := replaceInFile("docs/config.toml",
		`release = "(.*)"`, fmt.Sprintf(`release = "%s"`, ver)); err != nil {
		return err
	}

	return nil
}

func replaceInFile(filename string, oldNew ...string) error {
	fullFilename := hugoFilepath(filename)
	fi, err := os.Stat(fullFilename)
	if err != nil {
		return err
	}

	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)
}