diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..b1897072 --- /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 0764685f..8ccf8bcc 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 da000093..f5b7f643 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 a59d8ee1..1f5e5193 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 cfef434d..8d8bbd68 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 8053f770..f0d6fd24 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 c1e7691b..11f61700 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 7feae75f..1187cbb2 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 0c6f297a..e94ed25e 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 f3e984d5..f5b7a87d 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 d0568303..1271cf17 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 err := r.bumpVersions(newVersion); err != nil { - return err - } - - 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 !r.shouldRelease() { - fmt.Printf("Skip release ... Use --state=%d for next or --state=4 to finish\n", r.step+1) + if !shouldRelease { + fmt.Printf("Skip release ... ") return nil } - releaseNotesFile := getReleaseNotesDocsTempFilename(version) + // 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.bumpVersions(newVersion); err != nil { + return err + } + + 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, 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") != "" +}