diff options
Diffstat (limited to 'releaser')
-rw-r--r-- | releaser/git.go | 253 | ||||
-rw-r--r-- | releaser/git_test.go | 86 | ||||
-rw-r--r-- | releaser/github.go | 143 | ||||
-rw-r--r-- | releaser/github_test.go | 46 | ||||
-rw-r--r-- | releaser/releasenotes_writer.go | 191 | ||||
-rw-r--r-- | releaser/releasenotes_writer_test.go | 46 | ||||
-rw-r--r-- | releaser/releaser.go | 254 |
7 files changed, 94 insertions, 925 deletions
diff --git a/releaser/git.go b/releaser/git.go deleted file mode 100644 index ced363a9d..000000000 --- a/releaser/git.go +++ /dev/null @@ -1,253 +0,0 @@ -// 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" - "regexp" - "sort" - "strconv" - "strings" - - "github.com/gohugoio/hugo/common/hexec" -) - -var issueRe = regexp.MustCompile(`(?i)(?:Updates?|Closes?|Fix.*|See) #(\d+)`) - -type changeLog struct { - Version string - Notes gitInfos - All gitInfos - Docs gitInfos - - // Overall stats - Repo *gitHubRepo - ContributorCount int - ThemeCount int -} - -func newChangeLog(infos, docInfos gitInfos) *changeLog { - log := &changeLog{ - Docs: docInfos, - } - - for _, info := range infos { - // TODO(bep) improve - if regexp.MustCompile("(?i)deprecate|note").MatchString(info.Subject) { - log.Notes = append(log.Notes, info) - } - - log.All = append(log.All, info) - info.Subject = strings.TrimSpace(info.Subject) - - } - - 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, _ := hexec.SafeCommand("git", args...) - out, err := cmd.CombinedOutput() - if err != nil { - return "", fmt.Errorf("git failed: %q: %q (%q)", err, out, args) - } - return string(out), nil -} - -func getGitInfos(tag, repo, repoPath string, remote bool) (gitInfos, error) { - return getGitInfosBefore("HEAD", tag, repo, repoPath, 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, tag, repo, repoPath string, remote bool) (gitInfos, error) { - client := newGitHubAPI(repo) - var g gitInfos - - log, err := gitLogBefore(ref, tag, repoPath) - 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{} - - if len(items) > 0 { - gi.Hash = items[0] - } - if len(items) > 1 { - gi.Author = items[1] - } - if len(items) > 2 { - gi.Subject = items[2] - } - if len(items) > 3 { - gi.Body = items[3] - } - - if remote && gi.Hash != "" { - gc, err := client.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 = "snapcraft:|Merge commit|Squashed" - -func gitLogBefore(ref, tag, repoPath string) (string, error) { - var prevTag string - var err error - if tag != "" { - prevTag = tag - } else { - prevTag, err = gitVersionTagBefore(ref) - if err != nil { - return "", err - } - } - - defaultArgs := []string{"log", "-E", fmt.Sprintf("--grep=%s", ignoredCommits), "--invert-grep", "--pretty=format:%x1e%h%x1f%aE%x1f%s%x1f%b", "--abbrev-commit", prevTag + ".." + ref} - - var args []string - - if repoPath != "" { - args = append([]string{"-C", repoPath}, defaultArgs...) - } else { - args = defaultArgs - } - - log, err := git(args...) - if err != nil { - return ",", err - } - - return log, err -} - -func gitVersionTagBefore(ref string) (string, error) { - return gitShort("describe", "--tags", "--abbrev=0", "--always", "--match", "v[0-9]*", ref+"^") -} - -func gitShort(args ...string) (output string, err error) { - output, err = git(args...) - return strings.Replace(strings.Split(output, "\n")[0], "'", "", -1), err -} - -func tagExists(tag string) (bool, error) { - out, err := git("tag", "-l", tag) - if err != nil { - return false, err - } - - if strings.Contains(out, tag) { - return true, nil - } - - return false, nil -} diff --git a/releaser/git_test.go b/releaser/git_test.go deleted file mode 100644 index ff77eb8c6..000000000 --- a/releaser/git_test.go +++ /dev/null @@ -1,86 +0,0 @@ -// 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" - - qt "github.com/frankban/quicktest" -) - -func TestGitInfos(t *testing.T) { - c := qt.New(t) - skipIfCI(t) - infos, err := getGitInfos("v0.20", "hugo", "", false) - - c.Assert(err, qt.IsNil) - c.Assert(len(infos) > 0, qt.Equals, true) -} - -func TestIssuesRe(t *testing.T) { - c := qt.New(t) - - body := ` -This is a commit message. - -Updates #123 -Fix #345 -closes #543 -See #456 - ` - - issues := extractIssues(body) - - c.Assert(len(issues), qt.Equals, 4) - c.Assert(issues[0], qt.Equals, 123) - c.Assert(issues[2], qt.Equals, 543) - - bodyNoIssues := ` -This is a commit message without issue refs. - -But it has e #10 to make old regexp confused. -Streets #20. - ` - - emptyIssuesList := extractIssues(bodyNoIssues) - c.Assert(len(emptyIssuesList), qt.Equals, 0) -} - -func TestGitVersionTagBefore(t *testing.T) { - skipIfCI(t) - c := qt.New(t) - v1, err := gitVersionTagBefore("v0.18") - c.Assert(err, qt.IsNil) - c.Assert(v1, qt.Equals, "v0.17") -} - -func TestTagExists(t *testing.T) { - skipIfCI(t) - c := qt.New(t) - b1, err := tagExists("v0.18") - c.Assert(err, qt.IsNil) - c.Assert(b1, qt.Equals, true) - - b2, err := tagExists("adfagdsfg") - c.Assert(err, qt.IsNil) - c.Assert(b2, qt.Equals, false) -} - -func skipIfCI(t *testing.T) { - 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 deleted file mode 100644 index ffb880423..000000000 --- a/releaser/github.go +++ /dev/null @@ -1,143 +0,0 @@ -package releaser - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "os" - "strings" -) - -var ( - 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"` -} - -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 (g *gitHubAPI) fetchCommit(ref string) (gitHubCommit, error) { - var commit gitHubCommit - - u := fmt.Sprintf(g.commitsAPITemplate, ref) - - req, err := http.NewRequest("GET", u, nil) - if err != nil { - return commit, err - } - - err = doGitHubRequest(req, &commit) - - return commit, err -} - -func (g *gitHubAPI) fetchRepo() (gitHubRepo, error) { - var repo gitHubRepo - - req, err := http.NewRequest("GET", g.repoAPI, 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(g.contributorsAPITemplate+"?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 any) 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) - } -} diff --git a/releaser/github_test.go b/releaser/github_test.go deleted file mode 100644 index 23331bf38..000000000 --- a/releaser/github_test.go +++ /dev/null @@ -1,46 +0,0 @@ -// 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" - - qt "github.com/frankban/quicktest" -) - -func TestGitHubLookupCommit(t *testing.T) { - skipIfNoToken(t) - c := qt.New(t) - client := newGitHubAPI("hugo") - commit, err := client.fetchCommit("793554108763c0984f1a1b1a6ee5744b560d78d0") - c.Assert(err, qt.IsNil) - fmt.Println(commit) -} - -func TestFetchRepo(t *testing.T) { - skipIfNoToken(t) - c := qt.New(t) - client := newGitHubAPI("hugo") - repo, err := client.fetchRepo() - c.Assert(err, qt.IsNil) - 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.") - } -} diff --git a/releaser/releasenotes_writer.go b/releaser/releasenotes_writer.go deleted file mode 100644 index 5c50e4de4..000000000 --- a/releaser/releasenotes_writer.go +++ /dev/null @@ -1,191 +0,0 @@ -// 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 ( - "bytes" - "fmt" - "io" - "io/ioutil" - "net/http" - "os" - "path/filepath" - "strings" - "text/template" -) - -const ( - issueLinkTemplate = "#%d" - linkTemplate = "[%s](%s)" - releaseNotesMarkdownTemplatePatchRelease = ` -{{ if eq (len .All) 1 }} -This is a bug-fix release with one important fix. -{{ else }} -This is a bug-fix release with a couple of important fixes. -{{ end }} -{{ range .All }} -{{- if .GitHubCommit -}} -* {{ .Subject }} {{ .Hash }} {{ . | author }} {{ range .Issues }}{{ . | issue }} {{ end }} -{{ else -}} -* {{ .Subject }} {{ range .Issues }}{{ . | issue }} {{ end }} -{{ end -}} -{{- end }} - - -` - releaseNotesMarkdownTemplate = ` -{{- $contribsPerAuthor := .All.ContribCountPerAuthor -}} -{{- $docsContribsPerAuthor := .Docs.ContribCountPerAuthor -}} - -This release represents **{{ len .All }} contributions by {{ len $contribsPerAuthor }} contributors** to the main Hugo code base. - -{{- 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 thanks to [@digitalcraftsman](https://github.com/digitalcraftsman) for his ongoing work on keeping the themes site in pristine condition. -{{ end }} -Many have also been busy writing and fixing the documentation in [hugoDocs](https://github.com/gohugoio/hugoDocs), -which has received **{{ len .Docs }} contributions by {{ len $docsContribsPerAuthor }} contributors**. -{{- if gt (len $docsContribsPerAuthor) 3 -}} -{{- $u1 := index $docsContribsPerAuthor 0 -}} -{{- $u2 := index $docsContribsPerAuthor 1 -}} -{{- $u3 := index $docsContribsPerAuthor 2 -}} -{{- $u4 := index $docsContribsPerAuthor 3 }} A special thanks to {{ $u1.AuthorLink }}, {{ $u2.AuthorLink }}, {{ $u3.AuthorLink }}, and {{ $u4.AuthorLink }} for their work on the documentation site. -{{ end }} - -Hugo now has: - -{{ with .Repo -}} -* {{ .Stars }}+ [stars](https://github.com/gohugoio/hugo/stargazers) -* {{ len .Contributors }}+ [contributors](https://github.com/gohugoio/hugo/graphs/contributors) -{{- end -}} -{{ with .ThemeCount }} -* {{ . }}+ [themes](http://themes.gohugo.io/) -{{ end }} -{{ with .Notes }} -## Notes -{{ template "change-section" . }} -{{- end -}} -{{ with .All }} -## Changes -{{ template "change-section" . }} -{{ end }} - -{{ define "change-section" }} -{{ range . }} -{{- if .GitHubCommit -}} -* {{ .Subject }} {{ .Hash }} {{ . | author }} {{ range .Issues }}{{ . | issue }} {{ end }} -{{ else -}} -* {{ .Subject }} {{ range .Issues }}{{ . | issue }} {{ end }} -{{ end -}} -{{- end }} -{{ end }} -` -) - -var templateFuncs = template.FuncMap{ - "isPatch": func(c changeLog) bool { - return !strings.HasSuffix(c.Version, "0") - }, - "issue": func(id int) string { - return fmt.Sprintf(issueLinkTemplate, id) - }, - "commitURL": func(info gitInfo) string { - if info.GitHubCommit.HTMLURL == "" { - return "" - } - return fmt.Sprintf(linkTemplate, info.Hash, info.GitHubCommit.HTMLURL) - }, - "author": func(info gitInfo) string { - return "@" + info.GitHubCommit.Author.Login - }, -} - -func writeReleaseNotes(version string, infosMain, infosDocs gitInfos, to io.Writer) error { - client := newGitHubAPI("hugo") - changes := newChangeLog(infosMain, infosDocs) - changes.Version = version - repo, err := client.fetchRepo() - if err == nil { - changes.Repo = &repo - } - themeCount, err := fetchThemeCount() - if err == nil { - changes.ThemeCount = themeCount - } - - mtempl := releaseNotesMarkdownTemplate - - if !strings.HasSuffix(version, "0") { - mtempl = releaseNotesMarkdownTemplatePatchRelease - } - - tmpl, err := template.New("").Funcs(templateFuncs).Parse(mtempl) - 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://raw.githubusercontent.com/gohugoio/hugoThemesSiteBuilder/main/themes.txt") - if err != nil { - return 0, err - } - defer resp.Body.Close() - - b, _ := ioutil.ReadAll(resp.Body) - return bytes.Count(b, []byte("\n")) - bytes.Count(b, []byte("#")), nil -} - -func getReleaseNotesFilename(version string) string { - return filepath.FromSlash(fmt.Sprintf("temp/%s-relnotes-ready.md", version)) -} - -func (r *ReleaseHandler) writeReleaseNotesToTemp(version string, isPatch bool, infosMain, infosDocs gitInfos) (string, error) { - filename := getReleaseNotesFilename(version) - - var w io.WriteCloser - - if !r.try { - f, err := os.Create(filename) - if err != nil { - return "", err - } - - defer f.Close() - - w = f - - } else { - w = os.Stdout - } - - if err := writeReleaseNotes(version, infosMain, infosDocs, w); err != nil { - return "", err - } - - return filename, nil -} diff --git a/releaser/releasenotes_writer_test.go b/releaser/releasenotes_writer_test.go deleted file mode 100644 index 7dcd0ccaa..000000000 --- a/releaser/releasenotes_writer_test.go +++ /dev/null @@ -1,46 +0,0 @@ -// 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" - "os" - "testing" - - qt "github.com/frankban/quicktest" -) - -func _TestReleaseNotesWriter(t *testing.T) { - skipIfNoToken(t) - if os.Getenv("CI") != "" { - // Travis has an ancient git with no --invert-grep: https://github.com/travis-ci/travis-ci/issues/6328 - t.Skip("Skip git test on CI to make Travis happy..") - } - - c := qt.New(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.89.0", "hugo", "", false) - c.Assert(err, qt.IsNil) - - c.Assert(writeReleaseNotes("0.89.0", infos, infos, &b), qt.IsNil) - - fmt.Println(b.String()) -} diff --git a/releaser/releaser.go b/releaser/releaser.go index ebc344e98..fc16a2572 100644 --- a/releaser/releaser.go +++ b/releaser/releaser.go @@ -17,7 +17,6 @@ package releaser import ( "fmt" - "io/ioutil" "log" "os" "path/filepath" @@ -25,167 +24,105 @@ import ( "strings" "github.com/gohugoio/hugo/common/hexec" - - "errors" - "github.com/gohugoio/hugo/common/hugo" ) const commitPrefix = "releaser:" -// ReleaseHandler provides functionality to release a new version of Hugo. -// Test this locally without doing an actual release: -// go run -tags release main.go release --skip-publish --try -r 0.90.0 -// Or a variation of the above -- the skip-publish flag makes sure that any changes are performed to the local Git only. -type ReleaseHandler struct { - cliVersion string - - skipPublish bool - - // Just simulate, no actual changes. - try bool - - git func(args ...string) (string, error) -} - -func (r ReleaseHandler) calculateVersions() (hugo.Version, hugo.Version) { - newVersion := hugo.MustParseVersion(r.cliVersion) - finalVersion := newVersion.Next() - finalVersion.PatchLevel = 0 +// New initialises a ReleaseHandler. +func New(skipPush, try bool, step int) (*ReleaseHandler, error) { + if step < 1 || step > 2 { + return nil, fmt.Errorf("step must be 1 or 2") + } - if newVersion.Suffix != "-test" { - newVersion.Suffix = "" + prefix := "release-" + branch, err := git("rev-parse", "--abbrev-ref", "HEAD") + if err != nil { + return nil, err + } + if !strings.HasPrefix(branch, prefix) { + return nil, fmt.Errorf("branch %q is not a release branch", branch) } - finalVersion.Suffix = "-DEV" + logf("Branch: %s\n", branch) - return newVersion, finalVersion -} - -// New initialises a ReleaseHandler. -func New(version string, skipPublish, try bool) *ReleaseHandler { - // When triggered from CI release branch - version = strings.TrimPrefix(version, "release-") + version := strings.TrimPrefix(branch, prefix) version = strings.TrimPrefix(version, "v") - rh := &ReleaseHandler{cliVersion: version, skipPublish: skipPublish, try: try} + rh := &ReleaseHandler{branchVersion: version, skipPush: skipPush, try: try, step: step} if try { rh.git = func(args ...string) (string, error) { - fmt.Println("git", strings.Join(args, " ")) + logln("git", strings.Join(args, " ")) return "", nil } } else { rh.git = git } - return rh + return rh, nil } -// Run creates a new release. -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") - } +// ReleaseHandler provides functionality to release a new version of Hugo. +// Test this locally without doing an actual release: +// go run -tags release main.go release --skip-publish --try -r 0.90.0 +// Or a variation of the above -- the skip-publish flag makes sure that any changes are performed to the local Git only. +type ReleaseHandler struct { + branchVersion string - fmt.Printf("Start release from %q\n", wd()) + // 1 or 2. + step int - newVersion, finalVersion := r.calculateVersions() + // No remote pushes. + skipPush bool + + // Just simulate, no actual changes. + try bool + + git func(args ...string) (string, error) +} +// Run creates a new release. +func (r *ReleaseHandler) Run() error { + newVersion, finalVersion := r.calculateVersions() version := newVersion.String() tag := "v" + version - isPatch := newVersion.PatchLevel > 0 mainVersion := newVersion mainVersion.PatchLevel = 0 - // Exit early if tag already exists - exists, err := tagExists(tag) - if err != nil { - return err - } - - if exists { - return fmt.Errorf("tag %q already exists", tag) - } - - var changeLogFromTag string + defer r.gitPush() - if newVersion.PatchLevel == 0 { - // There may have been patch releases between, so set the tag explicitly. - changeLogFromTag = "v" + newVersion.Prev().String() - exists, _ := tagExists(changeLogFromTag) - if !exists { - // fall back to one that exists. - changeLogFromTag = "" + if r.step == 1 { + if err := r.bumpVersions(newVersion); err != nil { + return err } - } - - var ( - gitCommits gitInfos - gitCommitsDocs gitInfos - ) - defer r.gitPush() // TODO(bep) - - gitCommits, err = getGitInfos(changeLogFromTag, "hugo", "", !r.try) - if err != nil { - return err - } - - // TODO(bep) explicit tag? - gitCommitsDocs, err = getGitInfos("", "hugoDocs", "../hugoDocs", !r.try) - if err != nil { - return err - } - - releaseNotesFile, err := r.writeReleaseNotesToTemp(version, isPatch, gitCommits, gitCommitsDocs) - if err != nil { - return err - } - - if _, err := r.git("add", releaseNotesFile); err != nil { - return err - } - - commitMsg := fmt.Sprintf("%s Add release notes for %s", commitPrefix, newVersion) - commitMsg += "\n[ci skip]" - - if _, err := r.git("commit", "-m", commitMsg); err != nil { - return err - } - - 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 _, 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.git("tag", "-a", tag, "-m", fmt.Sprintf("%s %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil { - return err - } + // The above commit will be the target for this release, so print it to the console in a env friendly way. + sha, err := git("rev-parse", "HEAD") + if err != nil { + return err + } - if !r.skipPublish { - if _, err := r.git("push", "origin", tag); err != nil { + // Hugoreleaser will do the actual release using these values. + if err := r.replaceInFile("hugoreleaser.env", + `HUGORELEASER_TAG=(\S*)`, "HUGORELEASER_TAG="+tag, + `HUGORELEASER_COMMITISH=(\S*)`, "HUGORELEASER_COMMITISH="+sha, + ); err != nil { return err } - } + logf("HUGORELEASER_TAG=%s\n", tag) + logf("HUGORELEASER_COMMITISH=%s\n", sha) - if err := r.release(releaseNotesFile); err != nil { - return err + return nil } if err := r.bumpVersions(finalVersion); err != nil { return err } - if !r.try { - // No longer needed. - if err := os.Remove(releaseNotesFile); err != nil { - return err - } - } - if _, err := r.git("commit", "-a", "-m", fmt.Sprintf("%s Prepare repository for %s\n\n[ci skip]", commitPrefix, finalVersion)); err != nil { return err } @@ -193,36 +130,6 @@ 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...") - return nil - } - - args := []string{"--parallelism", "2", "--timeout", "120m", "--rm-dist", "--release-notes", releaseNotesFile} - if r.skipPublish { - args = append(args, "--skip-publish") - } - - cmd, _ := hexec.SafeCommand("goreleaser", args...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err := cmd.Run() - if err != nil { - return fmt.Errorf("goreleaser failed: %w", err) - } - return nil -} - func (r *ReleaseHandler) bumpVersions(ver hugo.Version) error { toDev := "" @@ -264,6 +171,29 @@ func (r *ReleaseHandler) bumpVersions(ver hugo.Version) error { return nil } +func (r ReleaseHandler) calculateVersions() (hugo.Version, hugo.Version) { + newVersion := hugo.MustParseVersion(r.branchVersion) + finalVersion := newVersion.Next() + finalVersion.PatchLevel = 0 + + if newVersion.Suffix != "-test" { + newVersion.Suffix = "" + } + + finalVersion.Suffix = "-DEV" + + return newVersion, finalVersion +} + +func (r *ReleaseHandler) gitPush() { + if r.skipPush { + return + } + if _, err := r.git("push", "origin", "HEAD"); err != nil { + log.Fatal("push failed:", err) + } +} + func (r *ReleaseHandler) replaceInFile(filename string, oldNew ...string) error { filename = filepath.FromSlash(filename) fi, err := os.Stat(filename) @@ -272,11 +202,11 @@ func (r *ReleaseHandler) replaceInFile(filename string, oldNew ...string) error } if r.try { - fmt.Printf("Replace in %q: %q\n", filename, oldNew) + logf("Replace in %q: %q\n", filename, oldNew) return nil } - b, err := ioutil.ReadFile(filename) + b, err := os.ReadFile(filename) if err != nil { return err } @@ -287,18 +217,22 @@ func (r *ReleaseHandler) replaceInFile(filename string, oldNew ...string) error newContent = re.ReplaceAllString(newContent, oldNew[i+1]) } - return ioutil.WriteFile(filename, []byte(newContent), fi.Mode()) -} - -func isCI() bool { - return os.Getenv("CI") != "" + return os.WriteFile(filename, []byte(newContent), fi.Mode()) } -func wd() string { - p, err := os.Getwd() +func git(args ...string) (string, error) { + cmd, _ := hexec.SafeCommand("git", args...) + out, err := cmd.CombinedOutput() if err != nil { - log.Fatal(err) + return "", fmt.Errorf("git failed: %q: %q (%q)", err, out, args) } - return p + return string(out), nil +} + +func logf(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, format, args...) +} +func logln(args ...interface{}) { + fmt.Fprintln(os.Stderr, args...) } |