Repository: goreleaser/goreleaser
Pull request #1212 (2019-10-21 22:51:57)

feat: Added new flags to support release notes header and footer.
Created two flags for release generation.
--release-footer
--release-header
These flags can help you to add custom changelog text before/after changes that are generated by git log.

This commit is based on this issue:
https://github.com/goreleaser/goreleaser/issues/1131


codecov-io: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/1212?src=pr&el=h1) Report
> Merging [#1212](https://codecov.io/gh/goreleaser/goreleaser/pull/1212?src=pr&el=desc) into [master](https://codecov.io/gh/goreleaser/goreleaser/commit/449ed95bbe73fb88159e061352fcc786987d7d7d?src=pr&el=desc) will **decrease** coverage by `0.06%`.
> The diff coverage is `57.14%`.

[![Impacted file tree graph](https://codecov.io/gh/goreleaser/goreleaser/pull/1212/graphs/tree.svg?width=650&token=3bJvv6b7hN&height=150&src=pr)](https://codecov.io/gh/goreleaser/goreleaser/pull/1212?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1212      +/-   ##
==========================================
- Coverage   83.65%   83.58%   -0.07%     
==========================================
  Files          58       58              
  Lines        3285     3308      +23     
==========================================
+ Hits         2748     2765      +17     
- Misses        456      461       +5     
- Partials       81       82       +1
```


| [Impacted Files](https://codecov.io/gh/goreleaser/goreleaser/pull/1212?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/context/context.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1212/diff?src=pr&el=tree#diff-cGtnL2NvbnRleHQvY29udGV4dC5nbw==) | `100% <ø> (ø)` | :arrow_up: |
| [main.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1212/diff?src=pr&el=tree#diff-bWFpbi5nbw==) | `48.06% <0%> (-1.54%)` | :arrow_down: |
| [internal/pipe/changelog/changelog.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1212/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcGlwZS9jaGFuZ2Vsb2cvY2hhbmdlbG9nLmdv) | `96.58% <90.9%> (-1.38%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/1212?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/1212?src=pr&el=footer). Last update [449ed95...6d3f7c2](https://codecov.io/gh/goreleaser/goreleaser/pull/1212?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

caarlos0: I'll take a look at some point, sorry for the delay so far

Patch:
diff --git a/internal/pipe/changelog/changelog.go b/internal/pipe/changelog/changelog.go
index 17827e14..e78f6394 100644
--- a/internal/pipe/changelog/changelog.go
+++ b/internal/pipe/changelog/changelog.go
@@ -35,6 +35,8 @@ func (Pipe) Run(ctx *context.Context) error {
 		if err != nil {
 			return err
 		}
+		log.WithField("file", ctx.ReleaseNotes).Info("loaded custom release notes")
+		log.WithField("file", ctx.ReleaseNotes).Debugf("custom release notes: \n%s", notes)
 		ctx.ReleaseNotes = notes
 	}
 	if ctx.Config.Changelog.Skip {
@@ -46,9 +48,25 @@ func (Pipe) Run(ctx *context.Context) error {
 	if ctx.ReleaseNotes != "" {
 		return nil
 	}
+	if ctx.ReleaseHeader != "" {
+		header, err := loadFromFile(ctx.ReleaseHeader)
+		if err != nil {
+			return err
+		}
+		ctx.ReleaseHeader = header
+	}
+	if ctx.ReleaseFooter != "" {
+		footer, err := loadFromFile(ctx.ReleaseFooter)
+		if err != nil {
+			return err
+		}
+		ctx.ReleaseFooter = footer
+	}
+
 	if err := checkSortDirection(ctx.Config.Changelog.Sort); err != nil {
 		return err
 	}
+
 	entries, err := buildChangelog(ctx)
 	if err != nil {
 		return err
@@ -61,7 +79,17 @@ func (Pipe) Run(ctx *context.Context) error {
 		log.Debug("is gitlab or gitea changelog")
 		changelogStringJoiner = "   \n"
 	}
-	ctx.ReleaseNotes = fmt.Sprintf("## Changelog\n\n%v\n", strings.Join(entries, changelogStringJoiner))
+
+	ctx.ReleaseNotes = strings.Join(
+		[]string{
+			ctx.ReleaseHeader,
+			"## Changelog",
+			strings.Join(entries, changelogStringJoiner),
+			ctx.ReleaseFooter,
+		},
+		"\n\n",
+	)
+
 	var path = filepath.Join(ctx.Config.Dist, "CHANGELOG.md")
 	log.WithField("changelog", path).Info("writing")
 	return ioutil.WriteFile(path, []byte(ctx.ReleaseNotes), 0644)
@@ -72,8 +100,6 @@ func loadFromFile(file string) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	log.WithField("file", file).Info("loaded custom release notes")
-	log.WithField("file", file).Debugf("custom release notes: \n%s", string(bts))
 	return string(bts), nil
 }
 
diff --git a/internal/pipe/nfpm/nfpm.go b/internal/pipe/nfpm/nfpm.go
index b1f10a74..34592aa5 100644
--- a/internal/pipe/nfpm/nfpm.go
+++ b/internal/pipe/nfpm/nfpm.go
@@ -104,33 +104,33 @@ func doRun(ctx *context.Context, fpm config.NFPM) error {
 }
 
 func mergeOverrides(fpm config.NFPM, format string) (*config.NFPMOverridables, error) {
-	var overrided config.NFPMOverridables
-	if err := mergo.Merge(&overrided, fpm.NFPMOverridables); err != nil {
+	var overridden config.NFPMOverridables
+	if err := mergo.Merge(&overridden, fpm.NFPMOverridables); err != nil {
 		return nil, err
 	}
 	perFormat, ok := fpm.Overrides[format]
 	if ok {
-		err := mergo.Merge(&overrided, perFormat, mergo.WithOverride)
+		err := mergo.Merge(&overridden, perFormat, mergo.WithOverride)
 		if err != nil {
 			return nil, err
 		}
 	}
-	return &overrided, nil
+	return &overridden, nil
 }
 
 func create(ctx *context.Context, fpm config.NFPM, format, arch string, binaries []*artifact.Artifact) error {
-	overrided, err := mergeOverrides(fpm, format)
+	overridden, err := mergeOverrides(fpm, format)
 	if err != nil {
 		return err
 	}
 	name, err := tmpl.New(ctx).
-		WithArtifact(binaries[0], overrided.Replacements).
-		Apply(overrided.NameTemplate)
+		WithArtifact(binaries[0], overridden.Replacements).
+		Apply(overridden.NameTemplate)
 	if err != nil {
 		return err
 	}
 	var files = map[string]string{}
-	for k, v := range overrided.Files {
+	for k, v := range overridden.Files {
 		files[k] = v
 	}
 	var log = log.WithField("package", name+"."+format).WithField("arch", arch)
@@ -157,18 +157,18 @@ func create(ctx *context.Context, fpm config.NFPM, format, arch string, binaries
 		License:     fpm.License,
 		Bindir:      fpm.Bindir,
 		Overridables: nfpm.Overridables{
-			Conflicts:    overrided.Conflicts,
-			Depends:      overrided.Dependencies,
-			Recommends:   overrided.Recommends,
-			Suggests:     overrided.Suggests,
-			EmptyFolders: overrided.EmptyFolders,
+			Conflicts:    overridden.Conflicts,
+			Depends:      overridden.Dependencies,
+			Recommends:   overridden.Recommends,
+			Suggests:     overridden.Suggests,
+			EmptyFolders: overridden.EmptyFolders,
 			Files:        files,
-			ConfigFiles:  overrided.ConfigFiles,
+			ConfigFiles:  overridden.ConfigFiles,
 			Scripts: nfpm.Scripts{
-				PreInstall:  overrided.Scripts.PreInstall,
-				PostInstall: overrided.Scripts.PostInstall,
-				PreRemove:   overrided.Scripts.PreRemove,
-				PostRemove:  overrided.Scripts.PostRemove,
+				PreInstall:  overridden.Scripts.PreInstall,
+				PostInstall: overridden.Scripts.PostInstall,
+				PreRemove:   overridden.Scripts.PreRemove,
+				PostRemove:  overridden.Scripts.PostRemove,
 			},
 		},
 	}
diff --git a/main.go b/main.go
index 5e3546b0..e37e7dc2 100644
--- a/main.go
+++ b/main.go
@@ -16,7 +16,7 @@ import (
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
 	"github.com/goreleaser/goreleaser/pkg/defaults"
-	kingpin "gopkg.in/alecthomas/kingpin.v2"
+	"gopkg.in/alecthomas/kingpin.v2"
 )
 
 // nolint: gochecknoglobals
@@ -28,15 +28,17 @@ var (
 )
 
 type releaseOptions struct {
-	Config       string
-	ReleaseNotes string
-	Snapshot     bool
-	SkipPublish  bool
-	SkipSign     bool
-	SkipValidate bool
-	RmDist       bool
-	Parallelism  int
-	Timeout      time.Duration
+	Config        string
+	ReleaseNotes  string
+	ReleaseHeader string
+	ReleaseFooter string
+	Snapshot      bool
+	SkipPublish   bool
+	SkipSign      bool
+	SkipValidate  bool
+	RmDist        bool
+	Parallelism   int
+	Timeout       time.Duration
 }
 
 func main() {
@@ -56,6 +58,8 @@ func main() {
 	var checkCmd = app.Command("check", "Checks if configuration is valid").Alias("c")
 	var releaseCmd = app.Command("release", "Releases the current project").Alias("r").Default()
 	var releaseNotes = releaseCmd.Flag("release-notes", "Load custom release notes from a markdown file").PlaceHolder("notes.md").String()
+	var releaseHeader = releaseCmd.Flag("release-header", "Load custom release notes header from a markdown file").PlaceHolder("notes-header.md").String()
+	var releaseFooter = releaseCmd.Flag("release-footer", "Load custom release notes footer from a markdown file").PlaceHolder("notes-footer.md").String()
 	var snapshot = releaseCmd.Flag("snapshot", "Generate an unversioned snapshot release, skipping all validations and without publishing any artifacts").Bool()
 	var skipPublish = releaseCmd.Flag("skip-publish", "Skips publishing artifacts").Bool()
 	var skipSign = releaseCmd.Flag("skip-sign", "Skips signing the artifacts").Bool()
@@ -96,15 +100,17 @@ func main() {
 		start := time.Now()
 		log.Infof(color.New(color.Bold).Sprintf("releasing using goreleaser %s...", version))
 		var options = releaseOptions{
-			Config:       *config,
-			ReleaseNotes: *releaseNotes,
-			Snapshot:     *snapshot,
-			SkipPublish:  *skipPublish,
-			SkipValidate: *skipValidate,
-			SkipSign:     *skipSign,
-			RmDist:       *rmDist,
-			Parallelism:  *parallelism,
-			Timeout:      *timeout,
+			Config:        *config,
+			ReleaseNotes:  *releaseNotes,
+			ReleaseHeader: *releaseHeader,
+			ReleaseFooter: *releaseFooter,
+			Snapshot:      *snapshot,
+			SkipPublish:   *skipPublish,
+			SkipValidate:  *skipValidate,
+			SkipSign:      *skipSign,
+			RmDist:        *rmDist,
+			Parallelism:   *parallelism,
+			Timeout:       *timeout,
 		}
 		if err := releaseProject(options); err != nil {
 			log.WithError(err).Errorf(color.New(color.Bold).Sprintf("release failed after %0.2fs", time.Since(start).Seconds()))
@@ -141,6 +147,8 @@ func releaseProject(options releaseOptions) error {
 	ctx.Parallelism = options.Parallelism
 	log.Debugf("parallelism: %v", ctx.Parallelism)
 	ctx.ReleaseNotes = options.ReleaseNotes
+	ctx.ReleaseHeader = options.ReleaseHeader
+	ctx.ReleaseFooter = options.ReleaseFooter
 	ctx.Snapshot = options.Snapshot
 	ctx.SkipPublish = ctx.Snapshot || options.SkipPublish
 	ctx.SkipValidate = ctx.Snapshot || options.SkipValidate
diff --git a/pkg/context/context.go b/pkg/context/context.go
index be9d8118..e09bf33d 100644
--- a/pkg/context/context.go
+++ b/pkg/context/context.go
@@ -53,22 +53,24 @@ const (
 // Context carries along some data through the pipes
 type Context struct {
 	ctx.Context
-	Config       config.Project
-	Env          Env
-	Token        string
-	TokenType    TokenType
-	Git          GitInfo
-	Artifacts    artifact.Artifacts
-	ReleaseNotes string
-	Version      string
-	Snapshot     bool
-	SkipPublish  bool
-	SkipSign     bool
-	SkipValidate bool
-	RmDist       bool
-	PreRelease   bool
-	Parallelism  int
-	Semver       Semver
+	Config        config.Project
+	Env           Env
+	Token         string
+	TokenType     TokenType
+	Git           GitInfo
+	Artifacts     artifact.Artifacts
+	ReleaseNotes  string
+	ReleaseHeader string
+	ReleaseFooter string
+	Version       string
+	Snapshot      bool
+	SkipPublish   bool
+	SkipSign      bool
+	SkipValidate  bool
+	RmDist        bool
+	PreRelease    bool
+	Parallelism   int
+	Semver        Semver
 }
 
 // Semver represents a semantic version


Test patch:
diff --git a/internal/pipe/changelog/changelog_test.go b/internal/pipe/changelog/changelog_test.go
index 5e3f2065..bd43133a 100644
--- a/internal/pipe/changelog/changelog_test.go
+++ b/internal/pipe/changelog/changelog_test.go
@@ -2,6 +2,7 @@ package changelog
 
 import (
 	"io/ioutil"
+	"os"
 	"path/filepath"
 	"testing"
 
@@ -46,6 +47,18 @@ func TestChangelogSkip(t *testing.T) {
 	testlib.AssertSkipped(t, Pipe{}.Run(ctx))
 }
 
+func TestReleaseHeaderProvidedViaFlagDoesntExist(t *testing.T) {
+	var ctx = context.New(config.Project{})
+	ctx.ReleaseHeader = "testdata/header.nope"
+	require.EqualError(t, Pipe{}.Run(ctx), "open testdata/header.nope: no such file or directory")
+}
+
+func TestReleaseFooterProvidedViaFlagDoesntExist(t *testing.T) {
+	var ctx = context.New(config.Project{})
+	ctx.ReleaseFooter = "testdata/footer.nope"
+	require.EqualError(t, Pipe{}.Run(ctx), "open testdata/footer.nope: no such file or directory")
+}
+
 func TestSnapshot(t *testing.T) {
 	var ctx = context.New(config.Project{})
 	ctx.Snapshot = true
@@ -285,3 +298,55 @@ func TestChangelogOnBranchWithSameNameAsTag(t *testing.T) {
 		require.Contains(t, ctx.ReleaseNotes, msg)
 	}
 }
+
+func TestChangeLogWithReleaseHeader(t *testing.T) {
+	current, err := os.Getwd()
+	require.NoError(t, err)
+	tmpdir, back := testlib.Mktmp(t)
+	defer back()
+	require.NoError(t, os.Symlink(current+"/testdata", tmpdir+"/testdata"))
+	testlib.GitInit(t)
+	var msgs = []string{
+		"initial commit",
+		"another one",
+		"one more",
+		"and finally this one",
+	}
+	for _, msg := range msgs {
+		testlib.GitCommit(t, msg)
+	}
+	testlib.GitTag(t, "v0.0.1")
+	testlib.GitCheckoutBranch(t, "v0.0.1")
+	var ctx = context.New(config.Project{})
+	ctx.Git.CurrentTag = "v0.0.1"
+	ctx.ReleaseHeader = "testdata/release-header.md"
+	require.NoError(t, Pipe{}.Run(ctx))
+	require.Contains(t, ctx.ReleaseNotes, "## Changelog")
+	require.Contains(t, ctx.ReleaseNotes, "test header")
+}
+
+func TestChangeLogWithReleaseFooter(t *testing.T) {
+	current, err := os.Getwd()
+	require.NoError(t, err)
+	tmpdir, back := testlib.Mktmp(t)
+	defer back()
+	require.NoError(t, os.Symlink(current+"/testdata", tmpdir+"/testdata"))
+	testlib.GitInit(t)
+	var msgs = []string{
+		"initial commit",
+		"another one",
+		"one more",
+		"and finally this one",
+	}
+	for _, msg := range msgs {
+		testlib.GitCommit(t, msg)
+	}
+	testlib.GitTag(t, "v0.0.1")
+	testlib.GitCheckoutBranch(t, "v0.0.1")
+	var ctx = context.New(config.Project{})
+	ctx.Git.CurrentTag = "v0.0.1"
+	ctx.ReleaseFooter = "testdata/release-footer.md"
+	require.NoError(t, Pipe{}.Run(ctx))
+	require.Contains(t, ctx.ReleaseNotes, "## Changelog")
+	require.Contains(t, ctx.ReleaseNotes, "test footer")
+}
diff --git a/internal/pipe/changelog/testdata/release-footer.md b/internal/pipe/changelog/testdata/release-footer.md
new file mode 100644
index 00000000..15b9a75f
--- /dev/null
+++ b/internal/pipe/changelog/testdata/release-footer.md
@@ -0,0 +1,1 @@
+test footer
diff --git a/internal/pipe/changelog/testdata/release-header.md b/internal/pipe/changelog/testdata/release-header.md
new file mode 100644
index 00000000..865d7487
--- /dev/null
+++ b/internal/pipe/changelog/testdata/release-header.md
@@ -0,0 +1,1 @@
+test header
diff --git a/main_test.go b/main_test.go
index e041a852..bc7618d7 100644
--- a/main_test.go
+++ b/main_test.go
@@ -11,7 +11,7 @@ import (
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-	yaml "gopkg.in/yaml.v2"
+	"gopkg.in/yaml.v2"
 )
 
 func init() {
@@ -107,6 +107,50 @@ func TestCustomReleaseNotesFile(t *testing.T) {
 	assert.NoError(t, releaseProject(params))
 }
 
+func TestCustomReleaseHeaderFileDontExist(t *testing.T) {
+	_, back := setup(t)
+	defer back()
+	params := testParams()
+	params.ReleaseHeader = "/header/that/dont/exist"
+	params.Snapshot = false
+	assert.Error(t, releaseProject(params))
+}
+
+func TestCustomReleaseHeaderFile(t *testing.T) {
+	_, back := setup(t)
+	defer back()
+	releaseHeader, err := ioutil.TempFile("", "")
+	assert.NoError(t, err)
+	createFile(t, releaseHeader.Name(), "some release header")
+	params := testParams()
+	params.ReleaseHeader = releaseHeader.Name()
+	params.Snapshot = false
+	params.SkipPublish = true
+	assert.NoError(t, releaseProject(params))
+}
+
+func TestCustomReleaseFooterFileDontExist(t *testing.T) {
+	_, back := setup(t)
+	defer back()
+	params := testParams()
+	params.ReleaseFooter = "/footer/that/dont/exist"
+	params.Snapshot = false
+	assert.Error(t, releaseProject(params))
+}
+
+func TestCustomReleaseFooterFile(t *testing.T) {
+	_, back := setup(t)
+	defer back()
+	releaseFooter, err := ioutil.TempFile("", "")
+	assert.NoError(t, err)
+	createFile(t, releaseFooter.Name(), "some release footer")
+	params := testParams()
+	params.ReleaseFooter = releaseFooter.Name()
+	params.Snapshot = false
+	params.SkipPublish = true
+	assert.NoError(t, releaseProject(params))
+}
+
 func TestBrokenPipe(t *testing.T) {
 	_, back := setup(t)
 	defer back()