github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/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  type releaseNotesState int
    36  
    37  const (
    38  	releaseNotesNone = iota
    39  	releaseNotesCreated
    40  	releaseNotesReady
    41  )
    42  
    43  // ReleaseHandler provides functionality to release a new version of Hugo.
    44  type ReleaseHandler struct {
    45  	cliVersion string
    46  
    47  	skipPublish bool
    48  
    49  	// Just simulate, no actual changes.
    50  	try bool
    51  
    52  	git func(args ...string) (string, error)
    53  }
    54  
    55  func (r ReleaseHandler) calculateVersions() (hugo.Version, hugo.Version) {
    56  	newVersion := hugo.MustParseVersion(r.cliVersion)
    57  	finalVersion := newVersion.Next()
    58  	finalVersion.PatchLevel = 0
    59  
    60  	if newVersion.Suffix != "-test" {
    61  		newVersion.Suffix = ""
    62  	}
    63  
    64  	finalVersion.Suffix = "-DEV"
    65  
    66  	return newVersion, finalVersion
    67  }
    68  
    69  // New initialises a ReleaseHandler.
    70  func New(version string, skipPublish, try bool) *ReleaseHandler {
    71  	// When triggered from CI release branch
    72  	version = strings.TrimPrefix(version, "release-")
    73  	version = strings.TrimPrefix(version, "v")
    74  	rh := &ReleaseHandler{cliVersion: version, skipPublish: skipPublish, try: try}
    75  
    76  	if try {
    77  		rh.git = func(args ...string) (string, error) {
    78  			fmt.Println("git", strings.Join(args, " "))
    79  			return "", nil
    80  		}
    81  	} else {
    82  		rh.git = git
    83  	}
    84  
    85  	return rh
    86  }
    87  
    88  // Run creates a new release.
    89  func (r *ReleaseHandler) Run() error {
    90  	if os.Getenv("GITHUB_TOKEN") == "" {
    91  		return errors.New("GITHUB_TOKEN not set, create one here with the repo scope selected: https://github.com/settings/tokens/new")
    92  	}
    93  
    94  	newVersion, finalVersion := r.calculateVersions()
    95  
    96  	version := newVersion.String()
    97  	tag := "v" + version
    98  	isPatch := newVersion.PatchLevel > 0
    99  	mainVersion := newVersion
   100  	mainVersion.PatchLevel = 0
   101  
   102  	// Exit early if tag already exists
   103  	exists, err := tagExists(tag)
   104  	if err != nil {
   105  		return err
   106  	}
   107  
   108  	if exists {
   109  		return fmt.Errorf("tag %q already exists", tag)
   110  	}
   111  
   112  	var changeLogFromTag string
   113  
   114  	if newVersion.PatchLevel == 0 {
   115  		// There may have been patch releases between, so set the tag explicitly.
   116  		changeLogFromTag = "v" + newVersion.Prev().String()
   117  		exists, _ := tagExists(changeLogFromTag)
   118  		if !exists {
   119  			// fall back to one that exists.
   120  			changeLogFromTag = ""
   121  		}
   122  	}
   123  
   124  	var (
   125  		gitCommits     gitInfos
   126  		gitCommitsDocs gitInfos
   127  		relNotesState  releaseNotesState
   128  	)
   129  
   130  	relNotesState, err = r.releaseNotesState(version)
   131  	if err != nil {
   132  		return err
   133  	}
   134  
   135  	prepareReleaseNotes := isPatch || relNotesState == releaseNotesNone
   136  	shouldRelease := isPatch || relNotesState == releaseNotesReady
   137  
   138  	defer r.gitPush() // TODO(bep)
   139  
   140  	if prepareReleaseNotes || shouldRelease {
   141  		gitCommits, err = getGitInfos(changeLogFromTag, "hugo", "", !r.try)
   142  		if err != nil {
   143  			return err
   144  		}
   145  
   146  		// TODO(bep) explicit tag?
   147  		gitCommitsDocs, err = getGitInfos("", "hugoDocs", "../hugoDocs", !r.try)
   148  		if err != nil {
   149  			return err
   150  		}
   151  	}
   152  
   153  	if relNotesState == releaseNotesCreated {
   154  		fmt.Println("Release notes created, but not ready. Rename to *-ready.md to continue ...")
   155  		return nil
   156  	}
   157  
   158  	if prepareReleaseNotes {
   159  		releaseNotesFile, err := r.writeReleaseNotesToTemp(version, isPatch, gitCommits, gitCommitsDocs)
   160  		if err != nil {
   161  			return err
   162  		}
   163  
   164  		if _, err := r.git("add", releaseNotesFile); err != nil {
   165  			return err
   166  		}
   167  
   168  		commitMsg := fmt.Sprintf("%s Add release notes for %s", commitPrefix, newVersion)
   169  		if !isPatch {
   170  			commitMsg += "\n\nRename to *-ready.md to continue."
   171  		}
   172  		commitMsg += "\n[ci skip]"
   173  
   174  		if _, err := r.git("commit", "-m", commitMsg); err != nil {
   175  			return err
   176  		}
   177  	}
   178  
   179  	if !shouldRelease {
   180  		fmt.Printf("Skip release ... ")
   181  		return nil
   182  	}
   183  
   184  	// For docs, for now we assume that:
   185  	// The /docs subtree is up to date and ready to go.
   186  	// The hugoDocs/dev and hugoDocs/master must be merged manually after release.
   187  	// TODO(bep) improve this when we see how it works.
   188  
   189  	if err := r.bumpVersions(newVersion); err != nil {
   190  		return err
   191  	}
   192  
   193  	if _, err := r.git("commit", "-a", "-m", fmt.Sprintf("%s Bump versions for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil {
   194  		return err
   195  	}
   196  
   197  	releaseNotesFile := getReleaseNotesDocsTempFilename(version, true)
   198  
   199  	title, description := version, version
   200  	if isPatch {
   201  		title = "Hugo " + version + ": A couple of Bug Fixes"
   202  		description = "This version fixes a couple of bugs introduced in " + mainVersion.String() + "."
   203  	}
   204  
   205  	// Write the release notes to the docs site as well.
   206  	docFile, err := r.writeReleaseNotesToDocs(title, description, releaseNotesFile)
   207  	if err != nil {
   208  		return err
   209  	}
   210  
   211  	if _, err := r.git("add", docFile); err != nil {
   212  		return err
   213  	}
   214  	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 {
   215  		return err
   216  	}
   217  
   218  	if _, err := r.git("tag", "-a", tag, "-m", fmt.Sprintf("%s %s [ci skip]", commitPrefix, newVersion)); err != nil {
   219  		return err
   220  	}
   221  
   222  	if !r.skipPublish {
   223  		if _, err := r.git("push", "origin", tag); err != nil {
   224  			return err
   225  		}
   226  	}
   227  
   228  	if err := r.release(releaseNotesFile); err != nil {
   229  		return err
   230  	}
   231  
   232  	if err := r.bumpVersions(finalVersion); err != nil {
   233  		return err
   234  	}
   235  
   236  	if !r.try {
   237  		// No longer needed.
   238  		if err := os.Remove(releaseNotesFile); err != nil {
   239  			return err
   240  		}
   241  	}
   242  
   243  	if _, err := r.git("commit", "-a", "-m", fmt.Sprintf("%s Prepare repository for %s\n\n[ci skip]", commitPrefix, finalVersion)); err != nil {
   244  		return err
   245  	}
   246  
   247  	return nil
   248  }
   249  
   250  func (r *ReleaseHandler) gitPush() {
   251  	if r.skipPublish {
   252  		return
   253  	}
   254  	if _, err := r.git("push", "origin", "HEAD"); err != nil {
   255  		log.Fatal("push failed:", err)
   256  	}
   257  }
   258  
   259  func (r *ReleaseHandler) release(releaseNotesFile string) error {
   260  	if r.try {
   261  		fmt.Println("Skip goreleaser...")
   262  		return nil
   263  	}
   264  
   265  	args := []string{"--parallelism", "3", "--timeout", "120m", "--rm-dist", "--release-notes", releaseNotesFile}
   266  	if r.skipPublish {
   267  		args = append(args, "--skip-publish")
   268  	}
   269  
   270  	cmd, _ := hexec.SafeCommand("goreleaser", args...)
   271  	cmd.Stdout = os.Stdout
   272  	cmd.Stderr = os.Stderr
   273  	err := cmd.Run()
   274  	if err != nil {
   275  		return errors.Wrap(err, "goreleaser failed")
   276  	}
   277  	return nil
   278  }
   279  
   280  func (r *ReleaseHandler) bumpVersions(ver hugo.Version) error {
   281  	toDev := ""
   282  
   283  	if ver.Suffix != "" {
   284  		toDev = ver.Suffix
   285  	}
   286  
   287  	if err := r.replaceInFile("common/hugo/version_current.go",
   288  		`Number:(\s{4,})(.*),`, fmt.Sprintf(`Number:${1}%.2f,`, ver.Number),
   289  		`PatchLevel:(\s*)(.*),`, fmt.Sprintf(`PatchLevel:${1}%d,`, ver.PatchLevel),
   290  		`Suffix:(\s{4,})".*",`, fmt.Sprintf(`Suffix:${1}"%s",`, toDev)); err != nil {
   291  		return err
   292  	}
   293  
   294  	snapcraftGrade := "stable"
   295  	if ver.Suffix != "" {
   296  		snapcraftGrade = "devel"
   297  	}
   298  	if err := r.replaceInFile("snap/snapcraft.yaml",
   299  		`version: "(.*)"`, fmt.Sprintf(`version: "%s"`, ver),
   300  		`grade: (.*) #`, fmt.Sprintf(`grade: %s #`, snapcraftGrade)); err != nil {
   301  		return err
   302  	}
   303  
   304  	var minVersion string
   305  	if ver.Suffix != "" {
   306  		// People use the DEV version in daily use, and we cannot create new themes
   307  		// with the next version before it is released.
   308  		minVersion = ver.Prev().String()
   309  	} else {
   310  		minVersion = ver.String()
   311  	}
   312  
   313  	if err := r.replaceInFile("commands/new.go",
   314  		`min_version = "(.*)"`, fmt.Sprintf(`min_version = "%s"`, minVersion)); err != nil {
   315  		return err
   316  	}
   317  
   318  	return nil
   319  }
   320  
   321  func (r *ReleaseHandler) replaceInFile(filename string, oldNew ...string) error {
   322  	fullFilename := hugoFilepath(filename)
   323  	fi, err := os.Stat(fullFilename)
   324  	if err != nil {
   325  		return err
   326  	}
   327  
   328  	if r.try {
   329  		fmt.Printf("Replace in %q: %q\n", filename, oldNew)
   330  		return nil
   331  	}
   332  
   333  	b, err := ioutil.ReadFile(fullFilename)
   334  	if err != nil {
   335  		return err
   336  	}
   337  	newContent := string(b)
   338  
   339  	for i := 0; i < len(oldNew); i += 2 {
   340  		re := regexp.MustCompile(oldNew[i])
   341  		newContent = re.ReplaceAllString(newContent, oldNew[i+1])
   342  	}
   343  
   344  	return ioutil.WriteFile(fullFilename, []byte(newContent), fi.Mode())
   345  }
   346  
   347  func hugoFilepath(filename string) string {
   348  	pwd, err := os.Getwd()
   349  	if err != nil {
   350  		log.Fatal(err)
   351  	}
   352  	return filepath.Join(pwd, filename)
   353  }
   354  
   355  func isCI() bool {
   356  	return os.Getenv("CI") != ""
   357  }