mirror of
https://github.com/gohugoio/hugo.git
synced 2025-08-16 20:53:59 +02:00
Automate the Hugo release process
This commit adds a work flow aroung GoReleaser to get the Hugo release process automated and more uniform: * It can be run fully automated or in two steps to allow for manual edits of the relase notes. * It supports both patch and full releases. * It fetches author, issue, repo info. etc. for the release notes from GitHub. * The file names produced are mainly the same as before, but we no use tar.gz as archive for all Unix versions. * There isn't a fully automated CI setup in place yet, but the release tag is marked in the commit message with "[ci deploy]" Fixes #3358
This commit is contained in:
267
releaser/releaser.go
Normal file
267
releaser/releaser.go
Normal file
@@ -0,0 +1,267 @@
|
||||
// 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"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/hugo/helpers"
|
||||
)
|
||||
|
||||
const commitPrefix = "releaser:"
|
||||
|
||||
type ReleaseHandler struct {
|
||||
patch int
|
||||
step int
|
||||
skipPublish bool
|
||||
}
|
||||
|
||||
func (r ReleaseHandler) shouldRelease() bool {
|
||||
return r.step < 1 || r.shouldContinue()
|
||||
}
|
||||
|
||||
func (r ReleaseHandler) shouldContinue() bool {
|
||||
return r.step == 2
|
||||
}
|
||||
|
||||
func (r ReleaseHandler) shouldPrepare() bool {
|
||||
return r.step < 1 || r.step == 1
|
||||
}
|
||||
|
||||
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
|
||||
out, err := git("tag", "-l", tag)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.Contains(out, tag) {
|
||||
return fmt.Errorf("Tag %q already exists", tag)
|
||||
}
|
||||
|
||||
var gitCommits gitInfos
|
||||
|
||||
if r.shouldPrepare() || r.shouldRelease() {
|
||||
gitCommits, err = getGitInfos(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if r.shouldPrepare() {
|
||||
if err := bumpVersions(newVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := git("commit", "-a", "-m", fmt.Sprintf("%s Bump versions for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
releaseNotesFile, err := writeReleaseNotesToDocsTemp(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 relase notes draft for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !r.shouldRelease() {
|
||||
fmt.Println("Skip release ... Use --state=2 to continue.")
|
||||
return nil
|
||||
}
|
||||
|
||||
releaseNotesFile := getRelaseNotesDocsTempFilename(version)
|
||||
|
||||
// Write the release notes to the docs site as well.
|
||||
docFile, err := writeReleaseNotesToDocs(version, releaseNotesFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := git("add", docFile); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := git("commit", "-m", fmt.Sprintf("%s Add relase notes to /docs for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := git("tag", "-a", tag, "-m", fmt.Sprintf("%s %s [ci deploy]", commitPrefix, newVersion)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if _, err := git("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())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func hugoFilepath(filename string) string {
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return filepath.Join(pwd, filename)
|
||||
}
|
Reference in New Issue
Block a user