mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05: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:
parent
0e87b18b66
commit
7f6430d84d
23 changed files with 1309 additions and 47 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -14,4 +14,5 @@ vendor/*/
|
|||
*.bench
|
||||
coverage*.out
|
||||
|
||||
GoBuilds
|
||||
GoBuilds
|
||||
dist
|
||||
|
|
|
@ -36,7 +36,7 @@ in the "man" directory under the current directory.`,
|
|||
header := &doc.GenManHeader{
|
||||
Section: "1",
|
||||
Manual: "Hugo Manual",
|
||||
Source: fmt.Sprintf("Hugo %s", helpers.HugoVersion()),
|
||||
Source: fmt.Sprintf("Hugo %s", helpers.CurrentHugoVersion),
|
||||
}
|
||||
if !strings.HasSuffix(genmandir, helpers.FilePathSeparator) {
|
||||
genmandir += helpers.FilePathSeparator
|
||||
|
|
|
@ -399,7 +399,7 @@ func InitializeConfig(subCmdVs ...*cobra.Command) (*deps.DepsCfg, error) {
|
|||
|
||||
if themeVersionMismatch {
|
||||
cfg.Logger.ERROR.Printf("Current theme does not support Hugo version %s. Minimum version required is %s\n",
|
||||
helpers.HugoReleaseVersion(), minVersion)
|
||||
helpers.CurrentHugoVersion.ReleaseVersion(), minVersion)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
|
|
62
commands/release.go
Normal file
62
commands/release.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
// +build release
|
||||
|
||||
// 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 commands
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/hugo/releaser"
|
||||
)
|
||||
|
||||
func init() {
|
||||
HugoCmd.AddCommand(createReleaser().cmd)
|
||||
}
|
||||
|
||||
type releaseCommandeer struct {
|
||||
cmd *cobra.Command
|
||||
|
||||
// Will be zero for main releases.
|
||||
patchLevel int
|
||||
|
||||
skipPublish bool
|
||||
|
||||
step int
|
||||
}
|
||||
|
||||
func createReleaser() *releaseCommandeer {
|
||||
// Note: This is a command only meant for internal use and must be run
|
||||
// via "go run -tags release main.go release" on the actual code base that is in the release.
|
||||
r := &releaseCommandeer{
|
||||
cmd: &cobra.Command{
|
||||
Use: "release",
|
||||
Short: "Release a new version of Hugo.",
|
||||
Hidden: true,
|
||||
},
|
||||
}
|
||||
|
||||
r.cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
return r.release()
|
||||
}
|
||||
|
||||
r.cmd.PersistentFlags().IntVarP(&r.patchLevel, "patch", "p", 0, "Patch level, defaults to 0 for main releases")
|
||||
r.cmd.PersistentFlags().IntVarP(&r.step, "step", "s", -1, "Release step, defaults to -1 for all steps.")
|
||||
r.cmd.PersistentFlags().BoolVarP(&r.skipPublish, "skip-publish", "", false, "Skip all publishing pipes of the release")
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *releaseCommandeer) release() error {
|
||||
return releaser.New(r.patchLevel, r.step, r.skipPublish).Run()
|
||||
}
|
|
@ -44,9 +44,9 @@ func printHugoVersion() {
|
|||
formatBuildDate() // format the compile time
|
||||
}
|
||||
if hugolib.CommitHash == "" {
|
||||
jww.FEEDBACK.Printf("Hugo Static Site Generator v%s %s/%s BuildDate: %s\n", helpers.HugoVersion(), runtime.GOOS, runtime.GOARCH, hugolib.BuildDate)
|
||||
jww.FEEDBACK.Printf("Hugo Static Site Generator v%s %s/%s BuildDate: %s\n", helpers.CurrentHugoVersion, runtime.GOOS, runtime.GOARCH, hugolib.BuildDate)
|
||||
} else {
|
||||
jww.FEEDBACK.Printf("Hugo Static Site Generator v%s-%s %s/%s BuildDate: %s\n", helpers.HugoVersion(), strings.ToUpper(hugolib.CommitHash), runtime.GOOS, runtime.GOARCH, hugolib.BuildDate)
|
||||
jww.FEEDBACK.Printf("Hugo Static Site Generator v%s-%s %s/%s BuildDate: %s\n", helpers.CurrentHugoVersion, strings.ToUpper(hugolib.CommitHash), runtime.GOOS, runtime.GOARCH, hugolib.BuildDate)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -45,6 +45,11 @@ pluralizeListTitles = false
|
|||
identifier = "about"
|
||||
pre = "<i class='fa fa-heart'></i>"
|
||||
weight = -110
|
||||
[[menu.main]]
|
||||
name = "Release Notes"
|
||||
url = "/release-notes/"
|
||||
pre = "<i class='fa fa-newspaper-o'></i>"
|
||||
weight = -111
|
||||
[[menu.main]]
|
||||
name = "Getting Started"
|
||||
identifier = "getting started"
|
||||
|
|
8
docs/content/release-notes/_index.md
Normal file
8
docs/content/release-notes/_index.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
date: 2017-04-17
|
||||
aliases:
|
||||
- /doc/release-notes/
|
||||
- /meta/release-notes/
|
||||
title: Release Notes
|
||||
weight: 10
|
||||
---
|
|
@ -2,12 +2,8 @@
|
|||
aliases:
|
||||
- /doc/release-notes/
|
||||
- /meta/release-notes/
|
||||
date: 2013-07-01
|
||||
menu:
|
||||
main:
|
||||
parent: about
|
||||
title: Release Notes
|
||||
weight: 10
|
||||
date: 2017-04-16
|
||||
title: Older Release Notes
|
||||
---
|
||||
# **0.20.2** April 16th 2017
|
||||
|
6
docs/layouts/section/release-notes.html
Normal file
6
docs/layouts/section/release-notes.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
{{ define "main" }}
|
||||
{{ range .Pages }}
|
||||
<h1>{{ .Title }} {{ .Date.Format "Jan 2, 2006" }}</h1>
|
||||
{{ .Content }}
|
||||
{{ end }}
|
||||
{{ end }}
|
48
goreleaser.yml
Normal file
48
goreleaser.yml
Normal file
|
@ -0,0 +1,48 @@
|
|||
build:
|
||||
main: main.go
|
||||
binary: hugo
|
||||
ldflags_template: -s -w -X hugolib.BuildDate={{.Date}}
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
- windows
|
||||
- freebsd
|
||||
- netbsd
|
||||
- openbsd
|
||||
- dragonfly
|
||||
goarch:
|
||||
- amd64
|
||||
- 386
|
||||
- arm
|
||||
- arm64
|
||||
fpm:
|
||||
formats:
|
||||
- deb
|
||||
vendor: "gohugo.io"
|
||||
url: "https://gohugo.io/"
|
||||
maintainer: "<Bjørn Erik Pedersen bjorn.erik.pedersen@gmail.com>"
|
||||
description: "A Fast and Flexible Static Site Generator built with love in GoLang."
|
||||
license: "Apache 2.0"
|
||||
archive:
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
name_template: "{{.Binary}}_{{.Version}}_{{.Os}}-{{.Arch}}"
|
||||
replacements:
|
||||
amd64: 64bit
|
||||
386: 32bit
|
||||
arm: ARM
|
||||
arm64: ARM64
|
||||
darwin: macOS
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
openbsd: OpenBSD
|
||||
netbsd: NetBSD
|
||||
freebsd: FreeBSD
|
||||
dragonfly: DragonFlyBSD
|
||||
files:
|
||||
- README.md
|
||||
- LICENSE.md
|
||||
release:
|
||||
draft: true
|
|
@ -286,7 +286,7 @@ func InitLoggers() {
|
|||
// plenty of time to fix their templates.
|
||||
func Deprecated(object, item, alternative string, err bool) {
|
||||
if err {
|
||||
DistinctErrorLog.Printf("%s's %s is deprecated and will be removed in Hugo %s. %s.", object, item, NextHugoReleaseVersion(), alternative)
|
||||
DistinctErrorLog.Printf("%s's %s is deprecated and will be removed in Hugo %s. %s.", object, item, CurrentHugoVersion.Next().ReleaseVersion(), alternative)
|
||||
|
||||
} else {
|
||||
// Make sure the users see this while avoiding build breakage. This will not lead to an os.Exit(-1)
|
||||
|
|
|
@ -20,35 +20,51 @@ import (
|
|||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// HugoVersionNumber represents the current build version.
|
||||
// This should be the only one
|
||||
const (
|
||||
// HugoVersion represents the Hugo build version.
|
||||
type HugoVersion struct {
|
||||
// Major and minor version.
|
||||
HugoVersionNumber = 0.21
|
||||
Number float32
|
||||
|
||||
// Increment this for bug releases
|
||||
HugoPatchVersion = 0
|
||||
)
|
||||
PatchLevel int
|
||||
|
||||
// HugoVersionSuffix is the suffix used in the Hugo version string.
|
||||
// It will be blank for release versions.
|
||||
const HugoVersionSuffix = "-DEV" // use this when not doing a release
|
||||
//const HugoVersionSuffix = "" // use this line when doing a release
|
||||
|
||||
// HugoVersion returns the current Hugo version. It will include
|
||||
// a suffix, typically '-DEV', if it's development version.
|
||||
func HugoVersion() string {
|
||||
return hugoVersion(HugoVersionNumber, HugoPatchVersion, HugoVersionSuffix)
|
||||
// HugoVersionSuffix is the suffix used in the Hugo version string.
|
||||
// It will be blank for release versions.
|
||||
Suffix string
|
||||
}
|
||||
|
||||
// HugoReleaseVersion is same as HugoVersion, but no suffix.
|
||||
func HugoReleaseVersion() string {
|
||||
return hugoVersionNoSuffix(HugoVersionNumber, HugoPatchVersion)
|
||||
func (v HugoVersion) String() string {
|
||||
return hugoVersion(v.Number, v.PatchLevel, v.Suffix)
|
||||
}
|
||||
|
||||
// NextHugoReleaseVersion returns the next Hugo release version.
|
||||
func NextHugoReleaseVersion() string {
|
||||
return hugoVersionNoSuffix(HugoVersionNumber+0.01, 0)
|
||||
// ReleaseVersion represents the release version.
|
||||
func (v HugoVersion) ReleaseVersion() HugoVersion {
|
||||
v.Suffix = ""
|
||||
return v
|
||||
}
|
||||
|
||||
// Next returns the next Hugo release version.
|
||||
func (v HugoVersion) Next() HugoVersion {
|
||||
return HugoVersion{Number: v.Number + 0.01}
|
||||
}
|
||||
|
||||
// Pre returns the previous Hugo release version.
|
||||
func (v HugoVersion) Prev() HugoVersion {
|
||||
return HugoVersion{Number: v.Number - 0.01}
|
||||
}
|
||||
|
||||
// NextPatchLevel returns the next patch/bugfix Hugo version.
|
||||
// This will be a patch increment on the previous Hugo version.
|
||||
func (v HugoVersion) NextPatchLevel(level int) HugoVersion {
|
||||
return HugoVersion{Number: v.Number - 0.01, PatchLevel: level}
|
||||
}
|
||||
|
||||
// CurrentHugoVersion represents the current build version.
|
||||
// This should be the only one.
|
||||
var CurrentHugoVersion = HugoVersion{
|
||||
Number: 0.21,
|
||||
PatchLevel: 0,
|
||||
Suffix: "-DEV",
|
||||
}
|
||||
|
||||
func hugoVersion(version float32, patchVersion int, suffix string) string {
|
||||
|
@ -58,19 +74,12 @@ func hugoVersion(version float32, patchVersion int, suffix string) string {
|
|||
return fmt.Sprintf("%.2f%s", version, suffix)
|
||||
}
|
||||
|
||||
func hugoVersionNoSuffix(version float32, patchVersion int) string {
|
||||
if patchVersion > 0 {
|
||||
return fmt.Sprintf("%.2f.%d", version, patchVersion)
|
||||
}
|
||||
return fmt.Sprintf("%.2f", version)
|
||||
}
|
||||
|
||||
// CompareVersion compares the given version string or number against the
|
||||
// running Hugo version.
|
||||
// It returns -1 if the given version is less than, 0 if equal and 1 if greater than
|
||||
// the running version.
|
||||
func CompareVersion(version interface{}) int {
|
||||
return compareVersions(HugoVersionNumber, HugoPatchVersion, version)
|
||||
return compareVersions(CurrentHugoVersion.Number, CurrentHugoVersion.PatchLevel, version)
|
||||
}
|
||||
|
||||
func compareVersions(inVersion float32, inPatchVersion int, in interface{}) int {
|
||||
|
|
|
@ -22,10 +22,14 @@ import (
|
|||
|
||||
func TestHugoVersion(t *testing.T) {
|
||||
assert.Equal(t, "0.15-DEV", hugoVersion(0.15, 0, "-DEV"))
|
||||
assert.Equal(t, "0.17", hugoVersionNoSuffix(0.16+0.01, 0))
|
||||
assert.Equal(t, "0.20", hugoVersionNoSuffix(0.20, 0))
|
||||
assert.Equal(t, "0.15.2-DEV", hugoVersion(0.15, 2, "-DEV"))
|
||||
assert.Equal(t, "0.17.3", hugoVersionNoSuffix(0.16+0.01, 3))
|
||||
|
||||
v := HugoVersion{Number: 0.21, PatchLevel: 0, Suffix: "-DEV"}
|
||||
|
||||
require.Equal(t, v.ReleaseVersion().String(), "0.21")
|
||||
require.Equal(t, "0.21-DEV", v.String())
|
||||
require.Equal(t, "0.22", v.Next().String())
|
||||
require.Equal(t, "0.20.3", v.NextPatchLevel(3).String())
|
||||
}
|
||||
|
||||
func TestCompareVersions(t *testing.T) {
|
||||
|
|
|
@ -41,9 +41,9 @@ type HugoInfo struct {
|
|||
|
||||
func init() {
|
||||
hugoInfo = &HugoInfo{
|
||||
Version: helpers.HugoVersion(),
|
||||
Version: helpers.CurrentHugoVersion.String(),
|
||||
CommitHash: CommitHash,
|
||||
BuildDate: BuildDate,
|
||||
Generator: template.HTML(fmt.Sprintf(`<meta name="generator" content="Hugo %s" />`, helpers.HugoVersion())),
|
||||
Generator: template.HTML(fmt.Sprintf(`<meta name="generator" content="Hugo %s" />`, helpers.CurrentHugoVersion.String())),
|
||||
}
|
||||
}
|
||||
|
|
265
releaser/git.go
Normal file
265
releaser/git.go
Normal file
|
@ -0,0 +1,265 @@
|
|||
// 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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var issueRe = regexp.MustCompile(`(?i)[Updates?|Closes?|Fix.*|See] #(\d+)`)
|
||||
|
||||
const (
|
||||
templateChanges = "templateChanges"
|
||||
coreChanges = "coreChanges"
|
||||
outChanges = "outChanges"
|
||||
docsChanges = "docsChanges"
|
||||
otherChanges = "otherChanges"
|
||||
)
|
||||
|
||||
type changeLog struct {
|
||||
Version string
|
||||
Enhancements map[string]gitInfos
|
||||
Fixes map[string]gitInfos
|
||||
All gitInfos
|
||||
|
||||
// Overall stats
|
||||
Repo *gitHubRepo
|
||||
ContributorCount int
|
||||
ThemeCount int
|
||||
}
|
||||
|
||||
func newChangeLog(infos gitInfos) changeLog {
|
||||
return changeLog{
|
||||
Enhancements: make(map[string]gitInfos),
|
||||
Fixes: make(map[string]gitInfos),
|
||||
All: infos,
|
||||
}
|
||||
}
|
||||
|
||||
func (l changeLog) addGitInfo(isFix bool, info gitInfo, category string) {
|
||||
var (
|
||||
infos gitInfos
|
||||
found bool
|
||||
segment map[string]gitInfos
|
||||
)
|
||||
|
||||
if isFix {
|
||||
segment = l.Fixes
|
||||
} else {
|
||||
segment = l.Enhancements
|
||||
}
|
||||
|
||||
infos, found = segment[category]
|
||||
if !found {
|
||||
infos = gitInfos{}
|
||||
}
|
||||
|
||||
infos = append(infos, info)
|
||||
segment[category] = infos
|
||||
}
|
||||
|
||||
func gitInfosToChangeLog(infos gitInfos) changeLog {
|
||||
log := newChangeLog(infos)
|
||||
for _, info := range infos {
|
||||
los := strings.ToLower(info.Subject)
|
||||
isFix := strings.Contains(los, "fix")
|
||||
var category = otherChanges
|
||||
|
||||
// TODO(bep) improve
|
||||
if regexp.MustCompile("(?i)tpl:|tplimpl:|layout").MatchString(los) {
|
||||
category = templateChanges
|
||||
} else if regexp.MustCompile("(?i)docs?:|documentation:").MatchString(los) {
|
||||
category = docsChanges
|
||||
} else if regexp.MustCompile("(?i)hugolib:").MatchString(los) {
|
||||
category = coreChanges
|
||||
} else if regexp.MustCompile("(?i)out(put)?:|media:|Output|Media").MatchString(los) {
|
||||
category = outChanges
|
||||
}
|
||||
|
||||
// Trim package prefix.
|
||||
colonIdx := strings.Index(info.Subject, ":")
|
||||
if colonIdx != -1 && colonIdx < (len(info.Subject)/2) {
|
||||
info.Subject = info.Subject[colonIdx+1:]
|
||||
}
|
||||
|
||||
info.Subject = strings.TrimSpace(info.Subject)
|
||||
|
||||
log.addGitInfo(isFix, info, category)
|
||||
}
|
||||
|
||||
return log
|
||||
}
|
||||
|
||||
type gitInfo struct {
|
||||
Hash string
|
||||
Author string
|
||||
Subject string
|
||||
Body string
|
||||
|
||||
GitHubCommit *gitHubCommit
|
||||
}
|
||||
|
||||
func (g gitInfo) Issues() []int {
|
||||
return extractIssues(g.Body)
|
||||
}
|
||||
|
||||
func (g gitInfo) AuthorID() string {
|
||||
if g.GitHubCommit != nil {
|
||||
return g.GitHubCommit.Author.Login
|
||||
}
|
||||
return g.Author
|
||||
}
|
||||
|
||||
func extractIssues(body string) []int {
|
||||
var i []int
|
||||
m := issueRe.FindAllStringSubmatch(body, -1)
|
||||
for _, mm := range m {
|
||||
issueID, err := strconv.Atoi(mm[1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
i = append(i, issueID)
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
type gitInfos []gitInfo
|
||||
|
||||
func git(args ...string) (string, error) {
|
||||
cmd := exec.Command("git", args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("git failed: %q: %q", err, out)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
func getGitInfos(remote bool) (gitInfos, error) {
|
||||
return getGitInfosBefore("HEAD", remote)
|
||||
}
|
||||
|
||||
type countribCount struct {
|
||||
Author string
|
||||
GitHubAuthor gitHubAuthor
|
||||
Count int
|
||||
}
|
||||
|
||||
func (c countribCount) AuthorLink() string {
|
||||
if c.GitHubAuthor.HtmlURL != "" {
|
||||
return fmt.Sprintf("[@%s](%s)", c.GitHubAuthor.Login, c.GitHubAuthor.HtmlURL)
|
||||
}
|
||||
|
||||
if !strings.Contains(c.Author, "@") {
|
||||
return c.Author
|
||||
}
|
||||
|
||||
return c.Author[:strings.Index(c.Author, "@")]
|
||||
|
||||
}
|
||||
|
||||
type contribCounts []countribCount
|
||||
|
||||
func (c contribCounts) Less(i, j int) bool { return c[i].Count > c[j].Count }
|
||||
func (c contribCounts) Len() int { return len(c) }
|
||||
func (c contribCounts) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
|
||||
|
||||
func (g gitInfos) ContribCountPerAuthor() contribCounts {
|
||||
var c contribCounts
|
||||
|
||||
counters := make(map[string]countribCount)
|
||||
|
||||
for _, gi := range g {
|
||||
authorID := gi.AuthorID()
|
||||
if count, ok := counters[authorID]; ok {
|
||||
count.Count = count.Count + 1
|
||||
counters[authorID] = count
|
||||
} else {
|
||||
var ghA gitHubAuthor
|
||||
if gi.GitHubCommit != nil {
|
||||
ghA = gi.GitHubCommit.Author
|
||||
}
|
||||
authorCount := countribCount{Count: 1, Author: gi.Author, GitHubAuthor: ghA}
|
||||
counters[authorID] = authorCount
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range counters {
|
||||
c = append(c, v)
|
||||
}
|
||||
|
||||
sort.Sort(c)
|
||||
return c
|
||||
}
|
||||
|
||||
func getGitInfosBefore(ref string, remote bool) (gitInfos, error) {
|
||||
|
||||
var g gitInfos
|
||||
|
||||
log, err := gitLogBefore(ref)
|
||||
if err != nil {
|
||||
return g, err
|
||||
}
|
||||
|
||||
log = strings.Trim(log, "\n\x1e'")
|
||||
entries := strings.Split(log, "\x1e")
|
||||
|
||||
for _, entry := range entries {
|
||||
items := strings.Split(entry, "\x1f")
|
||||
gi := gitInfo{
|
||||
Hash: items[0],
|
||||
Author: items[1],
|
||||
Subject: items[2],
|
||||
Body: items[3],
|
||||
}
|
||||
if remote {
|
||||
gc, err := fetchCommit(gi.Hash)
|
||||
if err == nil {
|
||||
gi.GitHubCommit = &gc
|
||||
}
|
||||
}
|
||||
g = append(g, gi)
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// Ignore autogenerated commits etc. in change log. This is a regexp.
|
||||
const ignoredCommits = "release:|vendor:|snapcraft:"
|
||||
|
||||
func gitLogBefore(ref string) (string, error) {
|
||||
prevTag, err := gitShort("describe", "--tags", "--abbrev=0", "--always", ref+"^")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
log, err := git("log", "-E", fmt.Sprintf("--grep=%s", ignoredCommits), "--invert-grep", "--pretty=format:%x1e%h%x1f%aE%x1f%s%x1f%b", "--abbrev-commit", prevTag+".."+ref)
|
||||
if err != nil {
|
||||
return ",", err
|
||||
}
|
||||
|
||||
return log, err
|
||||
}
|
||||
|
||||
func gitLog() (string, error) {
|
||||
return gitLogBefore("HEAD")
|
||||
}
|
||||
|
||||
func gitShort(args ...string) (output string, err error) {
|
||||
output, err = git(args...)
|
||||
return strings.Replace(strings.Split(output, "\n")[0], "'", "", -1), err
|
||||
}
|
53
releaser/git_test.go
Normal file
53
releaser/git_test.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
// 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
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"runtime"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGitInfos(t *testing.T) {
|
||||
if runtime.GOOS == "linux" {
|
||||
// Travis has an ancient git with no --invert-grep: https://github.com/travis-ci/travis-ci/issues/6328
|
||||
t.Skip("Skip git test on Linux to make Travis happy.")
|
||||
}
|
||||
infos, err := getGitInfos(false)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.True(t, len(infos) > 0)
|
||||
|
||||
}
|
||||
|
||||
func TestIssuesRe(t *testing.T) {
|
||||
|
||||
body := `
|
||||
This is a commit message.
|
||||
|
||||
Updates #123
|
||||
Fix #345
|
||||
closes #543
|
||||
See #456
|
||||
`
|
||||
|
||||
issues := extractIssues(body)
|
||||
|
||||
require.Len(t, issues, 4)
|
||||
require.Equal(t, 123, issues[0])
|
||||
require.Equal(t, 543, issues[2])
|
||||
|
||||
}
|
129
releaser/github.go
Normal file
129
releaser/github.go
Normal file
|
@ -0,0 +1,129 @@
|
|||
package releaser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
gitHubCommitsApi = "https://api.github.com/repos/spf13/hugo/commits/%s"
|
||||
gitHubRepoApi = "https://api.github.com/repos/spf13/hugo"
|
||||
gitHubContributorsApi = "https://api.github.com/repos/spf13/hugo/contributors"
|
||||
)
|
||||
|
||||
type gitHubCommit struct {
|
||||
Author gitHubAuthor `json:"author"`
|
||||
HtmlURL string `json:"html_url"`
|
||||
}
|
||||
|
||||
type gitHubAuthor struct {
|
||||
ID int `json:"id"`
|
||||
Login string `json:"login"`
|
||||
HtmlURL string `json:"html_url"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
}
|
||||
|
||||
type gitHubRepo struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
HtmlURL string `json:"html_url"`
|
||||
Stars int `json:"stargazers_count"`
|
||||
Contributors []gitHubContributor
|
||||
}
|
||||
|
||||
type gitHubContributor struct {
|
||||
ID int `json:"id"`
|
||||
Login string `json:"login"`
|
||||
HtmlURL string `json:"html_url"`
|
||||
Contributions int `json:"contributions"`
|
||||
}
|
||||
|
||||
func fetchCommit(ref string) (gitHubCommit, error) {
|
||||
var commit gitHubCommit
|
||||
|
||||
u := fmt.Sprintf(gitHubCommitsApi, ref)
|
||||
|
||||
req, err := http.NewRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return commit, err
|
||||
}
|
||||
|
||||
err = doGitHubRequest(req, &commit)
|
||||
|
||||
return commit, err
|
||||
}
|
||||
|
||||
func fetchRepo() (gitHubRepo, error) {
|
||||
var repo gitHubRepo
|
||||
|
||||
req, err := http.NewRequest("GET", gitHubRepoApi, nil)
|
||||
if err != nil {
|
||||
return repo, err
|
||||
}
|
||||
|
||||
err = doGitHubRequest(req, &repo)
|
||||
if err != nil {
|
||||
return repo, err
|
||||
}
|
||||
|
||||
var contributors []gitHubContributor
|
||||
page := 0
|
||||
for {
|
||||
page++
|
||||
var currPage []gitHubContributor
|
||||
url := fmt.Sprintf(gitHubContributorsApi+"?page=%d", page)
|
||||
|
||||
req, err = http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return repo, err
|
||||
}
|
||||
|
||||
err = doGitHubRequest(req, &currPage)
|
||||
if err != nil {
|
||||
return repo, err
|
||||
}
|
||||
if len(currPage) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
contributors = append(contributors, currPage...)
|
||||
|
||||
}
|
||||
|
||||
repo.Contributors = contributors
|
||||
|
||||
return repo, err
|
||||
|
||||
}
|
||||
|
||||
func doGitHubRequest(req *http.Request, v interface{}) error {
|
||||
addGitHubToken(req)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if isError(resp) {
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
return fmt.Errorf("GitHub lookup failed: %s", string(b))
|
||||
}
|
||||
|
||||
return json.NewDecoder(resp.Body).Decode(v)
|
||||
}
|
||||
|
||||
func isError(resp *http.Response) bool {
|
||||
return resp.StatusCode < 200 || resp.StatusCode > 299
|
||||
}
|
||||
|
||||
func addGitHubToken(req *http.Request) {
|
||||
gitHubToken := os.Getenv("GITHUB_TOKEN")
|
||||
if gitHubToken != "" {
|
||||
req.Header.Add("Authorization", "token "+gitHubToken)
|
||||
}
|
||||
}
|
42
releaser/github_test.go
Normal file
42
releaser/github_test.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
// 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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGitHubLookupCommit(t *testing.T) {
|
||||
skipIfNoToken(t)
|
||||
commit, err := fetchCommit("793554108763c0984f1a1b1a6ee5744b560d78d0")
|
||||
require.NoError(t, err)
|
||||
fmt.Println(commit)
|
||||
}
|
||||
|
||||
func TestFetchRepo(t *testing.T) {
|
||||
skipIfNoToken(t)
|
||||
repo, err := fetchRepo()
|
||||
require.NoError(t, err)
|
||||
fmt.Println(">>", len(repo.Contributors))
|
||||
}
|
||||
|
||||
func skipIfNoToken(t *testing.T) {
|
||||
if os.Getenv("GITHUB_TOKEN") == "" {
|
||||
t.Skip("Skip test against GitHub as no GITHUB_TOKEN set.")
|
||||
}
|
||||
}
|
245
releaser/releasenotes_writer.go
Normal file
245
releaser/releasenotes_writer.go
Normal file
|
@ -0,0 +1,245 @@
|
|||
// 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 release implements a set of utilities and a wrapper around Goreleaser
|
||||
// to help automate the Hugo release process.
|
||||
package releaser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
issueLinkTemplate = "[#%d](https://github.com/spf13/hugo/issues/%d)"
|
||||
linkTemplate = "[%s](%s)"
|
||||
releaseNotesMarkdownTemplate = `
|
||||
{{- $patchRelease := isPatch . -}}
|
||||
{{- $contribsPerAuthor := .All.ContribCountPerAuthor -}}
|
||||
|
||||
{{- if $patchRelease }}
|
||||
{{ if eq (len .All) 1 }}
|
||||
This is a bug-fix release with one important fix.
|
||||
{{ else }}
|
||||
This is a bug-fix relase with a couple of important fixes.
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
This release represents **{{ len .All }} contributions by {{ len $contribsPerAuthor }} contributors** to the main Hugo code base.
|
||||
{{ end -}}
|
||||
|
||||
{{- if gt (len $contribsPerAuthor) 3 -}}
|
||||
{{- $u1 := index $contribsPerAuthor 0 -}}
|
||||
{{- $u2 := index $contribsPerAuthor 1 -}}
|
||||
{{- $u3 := index $contribsPerAuthor 2 -}}
|
||||
{{- $u4 := index $contribsPerAuthor 3 -}}
|
||||
{{- $u1.AuthorLink }} leads the Hugo development with a significant amount of contributions, but also a big shoutout to {{ $u2.AuthorLink }}, {{ $u3.AuthorLink }}, and {{ $u4.AuthorLink }} for their ongoing contributions.
|
||||
And as always a big thanks to [@digitalcraftsman](https://github.com/digitalcraftsman) for his relentless work on keeping the documentation and the themes site in pristine condition.
|
||||
{{ end }}
|
||||
Hugo now has:
|
||||
|
||||
{{ with .Repo -}}
|
||||
* {{ .Stars }}+ [stars](https://github.com/spf13/hugo/stargazers)
|
||||
* {{ len .Contributors }}+ [contributors](https://github.com/spf13/hugo/graphs/contributors)
|
||||
{{- end -}}
|
||||
{{ with .ThemeCount }}
|
||||
* 156+ [themes](http://themes.gohugo.io/)
|
||||
{{- end }}
|
||||
|
||||
## Enhancements
|
||||
{{ template "change-headers" .Enhancements -}}
|
||||
## Fixes
|
||||
{{ template "change-headers" .Fixes -}}
|
||||
|
||||
{{ define "change-headers" }}
|
||||
{{ $tmplChanges := index . "templateChanges" -}}
|
||||
{{- $outChanges := index . "outChanges" -}}
|
||||
{{- $coreChanges := index . "coreChanges" -}}
|
||||
{{- $docsChanges := index . "docsChanges" -}}
|
||||
{{- $otherChanges := index . "otherChanges" -}}
|
||||
{{- with $tmplChanges -}}
|
||||
### Templates
|
||||
{{ template "change-section" . }}
|
||||
{{- end -}}
|
||||
{{- with $outChanges -}}
|
||||
### Output
|
||||
{{- template "change-section" . }}
|
||||
{{- end -}}
|
||||
{{- with $coreChanges -}}
|
||||
### Core
|
||||
{{ template "change-section" . }}
|
||||
{{- end -}}
|
||||
{{- with $docsChanges -}}
|
||||
### Docs
|
||||
{{- template "change-section" . }}
|
||||
{{- end -}}
|
||||
{{- with $otherChanges -}}
|
||||
### Other
|
||||
{{ template "change-section" . }}
|
||||
{{- end -}}
|
||||
{{ end }}
|
||||
|
||||
|
||||
{{ define "change-section" }}
|
||||
{{ range . }}
|
||||
{{- if .GitHubCommit -}}
|
||||
* {{ .Subject }} {{ . | commitURL }} {{ . | authorURL }} {{ range .Issues }}{{ . | issue }} {{ end }}
|
||||
{{ else -}}
|
||||
* {{ .Subject }} {{ range .Issues }}{{ . | issue }} {{ end }}
|
||||
{{ end -}}
|
||||
{{- end }}
|
||||
{{ end }}
|
||||
`
|
||||
)
|
||||
|
||||
var templateFuncs = template.FuncMap{
|
||||
"isPatch": func(c changeLog) bool {
|
||||
return strings.Count(c.Version, ".") > 1
|
||||
},
|
||||
"issue": func(id int) string {
|
||||
return fmt.Sprintf(issueLinkTemplate, id, id)
|
||||
},
|
||||
"commitURL": func(info gitInfo) string {
|
||||
if info.GitHubCommit.HtmlURL == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf(linkTemplate, info.Hash, info.GitHubCommit.HtmlURL)
|
||||
},
|
||||
"authorURL": func(info gitInfo) string {
|
||||
if info.GitHubCommit.Author.Login == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf(linkTemplate, "@"+info.GitHubCommit.Author.Login, info.GitHubCommit.Author.HtmlURL)
|
||||
},
|
||||
}
|
||||
|
||||
func writeReleaseNotes(version string, infos gitInfos, to io.Writer) error {
|
||||
changes := gitInfosToChangeLog(infos)
|
||||
changes.Version = version
|
||||
repo, err := fetchRepo()
|
||||
if err == nil {
|
||||
changes.Repo = &repo
|
||||
}
|
||||
themeCount, err := fetchThemeCount()
|
||||
if err == nil {
|
||||
changes.ThemeCount = themeCount
|
||||
}
|
||||
|
||||
tmpl, err := template.New("").Funcs(templateFuncs).Parse(releaseNotesMarkdownTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tmpl.Execute(to, changes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func fetchThemeCount() (int, error) {
|
||||
resp, err := http.Get("https://github.com/spf13/hugoThemes/blob/master/.gitmodules")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
return bytes.Count(b, []byte("submodule")), nil
|
||||
}
|
||||
|
||||
func writeReleaseNotesToTmpFile(version string, infos gitInfos) (string, error) {
|
||||
f, err := ioutil.TempFile("", "hugorelease")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
if err := writeReleaseNotes(version, infos, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return f.Name(), nil
|
||||
}
|
||||
|
||||
func getRelaseNotesDocsTempDirAndName(version string) (string, string) {
|
||||
return hugoFilepath("docs/temp"), fmt.Sprintf("%s-relnotes.md", version)
|
||||
}
|
||||
|
||||
func getRelaseNotesDocsTempFilename(version string) string {
|
||||
return filepath.Join(getRelaseNotesDocsTempDirAndName(version))
|
||||
}
|
||||
|
||||
func writeReleaseNotesToDocsTemp(version string, infos gitInfos) (string, error) {
|
||||
docsTempPath, name := getRelaseNotesDocsTempDirAndName(version)
|
||||
os.Mkdir(docsTempPath, os.ModePerm)
|
||||
|
||||
f, err := os.Create(filepath.Join(docsTempPath, name))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
if err := writeReleaseNotes(version, infos, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return f.Name(), nil
|
||||
|
||||
}
|
||||
|
||||
func writeReleaseNotesToDocs(title, sourceFilename string) (string, error) {
|
||||
targetFilename := filepath.Base(sourceFilename)
|
||||
contentDir := hugoFilepath("docs/content/release-notes")
|
||||
targetFullFilename := filepath.Join(contentDir, targetFilename)
|
||||
os.Mkdir(contentDir, os.ModePerm)
|
||||
|
||||
b, err := ioutil.ReadFile(sourceFilename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
f, err := os.Create(targetFullFilename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.WriteString(fmt.Sprintf(`
|
||||
---
|
||||
date: %s
|
||||
title: %s
|
||||
---
|
||||
|
||||
`, time.Now().Format("2006-01-02"), title)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := f.Write(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return targetFullFilename, nil
|
||||
|
||||
}
|
44
releaser/releasenotes_writer_test.go
Normal file
44
releaser/releasenotes_writer_test.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
// 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 commands defines and implements command-line commands and flags
|
||||
// used by Hugo. Commands and flags are implemented using Cobra.
|
||||
|
||||
package releaser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"runtime"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReleaseNotesWriter(t *testing.T) {
|
||||
if runtime.GOOS == "linux" {
|
||||
// Travis has an ancient git with no --invert-grep: https://github.com/travis-ci/travis-ci/issues/6328
|
||||
t.Skip("Skip git test on Linux to make Travis happy.")
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
|
||||
// TODO(bep) consider to query GitHub directly for the gitlog with author info, probably faster.
|
||||
infos, err := getGitInfosBefore("v0.20", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, writeReleaseNotes("0.20", infos, &b))
|
||||
|
||||
fmt.Println(">>>", b.String())
|
||||
}
|
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)
|
||||
}
|
78
releaser/releaser_test.go
Normal file
78
releaser/releaser_test.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
// 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 commands defines and implements command-line commands and flags
|
||||
// used by Hugo. Commands and flags are implemented using Cobra.
|
||||
|
||||
package releaser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/hugo/helpers"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCalculateVersions(t *testing.T) {
|
||||
startVersion := helpers.HugoVersion{Number: 0.20, Suffix: "-DEV"}
|
||||
|
||||
tests := []struct {
|
||||
handler *ReleaseHandler
|
||||
version helpers.HugoVersion
|
||||
v1 string
|
||||
v2 string
|
||||
}{
|
||||
{
|
||||
New(0, 0, true),
|
||||
startVersion,
|
||||
"0.20",
|
||||
"0.21-DEV",
|
||||
},
|
||||
{
|
||||
New(2, 0, true),
|
||||
startVersion,
|
||||
"0.20.2",
|
||||
"0.20-DEV",
|
||||
},
|
||||
{
|
||||
New(0, 1, true),
|
||||
startVersion,
|
||||
"0.20",
|
||||
"0.21-DEV",
|
||||
},
|
||||
{
|
||||
New(0, 2, true),
|
||||
startVersion,
|
||||
"0.20",
|
||||
"0.21-DEV",
|
||||
},
|
||||
{
|
||||
New(3, 1, true),
|
||||
startVersion,
|
||||
"0.20.3",
|
||||
"0.20-DEV",
|
||||
},
|
||||
{
|
||||
New(3, 2, true),
|
||||
startVersion.Next(),
|
||||
"0.21",
|
||||
"0.21-DEV",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
v1, v2 := test.handler.calculateVersions(test.version)
|
||||
require.Equal(t, test.v1, v1.String(), "Release version")
|
||||
require.Equal(t, test.v2, v2.String(), "Final version")
|
||||
}
|
||||
}
|
|
@ -22,7 +22,7 @@ import (
|
|||
)
|
||||
|
||||
var metaTagsCheck = regexp.MustCompile(`(?i)<meta\s+name=['|"]?generator['|"]?`)
|
||||
var hugoGeneratorTag = fmt.Sprintf(`<meta name="generator" content="Hugo %s" />`, helpers.HugoVersion())
|
||||
var hugoGeneratorTag = fmt.Sprintf(`<meta name="generator" content="Hugo %s" />`, helpers.CurrentHugoVersion)
|
||||
|
||||
// HugoGeneratorInject injects a meta generator tag for Hugo if none present.
|
||||
func HugoGeneratorInject(ct contentTransformer) {
|
||||
|
|
Loading…
Reference in a new issue