diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2017-04-13 16:59:05 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2017-04-24 11:08:56 +0200 |
commit | 7f6430d84d68765536b8758b1331a4b84db04c84 (patch) | |
tree | 7c2b1450c2d2789fbbc0323fc6027dac586ef6df | |
parent | 0e87b18b66d2c8ba9e2abc429630cb03f5b093d6 (diff) |
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
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | commands/genman.go | 2 | ||||
-rw-r--r-- | commands/hugo.go | 2 | ||||
-rw-r--r-- | commands/release.go | 62 | ||||
-rw-r--r-- | commands/version.go | 4 | ||||
-rw-r--r-- | docs/config.toml | 5 | ||||
-rw-r--r-- | docs/content/release-notes/_index.md | 8 | ||||
-rw-r--r-- | docs/content/release-notes/release-notes.md (renamed from docs/content/meta/release-notes.md) | 8 | ||||
-rw-r--r-- | docs/layouts/section/release-notes.html | 6 | ||||
-rw-r--r-- | goreleaser.yml | 48 | ||||
-rw-r--r-- | helpers/general.go | 2 | ||||
-rw-r--r-- | helpers/hugo.go | 65 | ||||
-rw-r--r-- | helpers/hugo_test.go | 10 | ||||
-rw-r--r-- | hugolib/hugo_info.go | 4 | ||||
-rw-r--r-- | releaser/git.go | 265 | ||||
-rw-r--r-- | releaser/git_test.go | 53 | ||||
-rw-r--r-- | releaser/github.go | 129 | ||||
-rw-r--r-- | releaser/github_test.go | 42 | ||||
-rw-r--r-- | releaser/releasenotes_writer.go | 245 | ||||
-rw-r--r-- | releaser/releasenotes_writer_test.go | 44 | ||||
-rw-r--r-- | releaser/releaser.go | 267 | ||||
-rw-r--r-- | releaser/releaser_test.go | 78 | ||||
-rw-r--r-- | transform/hugogeneratorinject.go | 2 |
23 files changed, 1308 insertions, 46 deletions
diff --git a/.gitignore b/.gitignore index de359fc21..47721b7cb 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ vendor/*/ *.bench coverage*.out -GoBuilds
\ No newline at end of file +GoBuilds +dist diff --git a/commands/genman.go b/commands/genman.go index 7b26afeed..cd5c8c84e 100644 --- a/commands/genman.go +++ b/commands/genman.go @@ -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 diff --git a/commands/hugo.go b/commands/hugo.go index 73dde5d2d..4260f6e56 100644 --- a/commands/hugo.go +++ b/commands/hugo.go @@ -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 diff --git a/commands/release.go b/commands/release.go new file mode 100644 index 000000000..f6d98e29f --- /dev/null +++ b/commands/release.go @@ -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() +} diff --git a/commands/version.go b/commands/version.go index f026c13a5..9e673b817 100644 --- a/commands/version.go +++ b/commands/version.go @@ -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) } } diff --git a/docs/config.toml b/docs/config.toml index 25288342b..8d91a66bd 100644 --- a/docs/config.toml +++ b/docs/config.toml @@ -46,6 +46,11 @@ pluralizeListTitles = false 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" pre = "<i class='fa fa-road'></i>" diff --git a/docs/content/release-notes/_index.md b/docs/content/release-notes/_index.md new file mode 100644 index 000000000..3b934c69d --- /dev/null +++ b/docs/content/release-notes/_index.md @@ -0,0 +1,8 @@ +--- +date: 2017-04-17 +aliases: +- /doc/release-notes/ +- /meta/release-notes/ +title: Release Notes +weight: 10 +--- diff --git a/docs/content/meta/release-notes.md b/docs/content/release-notes/release-notes.md index af82d908a..7f962e318 100644 --- a/docs/content/meta/release-notes.md +++ b/docs/content/release-notes/release-notes.md @@ -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 diff --git a/docs/layouts/section/release-notes.html b/docs/layouts/section/release-notes.html new file mode 100644 index 000000000..6af512603 --- /dev/null +++ b/docs/layouts/section/release-notes.html @@ -0,0 +1,6 @@ +{{ define "main" }} +{{ range .Pages }} +<h1>{{ .Title }} {{ .Date.Format "Jan 2, 2006" }}</h1> +{{ .Content }} +{{ end }} +{{ end }}
\ No newline at end of file diff --git a/goreleaser.yml b/goreleaser.yml new file mode 100644 index 000000000..4b7fef63a --- /dev/null +++ b/goreleaser.yml @@ -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 diff --git a/helpers/general.go b/helpers/general.go index ac2af4936..ea3620119 100644 --- a/helpers/general.go +++ b/helpers/general.go @@ -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) diff --git a/helpers/hugo.go b/helpers/hugo.go index 2fda7cdc4..05e47306c 100644 --- a/helpers/hugo.go +++ b/helpers/hugo.go @@ -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. + Suffix string +} + +func (v HugoVersion) String() string { + return hugoVersion(v.Number, v.PatchLevel, v.Suffix) +} + +// ReleaseVersion represents the release version. +func (v HugoVersion) ReleaseVersion() HugoVersion { + v.Suffix = "" + return v +} -// 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 +// Next returns the next Hugo release version. +func (v HugoVersion) Next() HugoVersion { + return HugoVersion{Number: v.Number + 0.01} +} -// 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) +// Pre returns the previous Hugo release version. +func (v HugoVersion) Prev() HugoVersion { + return HugoVersion{Number: v.Number - 0.01} } -// HugoReleaseVersion is same as HugoVersion, but no suffix. -func HugoReleaseVersion() string { - return hugoVersionNoSuffix(HugoVersionNumber, HugoPatchVersion) +// 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} } -// NextHugoReleaseVersion returns the next Hugo release version. -func NextHugoReleaseVersion() string { - return hugoVersionNoSuffix(HugoVersionNumber+0.01, 0) +// 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 { diff --git a/helpers/hugo_test.go b/helpers/hugo_test.go index b71517f71..c96a1351b 100644 --- a/helpers/hugo_test.go +++ b/helpers/hugo_test.go @@ -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) { diff --git a/hugolib/hugo_info.go b/hugolib/hugo_info.go index 77bf54802..86842f38b 100644 --- a/hugolib/hugo_info.go +++ b/hugolib/hugo_info.go @@ -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())), } } diff --git a/releaser/git.go b/releaser/git.go new file mode 100644 index 000000000..d8b5bef31 --- /dev/null +++ b/releaser/git.go @@ -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 +} diff --git a/releaser/git_test.go b/releaser/git_test.go new file mode 100644 index 000000000..dc1db5dc7 --- /dev/null +++ b/releaser/git_test.go @@ -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]) + +} diff --git a/releaser/github.go b/releaser/github.go new file mode 100644 index 000000000..0dbb1bca1 --- /dev/null +++ b/releaser/github.go @@ -0,0 +1,129 @@ +package releaser + +import ( + "encoding/json" < |