Repository: gembaadvantage/uplift (go)

Issue:
[Feature]: include changelog creation within the release workflow
### Describe your feature

Uplift can perform a `release` against a repository that currently bumps files and tags a repository using the latest calculated semantic version. Changelog generation is included as a separate command and should also be incorporated within the release workflow.

### Your potential solution

Wire the changelog task into the existing release workflow

### Any additional information?

_No response_

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct

Pull request description:
feat: enable changelog creation during release
closes #64 

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8528cce..4d6d39b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -61,6 +61,6 @@ jobs:
         if: github.ref == 'refs/heads/main'
         uses: gembaadvantage/uplift-action@v2.0.1
         with:
-          args: release
+          args: tag
         env:
           GITHUB_TOKEN: ${{ secrets.GH_UPLIFT }}
diff --git a/README.md b/README.md
index 8b02226..6ea28f7 100644
--- a/README.md
+++ b/README.md
@@ -181,7 +181,7 @@ uplift changelog --sort asc
 
 ### Release
 
-A full semantic release will be carried out. Combining both the `bump` and `tag` operations, in that order.
+A full semantic release will be carried out. Combining the `bump`, `changelog` and `tag` operations, in that order.
 
 ```sh
 uplift release
diff --git a/cmd/uplift/release.go b/cmd/uplift/release.go
index 9034974..6c744d9 100644
--- a/cmd/uplift/release.go
+++ b/cmd/uplift/release.go
@@ -36,6 +36,7 @@ import (
 	"github.com/gembaadvantage/uplift/internal/semver"
 	"github.com/gembaadvantage/uplift/internal/task"
 	"github.com/gembaadvantage/uplift/internal/task/bump"
+	"github.com/gembaadvantage/uplift/internal/task/changelog"
 	"github.com/gembaadvantage/uplift/internal/task/currentversion"
 	"github.com/gembaadvantage/uplift/internal/task/fetchtag"
 	"github.com/gembaadvantage/uplift/internal/task/gitpush"
@@ -108,6 +109,7 @@ func release(opts releaseOptions, out io.Writer) error {
 		nextversion.Task{},
 		nextcommit.Task{},
 		bump.Task{},
+		changelog.Task{},
 		gitpush.Task{},
 		gittag.Task{},
 	}
@@ -135,6 +137,9 @@ func setupReleaseContext(opts releaseOptions, out io.Writer) (*context.Context,
 	ctx.NoPush = opts.NoPush
 	ctx.Out = out
 
+	// Enable pre-tagging support for generating a changelog
+	ctx.ChangelogPreTag = true
+
 	// Handle prerelease suffix if one is provided
 	if opts.Prerelease != "" {
 		var err error
diff --git a/internal/context/context.go b/internal/context/context.go
index 28341c8..b4cdbdf 100644
--- a/internal/context/context.go
+++ b/internal/context/context.go
@@ -51,6 +51,7 @@ type Context struct {
 	ChangelogAll      bool
 	ChangelogExcludes []string
 	ChangelogSort     string
+	ChangelogPreTag   bool
 }
 
 // New constructs a context that captures both runtime configuration and
diff --git a/internal/git/utils.go b/internal/git/utils.go
index dfcd0e7..59b0482 100644
--- a/internal/git/utils.go
+++ b/internal/git/utils.go
@@ -168,7 +168,7 @@ func LatestCommit() (CommitDetails, error) {
 
 // Tag will create a lightweight tag against the repository and push it to the origin
 func Tag(tag string) error {
-	if _, err := Clean(Run("tag", tag)); err != nil {
+	if _, err := Clean(Run("tag", "-f", tag)); err != nil {
 		return err
 	}
 
@@ -194,6 +194,7 @@ func AnnotatedTag(tag string, cd CommitDetails) error {
 		"tag",
 		"-a",
 		tag,
+		"-f",
 		"-m",
 		cd.Message,
 	}
diff --git a/internal/task/changelog/changelog.go b/internal/task/changelog/changelog.go
index f33b699..cd1c471 100644
--- a/internal/task/changelog/changelog.go
+++ b/internal/task/changelog/changelog.go
@@ -91,6 +91,17 @@ func (t Task) Run(ctx *context.Context) error {
 		return nil
 	}
 
+	// To ensure the changelog is correctly generated during a release, the latest commit will
+	// need to be temporarily tagged. After changelog generation, it will be removed to
+	// ensure the release workflow behaves as expected. This will be a transparent operation
+	// that cannot be invoked by the caller
+	if ctx.ChangelogPreTag {
+		log.WithField("tag", ctx.NextVersion.Raw).Info("pre-tagging latest commit for changelog creation")
+		if err := git.Tag(ctx.NextVersion.Raw); err != nil {
+			return err
+		}
+	}
+
 	// Retrieve log entries based on the changelog expectations
 	var rels []release
 	var relErr error
@@ -192,7 +203,7 @@ func changelogReleases(ctx *context.Context) ([]release, error) {
 			nextTag = tags[i+1].Ref
 		}
 
-		log.WithField("tag", tags[i]).Info("determine changes for release")
+		log.WithField("tag", tags[i].Ref).Info("determine changes for release")
 		ents, err := git.LogBetween(tags[i].Ref, nextTag, ctx.ChangelogExcludes)
 		if err != nil {
 			return []release{}, err


Test patch:
diff --git a/cmd/uplift/release_test.go b/cmd/uplift/release_test.go
index a82fa5f..816ea63 100644
--- a/cmd/uplift/release_test.go
+++ b/cmd/uplift/release_test.go
@@ -44,10 +44,19 @@ func TestRelease(t *testing.T) {
 
 	tags := git.AllTags()
 	assert.Len(t, tags, 1)
+	assert.Equal(t, tags[0].Ref, "0.1.0")
+
+	// Ensure the tag is associated with the correct commit
+	out, err := git.Clean(git.Run("tag", "-l", "0.1.0", `--format='%(subject)'`))
+	require.NoError(t, err)
+	assert.Contains(t, out, "ci(uplift):")
 
 	actual, err := ioutil.ReadFile("test.txt")
 	require.NoError(t, err)
 	assert.NotEqual(t, string(data), string(actual))
+	assert.Contains(t, string(actual), "version: 0.1.0")
+
+	assert.True(t, changelogExists(t))
 }
 
 func TestRelease_CheckFlag(t *testing.T) {
diff --git a/internal/git/utils_test.go b/internal/git/utils_test.go
index 120ed3c..ad6ce14 100644
--- a/internal/git/utils_test.go
+++ b/internal/git/utils_test.go
@@ -26,6 +26,7 @@ import (
 	"fmt"
 	"io/ioutil"
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -89,6 +90,16 @@ func TestLatestTag_NoSemanticTags(t *testing.T) {
 	assert.Equal(t, "", tag.Ref)
 }
 
+func TestDescribeTag(t *testing.T) {
+	InitRepo(t)
+	Run("tag", "1.0.0")
+
+	desc := DescribeTag("1.0.0")
+
+	assert.Equal(t, "1.0.0", desc.Ref)
+	assert.Equal(t, time.Now().Format("2006-01-02"), desc.Created)
+}
+
 func TestLatestCommit(t *testing.T) {
 	InitRepo(t)
 
@@ -115,6 +126,16 @@ func TestLatestCommit_MultipleCommits(t *testing.T) {
 	assert.Equal(t, c.Message, m)
 }
 
+func TestCommitDetails_String(t *testing.T) {
+	cd := CommitDetails{
+		Author:  "uplift",
+		Email:   "uplift@test.com",
+		Message: "this is a test commit",
+	}
+
+	assert.Equal(t, "uplift <uplift@test.com>\nthis is a test commit", cd.String())
+}
+
 func TestTag(t *testing.T) {
 	InitRepo(t)