hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

releaser.go (7394B)

    1 // Copyright 2017-present The Hugo Authors. All rights reserved.
    2 //
    3 // Licensed under the Apache License, Version 2.0 (the "License");
    4 // you may not use this file except in compliance with the License.
    5 // You may obtain a copy of the License at
    6 // http://www.apache.org/licenses/LICENSE-2.0
    7 //
    8 // Unless required by applicable law or agreed to in writing, software
    9 // distributed under the License is distributed on an "AS IS" BASIS,
   10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   11 // See the License for the specific language governing permissions and
   12 // limitations under the License.
   13 
   14 // Package releaser implements a set of utilities and a wrapper around Goreleaser
   15 // to help automate the Hugo release process.
   16 package releaser
   17 
   18 import (
   19 	"fmt"
   20 	"io/ioutil"
   21 	"log"
   22 	"os"
   23 	"path/filepath"
   24 	"regexp"
   25 	"strings"
   26 
   27 	"github.com/gohugoio/hugo/common/hexec"
   28 
   29 	"errors"
   30 
   31 	"github.com/gohugoio/hugo/common/hugo"
   32 )
   33 
   34 const commitPrefix = "releaser:"
   35 
   36 // ReleaseHandler provides functionality to release a new version of Hugo.
   37 // Test this locally without doing an actual release:
   38 // go run -tags release main.go release --skip-publish --try -r 0.90.0
   39 // Or a variation of the above -- the skip-publish flag makes sure that any changes are performed to the local Git only.
   40 type ReleaseHandler struct {
   41 	cliVersion string
   42 
   43 	skipPublish bool
   44 
   45 	// Just simulate, no actual changes.
   46 	try bool
   47 
   48 	git func(args ...string) (string, error)
   49 }
   50 
   51 func (r ReleaseHandler) calculateVersions() (hugo.Version, hugo.Version) {
   52 	newVersion := hugo.MustParseVersion(r.cliVersion)
   53 	finalVersion := newVersion.Next()
   54 	finalVersion.PatchLevel = 0
   55 
   56 	if newVersion.Suffix != "-test" {
   57 		newVersion.Suffix = ""
   58 	}
   59 
   60 	finalVersion.Suffix = "-DEV"
   61 
   62 	return newVersion, finalVersion
   63 }
   64 
   65 // New initialises a ReleaseHandler.
   66 func New(version string, skipPublish, try bool) *ReleaseHandler {
   67 	// When triggered from CI release branch
   68 	version = strings.TrimPrefix(version, "release-")
   69 	version = strings.TrimPrefix(version, "v")
   70 	rh := &ReleaseHandler{cliVersion: version, skipPublish: skipPublish, try: try}
   71 
   72 	if try {
   73 		rh.git = func(args ...string) (string, error) {
   74 			fmt.Println("git", strings.Join(args, " "))
   75 			return "", nil
   76 		}
   77 	} else {
   78 		rh.git = git
   79 	}
   80 
   81 	return rh
   82 }
   83 
   84 // Run creates a new release.
   85 func (r *ReleaseHandler) Run() error {
   86 	if os.Getenv("GITHUB_TOKEN") == "" {
   87 		return errors.New("GITHUB_TOKEN not set, create one here with the repo scope selected: https://github.com/settings/tokens/new")
   88 	}
   89 
   90 	fmt.Printf("Start release from %q\n", wd())
   91 
   92 	newVersion, finalVersion := r.calculateVersions()
   93 
   94 	version := newVersion.String()
   95 	tag := "v" + version
   96 	isPatch := newVersion.PatchLevel > 0
   97 	mainVersion := newVersion
   98 	mainVersion.PatchLevel = 0
   99 
  100 	// Exit early if tag already exists
  101 	exists, err := tagExists(tag)
  102 	if err != nil {
  103 		return err
  104 	}
  105 
  106 	if exists {
  107 		return fmt.Errorf("tag %q already exists", tag)
  108 	}
  109 
  110 	var changeLogFromTag string
  111 
  112 	if newVersion.PatchLevel == 0 {
  113 		// There may have been patch releases between, so set the tag explicitly.
  114 		changeLogFromTag = "v" + newVersion.Prev().String()
  115 		exists, _ := tagExists(changeLogFromTag)
  116 		if !exists {
  117 			// fall back to one that exists.
  118 			changeLogFromTag = ""
  119 		}
  120 	}
  121 
  122 	var (
  123 		gitCommits     gitInfos
  124 		gitCommitsDocs gitInfos
  125 	)
  126 
  127 	defer r.gitPush() // TODO(bep)
  128 
  129 	gitCommits, err = getGitInfos(changeLogFromTag, "hugo", "", !r.try)
  130 	if err != nil {
  131 		return err
  132 	}
  133 
  134 	// TODO(bep) explicit tag?
  135 	gitCommitsDocs, err = getGitInfos("", "hugoDocs", "../hugoDocs", !r.try)
  136 	if err != nil {
  137 		return err
  138 	}
  139 
  140 	releaseNotesFile, err := r.writeReleaseNotesToTemp(version, isPatch, gitCommits, gitCommitsDocs)
  141 	if err != nil {
  142 		return err
  143 	}
  144 
  145 	if _, err := r.git("add", releaseNotesFile); err != nil {
  146 		return err
  147 	}
  148 
  149 	commitMsg := fmt.Sprintf("%s Add release notes for %s", commitPrefix, newVersion)
  150 	commitMsg += "\n[ci skip]"
  151 
  152 	if _, err := r.git("commit", "-m", commitMsg); err != nil {
  153 		return err
  154 	}
  155 
  156 	if err := r.bumpVersions(newVersion); err != nil {
  157 		return err
  158 	}
  159 
  160 	if _, err := r.git("commit", "-a", "-m", fmt.Sprintf("%s Bump versions for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil {
  161 		return err
  162 	}
  163 
  164 	if _, err := r.git("tag", "-a", tag, "-m", fmt.Sprintf("%s %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil {
  165 		return err
  166 	}
  167 
  168 	if !r.skipPublish {
  169 		if _, err := r.git("push", "origin", tag); err != nil {
  170 			return err
  171 		}
  172 	}
  173 
  174 	if err := r.release(releaseNotesFile); err != nil {
  175 		return err
  176 	}
  177 
  178 	if err := r.bumpVersions(finalVersion); err != nil {
  179 		return err
  180 	}
  181 
  182 	if !r.try {
  183 		// No longer needed.
  184 		if err := os.Remove(releaseNotesFile); err != nil {
  185 			return err
  186 		}
  187 	}
  188 
  189 	if _, err := r.git("commit", "-a", "-m", fmt.Sprintf("%s Prepare repository for %s\n\n[ci skip]", commitPrefix, finalVersion)); err != nil {
  190 		return err
  191 	}
  192 
  193 	return nil
  194 }
  195 
  196 func (r *ReleaseHandler) gitPush() {
  197 	if r.skipPublish {
  198 		return
  199 	}
  200 	if _, err := r.git("push", "origin", "HEAD"); err != nil {
  201 		log.Fatal("push failed:", err)
  202 	}
  203 }
  204 
  205 func (r *ReleaseHandler) release(releaseNotesFile string) error {
  206 	if r.try {
  207 		fmt.Println("Skip goreleaser...")
  208 		return nil
  209 	}
  210 
  211 	args := []string{"--parallelism", "2", "--timeout", "120m", "--rm-dist", "--release-notes", releaseNotesFile}
  212 	if r.skipPublish {
  213 		args = append(args, "--skip-publish")
  214 	}
  215 
  216 	cmd, _ := hexec.SafeCommand("goreleaser", args...)
  217 	cmd.Stdout = os.Stdout
  218 	cmd.Stderr = os.Stderr
  219 	err := cmd.Run()
  220 	if err != nil {
  221 		return fmt.Errorf("goreleaser failed: %w", err)
  222 	}
  223 	return nil
  224 }
  225 
  226 func (r *ReleaseHandler) bumpVersions(ver hugo.Version) error {
  227 	toDev := ""
  228 
  229 	if ver.Suffix != "" {
  230 		toDev = ver.Suffix
  231 	}
  232 
  233 	if err := r.replaceInFile("common/hugo/version_current.go",
  234 		`Minor:(\s*)(\d*),`, fmt.Sprintf(`Minor:${1}%d,`, ver.Minor),
  235 		`PatchLevel:(\s*)(\d*),`, fmt.Sprintf(`PatchLevel:${1}%d,`, ver.PatchLevel),
  236 		`Suffix:(\s*)".*",`, fmt.Sprintf(`Suffix:${1}"%s",`, toDev)); err != nil {
  237 		return err
  238 	}
  239 
  240 	snapcraftGrade := "stable"
  241 	if ver.Suffix != "" {
  242 		snapcraftGrade = "devel"
  243 	}
  244 	if err := r.replaceInFile("snap/snapcraft.yaml",
  245 		`version: "(.*)"`, fmt.Sprintf(`version: "%s"`, ver),
  246 		`grade: (.*) #`, fmt.Sprintf(`grade: %s #`, snapcraftGrade)); err != nil {
  247 		return err
  248 	}
  249 
  250 	var minVersion string
  251 	if ver.Suffix != "" {
  252 		// People use the DEV version in daily use, and we cannot create new themes
  253 		// with the next version before it is released.
  254 		minVersion = ver.Prev().String()
  255 	} else {
  256 		minVersion = ver.String()
  257 	}
  258 
  259 	if err := r.replaceInFile("commands/new.go",
  260 		`min_version = "(.*)"`, fmt.Sprintf(`min_version = "%s"`, minVersion)); err != nil {
  261 		return err
  262 	}
  263 
  264 	return nil
  265 }
  266 
  267 func (r *ReleaseHandler) replaceInFile(filename string, oldNew ...string) error {
  268 	filename = filepath.FromSlash(filename)
  269 	fi, err := os.Stat(filename)
  270 	if err != nil {
  271 		return err
  272 	}
  273 
  274 	if r.try {
  275 		fmt.Printf("Replace in %q: %q\n", filename, oldNew)
  276 		return nil
  277 	}
  278 
  279 	b, err := ioutil.ReadFile(filename)
  280 	if err != nil {
  281 		return err
  282 	}
  283 	newContent := string(b)
  284 
  285 	for i := 0; i < len(oldNew); i += 2 {
  286 		re := regexp.MustCompile(oldNew[i])
  287 		newContent = re.ReplaceAllString(newContent, oldNew[i+1])
  288 	}
  289 
  290 	return ioutil.WriteFile(filename, []byte(newContent), fi.Mode())
  291 }
  292 
  293 func isCI() bool {
  294 	return os.Getenv("CI") != ""
  295 }
  296 
  297 func wd() string {
  298 	p, err := os.Getwd()
  299 	if err != nil {
  300 		log.Fatal(err)
  301 	}
  302 	return p
  303 
  304 }