github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/releaser/releaser.go (about)

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