summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2017-09-10 17:14:02 +0200
committerGitHub <noreply@github.com>2017-09-10 17:14:02 +0200
commitd2249c50991ba7b00b092aca6e315ca1a4de75a1 (patch)
treee640af21a0cf72f2576f3420dc270d29d325b41a
parentf4bf214137ebd24a0d12f16d3a98d9038e6eabd3 (diff)
Set up Hugo release flow on CircleCI
This rewrites the release logic to use CircleCI 2.0 and its approve workflow in combination with the state of the release notes to determine what to do next. Fixes #3779
-rw-r--r--.circleci/config.yml49
-rw-r--r--commands/release.go5
-rw-r--r--helpers/hugo.go4
-rw-r--r--helpers/hugo_test.go2
-rw-r--r--releaser/git.go10
-rw-r--r--releaser/git_test.go5
-rw-r--r--releaser/github.go31
-rw-r--r--releaser/github_test.go6
-rw-r--r--releaser/releasenotes_writer.go37
-rw-r--r--releaser/releasenotes_writer_test.go2
-rw-r--r--releaser/releaser.go126
11 files changed, 194 insertions, 83 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 000000000..b1897072c
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,49 @@
+defaults: &defaults
+ working_directory: /go/src/github.com/gohugoio
+ docker:
+ - image: bepsays/ci-goreleaser:0.30.5-2
+
+version: 2
+jobs:
+ build:
+ <<: *defaults
+ steps:
+ - checkout:
+ path: hugo
+ - run:
+ command: |
+ git clone git@github.com:gohugoio/hugoDocs.git
+ cd hugo
+ make vendor
+ make check
+ - persist_to_workspace:
+ root: .
+ paths: .
+ release:
+ <<: *defaults
+ steps:
+ - attach_workspace:
+ at: /go/src/github.com/gohugoio
+ - run:
+ command: |
+ cd hugo
+ git config --global user.email "bjorn.erik.pedersen+hugoreleaser@gmail.com"
+ git config --global user.name "hugoreleaser"
+ go run -tags release main.go release -r ${CIRCLE_BRANCH}
+
+workflows:
+ version: 2
+ release:
+ jobs:
+ - build:
+ filters:
+ branches:
+ only: /release-.*/
+ - hold:
+ type: approval
+ requires:
+ - build
+ - release:
+ context: org-global
+ requires:
+ - hold
diff --git a/commands/release.go b/commands/release.go
index 0764685f0..8ccf8bcc2 100644
--- a/commands/release.go
+++ b/commands/release.go
@@ -33,8 +33,6 @@ type releaseCommandeer struct {
skipPublish bool
try bool
-
- step int
}
func createReleaser() *releaseCommandeer {
@@ -53,7 +51,6 @@ func createReleaser() *releaseCommandeer {
}
r.cmd.PersistentFlags().StringVarP(&r.version, "rel", "r", "", "new release version, i.e. 0.25.1")
- 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")
r.cmd.PersistentFlags().BoolVarP(&r.try, "try", "", false, "simulate a release, i.e. no changes")
@@ -64,5 +61,5 @@ func (r *releaseCommandeer) release() error {
if r.version == "" {
return errors.New("must set the --rel flag to the relevant version number")
}
- return releaser.New(r.version, r.step, r.skipPublish, r.try).Run()
+ return releaser.New(r.version, r.skipPublish, r.try).Run()
}
diff --git a/helpers/hugo.go b/helpers/hugo.go
index da0000937..f5b7f6431 100644
--- a/helpers/hugo.go
+++ b/helpers/hugo.go
@@ -41,6 +41,10 @@ func (v HugoVersion) String() string {
// ParseHugoVersion parses a version string.
func ParseHugoVersion(s string) (HugoVersion, error) {
var vv HugoVersion
+ if strings.HasSuffix(s, "-test") {
+ vv.Suffix = "-test"
+ s = strings.TrimSuffix(s, "-test")
+ }
if strings.Contains(s, "DEV") {
return vv, errors.New("DEV versions not supported by parse")
diff --git a/helpers/hugo_test.go b/helpers/hugo_test.go
index a59d8ee16..1f5e5193f 100644
--- a/helpers/hugo_test.go
+++ b/helpers/hugo_test.go
@@ -53,7 +53,7 @@ func TestCompareVersions(t *testing.T) {
func TestParseHugoVersion(t *testing.T) {
require.Equal(t, "0.25", MustParseHugoVersion("0.25").String())
require.Equal(t, "0.25.2", MustParseHugoVersion("0.25.2").String())
-
+ require.Equal(t, "0.25-test", MustParseHugoVersion("0.25-test").String())
_, err := ParseHugoVersion("0.25-DEV")
require.Error(t, err)
}
diff --git a/releaser/git.go b/releaser/git.go
index cfef434dd..8d8bbd68d 100644
--- a/releaser/git.go
+++ b/releaser/git.go
@@ -156,8 +156,8 @@ func git(args ...string) (string, error) {
return string(out), nil
}
-func getGitInfos(tag, repoPath string, remote bool) (gitInfos, error) {
- return getGitInfosBefore("HEAD", tag, repoPath, remote)
+func getGitInfos(tag, repo, repoPath string, remote bool) (gitInfos, error) {
+ return getGitInfosBefore("HEAD", tag, repo, repoPath, remote)
}
type countribCount struct {
@@ -213,8 +213,8 @@ func (g gitInfos) ContribCountPerAuthor() contribCounts {
return c
}
-func getGitInfosBefore(ref, tag, repoPath string, remote bool) (gitInfos, error) {
-
+func getGitInfosBefore(ref, tag, repo, repoPath string, remote bool) (gitInfos, error) {
+ client := newGitHubAPI(repo)
var g gitInfos
log, err := gitLogBefore(ref, tag, repoPath)
@@ -234,7 +234,7 @@ func getGitInfosBefore(ref, tag, repoPath string, remote bool) (gitInfos, error)
Body: items[3],
}
if remote {
- gc, err := fetchCommit(gi.Hash)
+ gc, err := client.fetchCommit(gi.Hash)
if err == nil {
gi.GitHubCommit = &gc
}
diff --git a/releaser/git_test.go b/releaser/git_test.go
index 8053f7702..f0d6fd24b 100644
--- a/releaser/git_test.go
+++ b/releaser/git_test.go
@@ -14,7 +14,6 @@
package releaser
import (
- "os"
"testing"
"github.com/stretchr/testify/require"
@@ -22,7 +21,7 @@ import (
func TestGitInfos(t *testing.T) {
skipIfCI(t)
- infos, err := getGitInfos("v0.20", "", false)
+ infos, err := getGitInfos("v0.20", "hugo", "", false)
require.NoError(t, err)
require.True(t, len(infos) > 0)
@@ -68,7 +67,7 @@ func TestTagExists(t *testing.T) {
}
func skipIfCI(t *testing.T) {
- if os.Getenv("CI") != "" {
+ if isCI() {
// Travis has an ancient git with no --invert-grep: https://github.com/travis-ci/travis-ci/issues/6328
// Also Travis clones very shallowly, making some of the tests above shaky.
t.Skip("Skip git test on Linux to make Travis happy.")
diff --git a/releaser/github.go b/releaser/github.go
index c1e7691b8..11f617007 100644
--- a/releaser/github.go
+++ b/releaser/github.go
@@ -6,14 +6,29 @@ import (
"io/ioutil"
"net/http"
"os"
+ "strings"
)
var (
- gitHubCommitsApi = "https://api.github.com/repos/gohugoio/hugo/commits/%s"
- gitHubRepoApi = "https://api.github.com/repos/gohugoio/hugo"
- gitHubContributorsApi = "https://api.github.com/repos/gohugoio/hugo/contributors"
+ gitHubCommitsAPI = "https://api.github.com/repos/gohugoio/REPO/commits/%s"
+ gitHubRepoAPI = "https://api.github.com/repos/gohugoio/REPO"
+ gitHubContributorsAPI = "https://api.github.com/repos/gohugoio/REPO/contributors"
)
+type gitHubAPI struct {
+ commitsAPITemplate string
+ repoAPI string
+ contributorsAPITemplate string
+}
+
+func newGitHubAPI(repo string) *gitHubAPI {
+ return &gitHubAPI{
+ commitsAPITemplate: strings.Replace(gitHubCommitsAPI, "REPO", repo, -1),
+ repoAPI: strings.Replace(gitHubRepoAPI, "REPO", repo, -1),
+ contributorsAPITemplate: strings.Replace(gitHubContributorsAPI, "REPO", repo, -1),
+ }
+}
+
type gitHubCommit struct {
Author gitHubAuthor `json:"author"`
HtmlURL string `json:"html_url"`
@@ -42,10 +57,10 @@ type gitHubContributor struct {
Contributions int `json:"contributions"`
}
-func fetchCommit(ref string) (gitHubCommit, error) {
+func (g *gitHubAPI) fetchCommit(ref string) (gitHubCommit, error) {
var commit gitHubCommit
- u := fmt.Sprintf(gitHubCommitsApi, ref)
+ u := fmt.Sprintf(g.commitsAPITemplate, ref)
req, err := http.NewRequest("GET", u, nil)
if err != nil {
@@ -57,10 +72,10 @@ func fetchCommit(ref string) (gitHubCommit, error) {
return commit, err
}
-func fetchRepo() (gitHubRepo, error) {
+func (g *gitHubAPI) fetchRepo() (gitHubRepo, error) {
var repo gitHubRepo
- req, err := http.NewRequest("GET", gitHubRepoApi, nil)
+ req, err := http.NewRequest("GET", g.repoAPI, nil)
if err != nil {
return repo, err
}
@@ -75,7 +90,7 @@ func fetchRepo() (gitHubRepo, error) {
for {
page++
var currPage []gitHubContributor
- url := fmt.Sprintf(gitHubContributorsApi+"?page=%d", page)
+ url := fmt.Sprintf(g.contributorsAPITemplate+"?page=%d", page)
req, err = http.NewRequest("GET", url, nil)
if err != nil {
diff --git a/releaser/github_test.go b/releaser/github_test.go
index 7feae75f5..1187cbb2c 100644
--- a/releaser/github_test.go
+++ b/releaser/github_test.go
@@ -23,14 +23,16 @@ import (
func TestGitHubLookupCommit(t *testing.T) {
skipIfNoToken(t)
- commit, err := fetchCommit("793554108763c0984f1a1b1a6ee5744b560d78d0")
+ client := newGitHubAPI("hugo")
+ commit, err := client.fetchCommit("793554108763c0984f1a1b1a6ee5744b560d78d0")
require.NoError(t, err)
fmt.Println(commit)
}
func TestFetchRepo(t *testing.T) {
skipIfNoToken(t)
- repo, err := fetchRepo()
+ client := newGitHubAPI("hugo")
+ repo, err := client.fetchRepo()
require.NoError(t, err)
fmt.Println(">>", len(repo.Contributors))
}
diff --git a/releaser/releasenotes_writer.go b/releaser/releasenotes_writer.go
index 0c6f297a4..e94ed25e1 100644
--- a/releaser/releasenotes_writer.go
+++ b/releaser/releasenotes_writer.go
@@ -139,9 +139,10 @@ var templateFuncs = template.FuncMap{
}
func writeReleaseNotes(version string, infosMain, infosDocs gitInfos, to io.Writer) error {
+ client := newGitHubAPI("hugo")
changes := gitInfosToChangeLog(infosMain, infosDocs)
changes.Version = version
- repo, err := fetchRepo()
+ repo, err := client.fetchRepo()
if err == nil {
changes.Repo = &repo
}
@@ -190,17 +191,43 @@ func writeReleaseNotesToTmpFile(version string, infosMain, infosDocs gitInfos) (
return f.Name(), nil
}
-func getReleaseNotesDocsTempDirAndName(version string) (string, string) {
+func getReleaseNotesDocsTempDirAndName(version string, final bool) (string, string) {
+ if final {
+ return hugoFilepath("temp"), fmt.Sprintf("%s-relnotes-ready.md", version)
+ }
return hugoFilepath("temp"), fmt.Sprintf("%s-relnotes.md", version)
}
-func getReleaseNotesDocsTempFilename(version string) string {
- return filepath.Join(getReleaseNotesDocsTempDirAndName(version))
+func getReleaseNotesDocsTempFilename(version string, final bool) string {
+ return filepath.Join(getReleaseNotesDocsTempDirAndName(version, final))
+}
+
+func (r *ReleaseHandler) releaseNotesState(version string) (releaseNotesState, error) {
+ docsTempPath, name := getReleaseNotesDocsTempDirAndName(version, false)
+ _, err := os.Stat(filepath.Join(docsTempPath, name))
+
+ if err == nil {
+ return releaseNotesCreated, nil
+ }
+
+ docsTempPath, name = getReleaseNotesDocsTempDirAndName(version, true)
+ _, err = os.Stat(filepath.Join(docsTempPath, name))
+
+ if err == nil {
+ return releaseNotesReady, nil
+ }
+
+ if !os.IsNotExist(err) {
+ return releaseNotesNone, err
+ }
+
+ return releaseNotesNone, nil
+
}
func (r *ReleaseHandler) writeReleaseNotesToTemp(version string, infosMain, infosDocs gitInfos) (string, error) {
- docsTempPath, name := getReleaseNotesDocsTempDirAndName(version)
+ docsTempPath, name := getReleaseNotesDocsTempDirAndName(version, false)
var (
w io.WriteCloser
diff --git a/releaser/releasenotes_writer_test.go b/releaser/releasenotes_writer_test.go
index f3e984d55..f5b7a87d3 100644
--- a/releaser/releasenotes_writer_test.go
+++ b/releaser/releasenotes_writer_test.go
@@ -34,7 +34,7 @@ func _TestReleaseNotesWriter(t *testing.T) {
var b bytes.Buffer
// TODO(bep) consider to query GitHub directly for the gitlog with author info, probably faster.
- infos, err := getGitInfosBefore("HEAD", "v0.20", "", false)
+ infos, err := getGitInfosBefore("HEAD", "v0.20", "hugo", "", false)
require.NoError(t, err)
require.NoError(t, writeReleaseNotes("0.21", infos, infos, &b))
diff --git a/releaser/releaser.go b/releaser/releaser.go
index d05683033..1271cf179 100644
--- a/releaser/releaser.go
+++ b/releaser/releaser.go
@@ -31,15 +31,18 @@ import (
const commitPrefix = "releaser:"
+type releaseNotesState int
+
+const (
+ releaseNotesNone = iota
+ releaseNotesCreated
+ releaseNotesReady
+)
+
// ReleaseHandler provides functionality to release a new version of Hugo.
type ReleaseHandler struct {
cliVersion string
- // If set, we do the releases in 3 steps:
- // 1: Create and write a draft release note
- // 2: Prepare files for new version
- // 3: Release
- step int
skipPublish bool
// Just simulate, no actual changes.
@@ -48,29 +51,14 @@ type ReleaseHandler struct {
git func(args ...string) (string, error)
}
-func (r ReleaseHandler) shouldRelease() bool {
- return r.step < 1 || r.shouldContinue()
-}
-
-func (r ReleaseHandler) shouldContinue() bool {
- return r.step >= 3
-}
-
-func (r ReleaseHandler) shouldPrepareReleasenotes() bool {
- return r.step < 1 || r.step == 1
-}
-
-func (r ReleaseHandler) shouldPrepareVersions() bool {
- return r.step < 1 || r.step == 2 || r.step > 3
-}
-
func (r ReleaseHandler) calculateVersions() (helpers.HugoVersion, helpers.HugoVersion) {
-
newVersion := helpers.MustParseHugoVersion(r.cliVersion)
finalVersion := newVersion
finalVersion.PatchLevel = 0
- newVersion.Suffix = ""
+ if newVersion.Suffix != "-test" {
+ newVersion.Suffix = ""
+ }
if newVersion.PatchLevel == 0 {
finalVersion = finalVersion.Next()
@@ -82,8 +70,11 @@ func (r ReleaseHandler) calculateVersions() (helpers.HugoVersion, helpers.HugoVe
}
// New initialises a ReleaseHandler.
-func New(version string, step int, skipPublish, try bool) *ReleaseHandler {
- rh := &ReleaseHandler{cliVersion: version, step: step, skipPublish: skipPublish, try: try}
+func New(version string, skipPublish, try bool) *ReleaseHandler {
+ // When triggered from CI release branch
+ version = strings.TrimPrefix(version, "release-")
+ version = strings.TrimPrefix(version, "v")
+ rh := &ReleaseHandler{cliVersion: version, skipPublish: skipPublish, try: try}
if try {
rh.git = func(args ...string) (string, error) {
@@ -133,20 +124,38 @@ func (r *ReleaseHandler) Run() error {
var (
gitCommits gitInfos
gitCommitsDocs gitInfos
+ relNotesState releaseNotesState
)
- if r.shouldPrepareReleasenotes() || r.shouldRelease() {
- gitCommits, err = getGitInfos(changeLogFromTag, "", !r.try)
+ relNotesState, err = r.releaseNotesState(version)
+ if err != nil {
+ return err
+ }
+
+ prepareRelaseNotes := relNotesState == releaseNotesNone
+ shouldRelease := relNotesState == releaseNotesReady
+
+ defer r.gitPush() // TODO(bep)
+
+ if prepareRelaseNotes || shouldRelease {
+ gitCommits, err = getGitInfos(changeLogFromTag, "hugo", "", !r.try)
if err != nil {
return err
}
- gitCommitsDocs, err = getGitInfos(changeLogFromTag, "../hugoDocs", !r.try)
+
+ // TODO(bep) explicit tag?
+ gitCommitsDocs, err = getGitInfos("", "hugoDocs", "../hugoDocs", !r.try)
if err != nil {
return err
}
}
- if r.shouldPrepareReleasenotes() {
+ if relNotesState == releaseNotesCreated {
+ fmt.Println("Release notes created, but not ready. Reneame to *-ready.md to continue ...")
+ return nil
+ }
+
+ if prepareRelaseNotes {
releaseNotesFile, err := r.writeReleaseNotesToTemp(version, gitCommits, gitCommitsDocs)
if err != nil {
return err
@@ -155,33 +164,30 @@ func (r *ReleaseHandler) Run() error {
if _, err := r.git("add", releaseNotesFile); err != nil {
return err
}
- if _, err := r.git("commit", "-m", fmt.Sprintf("%s Add release notes draft for %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil {
+ if _, err := r.git("commit", "-m", fmt.Sprintf("%s Add release notes draft for %s\n\nRename to *-ready.md to continue. [ci skip]", commitPrefix, newVersion)); err != nil {
return err
}
}
- if r.shouldPrepareVersions() {
-
- // For docs, for now we assume that:
- // The /docs subtree is up to date and ready to go.
- // The hugoDocs/dev and hugoDocs/master must be merged manually after release.
- // TODO(bep) improve this when we see how it works.
+ if !shouldRelease {
+ fmt.Printf("Skip release ... ")
+ return nil
+ }
- if err := r.bumpVersions(newVersion); err != nil {
- return err
- }
+ // For docs, for now we assume that:
+ // The /docs subtree is up to date and ready to go.
+ // The hugoDocs/dev and hugoDocs/master must be merged manually after release.
+ // TODO(bep) improve this when we see how it works.
- if _, err := r.git("commit", "-a", "-m", fmt.Sprintf("%s Bump versions for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil {
- return err
- }
+ if err := r.bumpVersions(newVersion); err != nil {
+ return err
}
- if !r.shouldRelease() {
- fmt.Printf("Skip release ... Use --state=%d for next or --state=4 to finish\n", r.step+1)
- return nil
+ if _, err := r.git("commit", "-a", "-m", fmt.Sprintf("%s Bump versions for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil {
+ return err
}
- releaseNotesFile := getReleaseNotesDocsTempFilename(version)
+ releaseNotesFile := getReleaseNotesDocsTempFilename(version, true)
// Write the release notes to the docs site as well.
docFile, err := r.writeReleaseNotesToDocs(version, releaseNotesFile)
@@ -196,12 +202,14 @@ func (r *ReleaseHandler) Run() error {
return err
}
- if _, err := r.git("tag", "-a", tag, "-m", fmt.Sprintf("%s %s [ci deploy]", commitPrefix, newVersion)); err != nil {
+ if _, err := r.git("tag", "-a", tag, "-m", fmt.Sprintf("%s %s [ci skip]", commitPrefix, newVersion)); err != nil {
return err
}
- if _, err := r.git("push", "origin", tag); err != nil {
- return err
+ if !r.skipPublish {
+ if _, err := r.git("push", "origin", tag); err != nil {
+ return err
+ }
}
if err := r.release(releaseNotesFile); err != nil {
@@ -226,6 +234,15 @@ func (r *ReleaseHandler) Run() error {
return nil
}
+func (r *ReleaseHandler) gitPush() {
+ if r.skipPublish {
+ return
+ }
+ if _, err := r.git("push", "origin", "HEAD"); err != nil {
+ log.Fatal("push failed:", err)
+ }
+}
+
func (r *ReleaseHandler) release(releaseNotesFile string) error {
if r.try {
fmt.Println("Skip goreleaser...")
@@ -243,19 +260,16 @@ func (r *ReleaseHandler) release(releaseNotesFile string) error {
}
func (r *ReleaseHandler) bumpVersions(ver helpers.HugoVersion) error {
- fromDev := ""
toDev := ""
if ver.Suffix != "" {
- toDev = "-DEV"
- } else {
- fromDev = "-DEV"
+ toDev = ver.Suffix
}
if err := r.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 {
+ `Suffix:(\s{4,})".*",`, fmt.Sprintf(`Suffix:${1}"%s",`, toDev)); err != nil {
return err
}
@@ -325,3 +339,7 @@ func hugoFilepath(filename string) string {
}
return filepath.Join(pwd, filename)
}
+
+func isCI() bool {
+ return os.Getenv("CI") != ""
+}