Repository: gembaadvantage/uplift
Pull request #47 (2021-11-15 04:19:02)

chore(deps): bump gembaadvantage/uplift-action from 1 to 2.0.1
Bumps [gembaadvantage/uplift-action](https://github.com/gembaadvantage/uplift-action) from 1 to 2.0.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/gembaadvantage/uplift-action/releases">gembaadvantage/uplift-action's releases</a>.</em></p>
<blockquote>
<h2>v2.0.1</h2>
<h2>Changelog</h2>
<p>0f01558 ci(bump): bumped version to v2.0.1
ec16343 fix: rebuild index for latest changes</p>
<h2>v2.0.0</h2>
<h2>Changelog</h2>
<p>f313ac4 docs: consistency within readme
fe96e0e docs: update readme to include v2 instructions
d923f01 feat!: action now requires args as input (<a href="https://github-redirect.dependabot.com/gembaadvantage/uplift-action/issues/30">#30</a>)
7833e0c ci: upgrade to v5 of github-script action
3f6d213 ci: remove auto-merging workflow</p>
<h2>v1.2.0</h2>
<h2>Changelog</h2>
<p>40d2d67 feat: support installation of the uplift binary only (<a href="https://github-redirect.dependabot.com/gembaadvantage/uplift-action/issues/21">#21</a>)
d4efde7 chore(deps): bump codecov/codecov-action from 1.5.0 to 1.5.2 (<a href="https://github-redirect.dependabot.com/gembaadvantage/uplift-action/issues/18">#18</a>) …
41328c0 ci: revert back to using v1 tag in workflow
6eebf97 docs: add section on personal access tokens</p>
<h2>v1.1.1</h2>
<h2>Changelog</h2>
<p>fix: update action yaml with defaults (<a href="https://github-redirect.dependabot.com/gembaadvantage/uplift-action/issues/17">#17</a>)</p>
<h2>v1.1.0</h2>
<h2>Changelog</h2>
<p>00b9342 chore(release): release managed by uplift
11e5fd6 feat: support individual arguments within uplift action (<a href="https://github-redirect.dependabot.com/gembaadvantage/uplift-action/issues/15">#15</a>)</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/gembaadvantage/uplift-action/commit/0f015585b669da121873849ace19daa2f49090bc"><code>0f01558</code></a> ci(bump): bumped version to v2.0.1</li>
<li><a href="https://github.com/gembaadvantage/uplift-action/commit/ec1634365295f769b3e18ac62ccb445065165f5c"><code>ec16343</code></a> fix: rebuild index for latest changes</li>
<li><a href="https://github.com/gembaadvantage/uplift-action/commit/a9ef8345a86b4a76f8b42acdba286fa93a65abe1"><code>a9ef834</code></a> ci: switch to v2 of uplift action</li>
<li><a href="https://github.com/gembaadvantage/uplift-action/commit/f313ac445196fda8f8dcde3ad104bbbae221a0cd"><code>f313ac4</code></a> docs: consistency within readme</li>
<li><a href="https://github.com/gembaadvantage/uplift-action/commit/fe96e0e84194dd05a19976af8dd4360bfc9182ff"><code>fe96e0e</code></a> docs: update readme to include v2 instructions</li>
<li><a href="https://github.com/gembaadvantage/uplift-action/commit/8dd659d82a4e1e853d9ffe813ddf6c7ca289cb4a"><code>8dd659d</code></a> ci(bump): bumped version to v2.0.0</li>
<li><a href="https://github.com/gembaadvantage/uplift-action/commit/d923f013e599a1d9e1d97f8226915ecba2570717"><code>d923f01</code></a> feat!: action now requires args as input (<a href="https://github-redirect.dependabot.com/gembaadvantage/uplift-action/issues/30">#30</a>)</li>
<li><a href="https://github.com/gembaadvantage/uplift-action/commit/7833e0c3a95a74ba8f243ca007d1b0c283b805dd"><code>7833e0c</code></a> ci: upgrade to v5 of github-script action</li>
<li><a href="https://github.com/gembaadvantage/uplift-action/commit/3f6d213c1e51c90ed97c7087b28228b75ac8d2e5"><code>3f6d213</code></a> ci: remove auto-merging workflow</li>
<li><a href="https://github.com/gembaadvantage/uplift-action/commit/0e6ecea48aa38c03b0ab246fb2f045e12641d06f"><code>0e6ecea</code></a> docs: create issue templates (<a href="https://github-redirect.dependabot.com/gembaadvantage/uplift-action/issues/22">#22</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/gembaadvantage/uplift-action/compare/v1...v2.0.1">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=gembaadvantage/uplift-action&package-manager=github_actions&previous-version=1&new-version=2.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

codecov-commenter: # [Codecov](https://codecov.io/gh/gembaadvantage/uplift/pull/47?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage) Report
> Merging [#47](https://codecov.io/gh/gembaadvantage/uplift/pull/47?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage) (b3bdbea) into [main](https://codecov.io/gh/gembaadvantage/uplift/commit/e531cb4b446a49c374a78aecffbb3256b51a768b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage) (e531cb4) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/gembaadvantage/uplift/pull/47/graphs/tree.svg?width=650&height=150&src=pr&token=K13FIQH5ZB&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage)](https://codecov.io/gh/gembaadvantage/uplift/pull/47?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage)

```diff
@@           Coverage Diff           @@
##             main      #47   +/-   ##
=======================================
  Coverage   79.64%   79.64%           
=======================================
  Files          12       12           
  Lines         447      447           
=======================================
  Hits          356      356           
  Misses         84       84           
  Partials        7        7           
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/gembaadvantage/uplift/pull/47?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/gembaadvantage/uplift/pull/47?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage). Last update [e531cb4...b3bdbea](https://codecov.io/gh/gembaadvantage/uplift/pull/47?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage).


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4c7155a..51c7f88 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -49,7 +49,7 @@ jobs:
         run: make build
       - name: Tag
         if: github.ref == 'refs/heads/main'
-        uses: gembaadvantage/uplift-action@v1
+        uses: gembaadvantage/uplift-action@v2.0.1
         with:
           version: v0.7.0
         env:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a399f60..8f2d0e1 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -41,6 +41,15 @@ jobs:
         env:
           GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
           PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
+      - name: Snapcraft Install
+        # https://github.com/goreleaser/goreleaser/issues/1715
+        run: |
+          sudo apt-get update
+          sudo apt-get -yq --no-install-suggests --no-install-recommends install snapcraft
+          mkdir -p $HOME/.cache/snapcraft/download
+          mkdir -p $HOME/.cache/snapcraft/stage-packages
+      - name: Snapcraft Login
+        run: snapcraft login --with <(echo "${{ secrets.GH_SNAPCRAFT_LOGIN }}")
       - name: GoReleaser
         uses: goreleaser/goreleaser-action@v2
         with:
diff --git a/.goreleaser.yml b/.goreleaser.yml
index ae80768..c736498 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -97,3 +97,15 @@ scoop:
   homepage: "https://github.com/gembaadvantage/uplift"
   description: "Semantic versioning the easy way"
   license: MIT
+snapcrafts:
+  - name_template: |-
+      {{ .ProjectName }}_{{ .Version }}_{{ .Os }}-{{ .Arch }}
+    summary: "Semantic versioning the easy way"
+    description: |
+      Semantic versioning the easy way. Automatic tagging and version bumping of 
+      files in your repositories based on your commit messages. Powered by 
+      Conventional Commits. Easy to include in your CI.
+    grade: stable
+    confinement: classic
+    publish: true
+    license: MIT
diff --git a/README.md b/README.md
index 28e1b49..4f42d50 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
 [![Go Version](https://img.shields.io/github/go-mod/go-version/gembaadvantage/uplift.svg?style=flat-square)](go.mod)
 [![codecov](https://codecov.io/gh/gembaadvantage/uplift/branch/main/graph/badge.svg)](https://codecov.io/gh/gembaadvantage/uplift)
 
-Semantic versioning the easy way. Automatic tagging and version bumping of files in your respositories based on your commit messages. Powered by [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). Easy to include in your CI.
+Semantic versioning the easy way. Automatic tagging and version bumping of files in your repositories based on your commit messages. Powered by [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). Easy to include in your CI.
 
 :octocat: Github [action](https://github.com/marketplace/actions/uplift-action) available.
 
@@ -62,14 +62,16 @@ Based on the latest commit, the repository will be tagged with the next semantic
 ```sh
 $ uplift tag
 
-  • current version
-    • identified version        current=0.1.2
-  • next version
-    • identified next version   commit=feat: new feature current=0.1.2 next=0.2.0
-  • next commit
-    • committing with           email=joe.bloggs@example.com message=ci(bump): bumped version to 0.2.0 name=joe.bloggs
-  • tag
-    • with standard tag         tag=0.2.0
+  • latest commit
+      • retrieved latest commit   author=joe.bloggs email=joe.bloggs@example.com message=feat: new feature
+   • current version
+      • identified version        current=0.1.2
+   • next version
+      • identified next version   commit=feat: new feature current=0.1.2 next=0.2.0
+   • next commit
+      • changes will be committed with email=joe.bloggs@example.com message=ci(uplift): uplifted for version 0.2.0 name=joe.bloggs
+   • tag
+      • with standard tag         tag=1.0.0
 ```
 
 ### File Bumping
@@ -79,18 +81,39 @@ When configured, the version within any file in a git repository can be bumped t
 ```sh
 $ uplift bump
 
-  • current version
-    • identified version        current=0.2.0
-  • next version
-    • identified next version   commit=feat: new feature current=0.2.0 next=0.3.0
-  • next commit
-    • committing with           email=joe.bloggs@example.com message=ci(bump): bumped version to 0.2.0 name=joe.bloggs
-  • bump
-    • file bumped               current=0.2.0 file=chart/test/Chart.yaml next=0.3.0
-    • successfully staged file  file=chart/test/Chart.yaml
-    • attempting to commit file changes
-  • git push
-    • check and push any outstanding commits
+  • latest commit
+      • retrieved latest commit   author=joe.bloggs email=joe.bloggs@example.com message=feat: new feature
+   • current version
+      • identified version        current=1.0.0
+   • next version
+      • identified next version   commit=feat: new feature current=1.0.0 next=1.1.0
+   • next commit
+      • changes will be committed with email=joe.bloggs@example.com message=ci(uplift): uplifted for version 1.1.0 name=joe.bloggs
+   • bump
+      • file bumped               current=1.0.0 file=chart/test/Chart.yaml next=1.1.0
+      • successfully staged file  file=chart/test/Chart.yaml
+   • git push
+      • commit outstanding changes
+      • push commit to remote
+```
+
+### Changelog
+
+A changelog can be generated for the latest tagged semantic release and written to a `CHANGELOG.md` file. File will be created if one doesn't exist. Format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
+
+```sh
+$ uplift changelog
+
+   • latest commit
+      • retrieved latest commit   author=joe.bloggs email=joe.bloggs@example.com message=fix: a bug fix
+   • next commit
+      • changes will be committed with email=paul.t@gembaadvantage.com message=ci(uplift): uplifted for version 1.0.1 name=paul.t
+   • changelog
+      • determine changes for release tag=1.0.1
+      • changeset identified      commits=3 date=2021-11-19 tag=1.0.1
+   • git push
+      • commit outstanding changes
+      • push commit to remote
 ```
 
 ### Release
@@ -103,7 +126,7 @@ uplift release
 
 ## Configuration
 
-Uplift can be configured through the existance of an optional `.uplift.yml` configuration file in the root of your repository. Other supported variations are: `.uplift.yaml`, `uplift.yml` and `uplift.yaml`.
+Uplift can be configured through the existence of an optional `.uplift.yml` configuration file in the root of your repository. Other supported variations are: `.uplift.yaml`, `uplift.yml` and `uplift.yaml`.
 
 ```yaml
 # An initial version that will be used for the first tag in your repository.
@@ -117,11 +140,11 @@ firstVersion: v1.0.0
 # Defaults to false
 annotatedTags: true
 
-# Changes the commit message when bumping files
-# Defaults to ci(bump): bumped version to $VERSION
+# Changes the  default commit message used when committing any staged changes
+# Defaults to ci(uplift): uplifted for version $VERSION
 commitMessage: "chore: a custom commit message"
 
-# Changes the commit author when bumping files
+# Changes the commit author used when committing any staged changes
 commitAuthor:
   # Name of the author
   # Defaults to the author name within the last commit
diff --git a/cmd/uplift/bump.go b/cmd/uplift/bump.go
index 56edc0b..c5dda80 100644
--- a/cmd/uplift/bump.go
+++ b/cmd/uplift/bump.go
@@ -32,6 +32,7 @@ import (
 	"github.com/gembaadvantage/uplift/internal/task/bump"
 	"github.com/gembaadvantage/uplift/internal/task/currentversion"
 	"github.com/gembaadvantage/uplift/internal/task/gitpush"
+	"github.com/gembaadvantage/uplift/internal/task/lastcommit"
 	"github.com/gembaadvantage/uplift/internal/task/nextcommit"
 	"github.com/gembaadvantage/uplift/internal/task/nextversion"
 	"github.com/spf13/cobra"
@@ -59,6 +60,7 @@ func newBumpCmd(out io.Writer, ctx *context.Context) *cobra.Command {
 
 func bumpFiles(out io.Writer, ctx *context.Context) error {
 	tsks := []task.Runner{
+		lastcommit.Task{},
 		currentversion.Task{},
 		nextversion.Task{},
 		nextcommit.Task{},
diff --git a/cmd/uplift/changelog.go b/cmd/uplift/changelog.go
new file mode 100644
index 0000000..74ff6e8
--- /dev/null
+++ b/cmd/uplift/changelog.go
@@ -0,0 +1,86 @@
+/*
+Copyright (c) 2021 Gemba Advantage
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+package main
+
+import (
+	"io"
+
+	"github.com/gembaadvantage/uplift/internal/context"
+	"github.com/gembaadvantage/uplift/internal/git"
+	"github.com/gembaadvantage/uplift/internal/middleware/logging"
+	"github.com/gembaadvantage/uplift/internal/middleware/skip"
+	"github.com/gembaadvantage/uplift/internal/task"
+	"github.com/gembaadvantage/uplift/internal/task/changelog"
+	"github.com/gembaadvantage/uplift/internal/task/gitpush"
+	"github.com/gembaadvantage/uplift/internal/task/lastcommit"
+	"github.com/gembaadvantage/uplift/internal/task/nextcommit"
+	"github.com/spf13/cobra"
+)
+
+const (
+	chlogDesc = `Create or update an existing changelog with an entry for
+the latest semantic release. For a first release, all commits
+between the latest tag and trunk will be written to the
+changelog. Subsequent entries will contain only commits between 
+release tags`
+)
+
+func newChangelogCmd(out io.Writer, ctx *context.Context) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "changelog",
+		Short: "Create or update a changelog with the latest semantic release",
+		Long:  chlogDesc,
+		Args:  cobra.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			// Attempt to retrieve the latest 2 tags for generating a changelog entry
+			tags := git.AllTags()
+			if len(tags) == 1 {
+				ctx.NextVersion.Raw = tags[0]
+			} else if len(tags) > 1 {
+				ctx.NextVersion.Raw = tags[0]
+				ctx.CurrentVersion.Raw = tags[1]
+			}
+
+			return writeChangelog(out, ctx)
+		},
+	}
+
+	return cmd
+}
+
+func writeChangelog(out io.Writer, ctx *context.Context) error {
+	tsks := []task.Runner{
+		lastcommit.Task{},
+		nextcommit.Task{},
+		changelog.Task{},
+		gitpush.Task{},
+	}
+
+	for _, tsk := range tsks {
+		if err := skip.Running(tsk.Skip, logging.Log(tsk.String(), tsk.Run))(ctx); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/cmd/uplift/release.go b/cmd/uplift/release.go
index 9e28809..7d89dfb 100644
--- a/cmd/uplift/release.go
+++ b/cmd/uplift/release.go
@@ -33,6 +33,7 @@ import (
 	"github.com/gembaadvantage/uplift/internal/task/currentversion"
 	"github.com/gembaadvantage/uplift/internal/task/fetchtag"
 	"github.com/gembaadvantage/uplift/internal/task/gitpush"
+	"github.com/gembaadvantage/uplift/internal/task/lastcommit"
 	"github.com/gembaadvantage/uplift/internal/task/nextcommit"
 	"github.com/gembaadvantage/uplift/internal/task/nextversion"
 	"github.com/gembaadvantage/uplift/internal/task/tag"
@@ -63,6 +64,7 @@ func newReleaseCmd(out io.Writer, ctx *context.Context) *cobra.Command {
 func release(out io.Writer, ctx *context.Context) error {
 	tsks := []task.Runner{
 		fetchtag.Task{},
+		lastcommit.Task{},
 		currentversion.Task{},
 		nextversion.Task{},
 		nextcommit.Task{},
diff --git a/cmd/uplift/root.go b/cmd/uplift/root.go
index 6a1615f..61e46fd 100644
--- a/cmd/uplift/root.go
+++ b/cmd/uplift/root.go
@@ -54,7 +54,8 @@ func newRootCmd(out io.Writer, args []string, ctx *context.Context) (*cobra.Comm
 		newBumpCmd(out, ctx),
 		newCompletionCmd(out),
 		newTagCmd(out, ctx),
-		newReleaseCmd(out, ctx))
+		newReleaseCmd(out, ctx),
+		newChangelogCmd(out, ctx))
 
 	return cmd, nil
 }
diff --git a/cmd/uplift/tag.go b/cmd/uplift/tag.go
index 91cc08e..df20d5e 100644
--- a/cmd/uplift/tag.go
+++ b/cmd/uplift/tag.go
@@ -31,6 +31,7 @@ import (
 	"github.com/gembaadvantage/uplift/internal/task"
 	"github.com/gembaadvantage/uplift/internal/task/currentversion"
 	"github.com/gembaadvantage/uplift/internal/task/fetchtag"
+	"github.com/gembaadvantage/uplift/internal/task/lastcommit"
 	"github.com/gembaadvantage/uplift/internal/task/nextcommit"
 	"github.com/gembaadvantage/uplift/internal/task/nextversion"
 	"github.com/gembaadvantage/uplift/internal/task/tag"
@@ -60,6 +61,7 @@ func newTagCmd(out io.Writer, ctx *context.Context) *cobra.Command {
 func tagRepo(out io.Writer, ctx *context.Context) error {
 	tsks := []task.Runner{
 		fetchtag.Task{},
+		lastcommit.Task{},
 		currentversion.Task{},
 		nextversion.Task{},
 		nextcommit.Task{},
diff --git a/internal/git/utils.go b/internal/git/utils.go
index 02cdc28..03c94db 100644
--- a/internal/git/utils.go
+++ b/internal/git/utils.go
@@ -36,6 +36,13 @@ type CommitDetails struct {
 	Email   string
 }
 
+// LogEntry contains details about a specific git log entry
+type LogEntry struct {
+	Hash       string
+	AbbrevHash string
+	Message    string
+}
+
 // String prints out a user friendly string representation
 func (c CommitDetails) String() string {
 	return fmt.Sprintf("%s <%s>\n%s", c.Author, c.Email, c.Message)
@@ -67,20 +74,30 @@ func FetchTags() error {
 	return nil
 }
 
-// LatestTag retrieves the latest tag within the repository
-func LatestTag() string {
+// AllTags retrieves all tags within the repository from newest to oldest
+func AllTags() []string {
 	// Filter out all tags that are non in the supported formats
 	tags, err := Clean(Run("tag", "-l", "--sort=-v:refname", "v*.*.*", "*.*.*"))
 	if err != nil {
-		return ""
+		return []string{}
 	}
 
-	// If no tags are found, then just return an empty string
+	// If no tags are found, then just return an empty slice
 	if tags == "" {
+		return []string{}
+	}
+
+	return strings.Split(tags, "\n")
+}
+
+// LatestTag retrieves the latest tag within the repository
+func LatestTag() string {
+	tags := AllTags()
+	if len(tags) == 0 {
 		return ""
 	}
 
-	return strings.Split(tags, "\n")[0]
+	return tags[0]
 }
 
 // LatestCommit retrieves the latest commit within the repository
@@ -202,6 +219,59 @@ func Push() error {
 	return nil
 }
 
+// LogBetween retrieves all log entries between two points of time within the
+// git history of the repository. Supports tags and specific git hashes as its
+// reference points. From must always be the closest point to HEAD
+func LogBetween(from, to string) ([]LogEntry, error) {
+	fmtFrom := from
+	if fmtFrom == "" {
+		fmtFrom = "HEAD"
+	}
+
+	fmtTo := to
+	if fmtTo != "" {
+		// A range query requires ... ellipses
+		fmtTo = fmt.Sprintf("...%s", fmtTo)
+	}
+
+	args := []string{
+		"log",
+		fmt.Sprintf("%s%s", fmtFrom, fmtTo),
+		"--pretty=format:'%H%s'",
+	}
+
+	log, err := Clean(Run(args...))
+	if err != nil {
+		return []LogEntry{}, err
+	}
+
+	rows := strings.Split(log, "\n")
+	les := make([]LogEntry, 0, len(rows))
+	for _, r := range rows {
+		les = append(les, LogEntry{
+			Hash:       r[:40],
+			AbbrevHash: r[:7],
+			Message:    r[40:],
+		})
+	}
+
+	return les, nil
+}
+
+// Staged retrieves a list of all files that are currently staged
+func Staged() ([]string, error) {
+	files, err := Clean(Run("diff", "--cached", "--name-only"))
+	if err != nil {
+		return []string{}, err
+	}
+
+	if files == "" {
+		return []string{}, nil
+	}
+
+	return strings.Split(files, "\n"), nil
+}
+
 // Clean the output
 func Clean(output string, err error) (string, error) {
 	// Preserve multi-line output, but trim the trailing newline
diff --git a/internal/task/bump/bump.go b/internal/task/bump/bump.go
index 4f42f48..a7ab066 100644
--- a/internal/task/bump/bump.go
+++ b/internal/task/bump/bump.go
@@ -93,8 +93,7 @@ func (t Task) Run(ctx *context.Context) error {
 	}
 
 	if n > 0 {
-		log.Info("attempting to commit file changes")
-		return git.Commit(ctx.CommitDetails)
+		log.WithField("count", n).Debug("bumped files staged")
 	}
 
 	return nil
diff --git a/internal/task/changelog/changelog.go b/internal/task/changelog/changelog.go
new file mode 100644
index 0000000..9c1907d
--- /dev/null
+++ b/internal/task/changelog/changelog.go
@@ -0,0 +1,173 @@
+/*
+Copyright (c) 2021 Gemba Advantage
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+package changelog
+
+import (
+	"bytes"
+	_ "embed"
+	"errors"
+	"io/ioutil"
+	"os"
+	"strings"
+	"text/template"
+	"time"
+
+	"github.com/apex/log"
+	"github.com/gembaadvantage/uplift/internal/context"
+	"github.com/gembaadvantage/uplift/internal/git"
+)
+
+const (
+	//  MarkdownFile defines the name of the changelog markdown file within a repository
+	MarkdownFile = "CHANGELOG.md"
+
+	// ChangeDate defines a date formatting used when logging a new change
+	ChangeDate = "2006-01-02"
+
+	appendHeader = "## [Unreleased]\n\n"
+)
+
+var (
+	//go:embed template/new.tpl
+	newTpl string
+
+	//go:embed template/append.tpl
+	appendTpl string
+
+	newTplBody    = template.Must(template.New("new").Parse(newTpl))
+	appendTplBody = template.Must(template.New("append").Parse(appendTpl))
+
+	// ErrNoAppendHeader is reported if a changelog is missing the expected append header
+	ErrNoAppendHeader = errors.New("changelog missing supported append header")
+)
+
+type release struct {
+	Tag     string
+	Date    string
+	Changes []git.LogEntry
+}
+
+// Task that generates a changelog for the current repository
+type Task struct{}
+
+// String generates a string representation of the task
+func (t Task) String() string {
+	return "changelog"
+}
+
+// Skip running the task if no changelog is needed
+func (t Task) Skip(ctx *context.Context) bool {
+	return false
+}
+
+// Run the task
+func (t Task) Run(ctx *context.Context) error {
+	if ctx.NextVersion.Raw == "" {
+		log.Info("no release detected, skipping changelog")
+		return nil
+	}
+
+	log.WithField("tag", ctx.NextVersion.Raw).Info("determine changes for release")
+	ents, err := git.LogBetween(ctx.NextVersion.Raw, ctx.CurrentVersion.Raw)
+	if err != nil {
+		return err
+	}
+
+	// Package log entries into release ready for template generation
+	rel := release{
+		Tag:     ctx.NextVersion.Raw,
+		Date:    time.Now().UTC().Format(ChangeDate),
+		Changes: ents,
+	}
+	log.WithFields(log.Fields{
+		"tag":     ctx.NextVersion.Raw,
+		"date":    time.Now().UTC().Format(ChangeDate),
+		"commits": len(rel.Changes),
+	}).Info("changeset identified")
+
+	if ctx.Debug {
+		for _, c := range rel.Changes {
+			log.WithFields(log.Fields{
+				"hash":    c.AbbrevHash,
+				"message": c.Message,
+			}).Debug("commit")
+		}
+	}
+
+	if ctx.DryRun {
+		log.Info("skip writing to changelog in dry run mode")
+		return nil
+	}
+
+	var chgErr error
+	if noChangelogExists() {
+		chgErr = newChangelog(rel)
+	} else {
+		chgErr = appendChangelog(rel)
+	}
+	if chgErr != nil {
+		return chgErr
+	}
+
+	log.Debug("staging CHANGELOG.md")
+	return git.Stage(MarkdownFile)
+}
+
+func noChangelogExists() bool {
+	_, err := os.Stat(MarkdownFile)
+	return os.IsNotExist(err)
+}
+
+func newChangelog(rel release) error {
+	f, err := os.OpenFile(MarkdownFile, os.O_CREATE|os.O_RDWR, 0644)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	log.Debug("create new changelog in repository")
+	return newTplBody.Execute(f, rel)
+}
+
+func appendChangelog(rel release) error {
+	cl, err := ioutil.ReadFile(MarkdownFile)
+	if err != nil {
+		return err
+	}
+
+	// Appending is only possible if token token exists
+	clStr := string(cl)
+	if idx := strings.Index(clStr, appendHeader); idx == -1 {
+		return ErrNoAppendHeader
+	}
+
+	var buf bytes.Buffer
+	if err := appendTplBody.Execute(&buf, rel); err != nil {
+		return err
+	}
+
+	apnd := strings.Replace(clStr, appendHeader, buf.String(), 1)
+
+	log.Debug("append to existing changelog in repository")
+	return ioutil.WriteFile(MarkdownFile, []byte(apnd), 0644)
+}
diff --git a/internal/task/changelog/template/append.tpl b/internal/task/changelog/template/append.tpl
new file mode 100644
index 0000000..91a70d5
--- /dev/null
+++ b/internal/task/changelog/template/append.tpl
@@ -0,0 +1,7 @@
+## [Unreleased]
+
+## [{{.Tag}}] - {{.Date}}
+{{range $chg := .Changes }}
+{{.AbbrevHash}} {{.Message}}
+{{- end}}
+
diff --git a/internal/task/changelog/template/new.tpl b/internal/task/changelog/template/new.tpl
new file mode 100644
index 0000000..e67ee65
--- /dev/null
+++ b/internal/task/changelog/template/new.tpl
@@ -0,0 +1,10 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [Unreleased]
+
+## [{{.Tag}}] - {{.Date}}
+{{range $chg := .Changes }}
+{{.AbbrevHash}} {{.Message}}
+{{- end}}
diff --git a/internal/task/gitpush/gitpush.go b/internal/task/gitpush/gitpush.go
index 8a38817..f6e52be 100644
--- a/internal/task/gitpush/gitpush.go
+++ b/internal/task/gitpush/gitpush.go
@@ -36,18 +36,29 @@ func (t Task) String() string {
 	return "git push"
 }
 
-// Skip running the task if no version has changed
+// Skip running the task if no version change is detected, or if running
+// in dry-run mode
 func (t Task) Skip(ctx *context.Context) bool {
-	return ctx.NoVersionChanged
+	return ctx.DryRun || ctx.NoVersionChanged
 }
 
 // Run the task
 func (t Task) Run(ctx *context.Context) error {
-	if ctx.DryRun {
-		log.Info("skipping push in dry run mode")
+	stg, err := git.Staged()
+	if err != nil {
+		return err
+	}
+
+	if len(stg) == 0 {
+		log.Info("no files staged skipping push")
 		return nil
 	}
 
-	log.Info("check and push any outstanding commits")
+	log.Info("commit outstanding changes")
+	if err := git.Commit(ctx.CommitDetails); err != nil {
+		return err
+	}
+
+	log.Info("push commit to remote")
 	return git.Push()
 }
diff --git a/internal/task/lastcommit/lastcommit.go b/internal/task/lastcommit/lastcommit.go
new file mode 100644
index 0000000..10306c8
--- /dev/null
+++ b/internal/task/lastcommit/lastcommit.go
@@ -0,0 +1,58 @@
+/*
+Copyright (c) 2021 Gemba Advantage
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+package lastcommit
+
+import (
+	"github.com/apex/log"
+	"github.com/gembaadvantage/uplift/internal/context"
+	"github.com/gembaadvantage/uplift/internal/git"
+)
+
+// Task for reading the last commit message
+type Task struct{}
+
+// String generates a string representation of the task
+func (t Task) String() string {
+	return "latest commit"
+}
+
+// Skip is disabled for this task
+func (t Task) Skip(ctx *context.Context) bool {
+	return false
+}
+
+// Run the task
+func (t Task) Run(ctx *context.Context) error {
+	commit, err := git.LatestCommit()
+	if err != nil {
+		return err
+	}
+	log.WithFields(log.Fields{
+		"author":  commit.Author,
+		"email":   commit.Email,
+		"message": commit.Message,
+	}).Info("retrieved latest commit")
+
+	ctx.CommitDetails = commit
+	return nil
+}
diff --git a/internal/task/nextcommit/nextcommit.go b/internal/task/nextcommit/nextcommit.go
index 65a23c4..ea10268 100644
--- a/internal/task/nextcommit/nextcommit.go
+++ b/internal/task/nextcommit/nextcommit.go
@@ -49,7 +49,7 @@ func (t Task) Run(ctx *context.Context) error {
 	c := git.CommitDetails{
 		Author:  ctx.CommitDetails.Author,
 		Email:   ctx.CommitDetails.Email,
-		Message: fmt.Sprintf("ci(bump): bumped version to %s", ctx.NextVersion.Raw),
+		Message: fmt.Sprintf("ci(uplift): uplifted for version %s", ctx.NextVersion.Raw),
 	}
 
 	if ctx.Config.CommitAuthor.Name != "" {
@@ -72,6 +72,6 @@ func (t Task) Run(ctx *context.Context) error {
 		"name":    ctx.CommitDetails.Author,
 		"email":   ctx.CommitDetails.Email,
 		"message": ctx.CommitDetails.Message,
-	}).Info("committing with")
+	}).Info("changes will be committed with")
 	return nil
 }
diff --git a/internal/task/nextversion/nextversion.go b/internal/task/nextversion/nextversion.go
index 10c77a8..4c83a21 100644
--- a/internal/task/nextversion/nextversion.go
+++ b/internal/task/nextversion/nextversion.go
@@ -27,7 +27,6 @@ import (
 	"github.com/apex/log"
 
 	"github.com/gembaadvantage/uplift/internal/context"
-	"github.com/gembaadvantage/uplift/internal/git"
 	"github.com/gembaadvantage/uplift/internal/semver"
 )
 
@@ -51,13 +50,7 @@ func (t Task) Skip(ctx *context.Context) bool {
 
 // Run the task
 func (t Task) Run(ctx *context.Context) error {
-	commit, err := git.LatestCommit()
-	if err != nil {
-		return err
-	}
-	ctx.CommitDetails = commit
-
-	inc := semver.ParseCommit(commit.Message)
+	inc := semver.ParseCommit(ctx.CommitDetails.Message)
 	if inc == semver.NoIncrement {
 		ctx.NextVersion = ctx.CurrentVersion
 		ctx.NoVersionChanged = true


Test patch:
diff --git a/.github/workflows/windows-test.yml b/.github/workflows/windows-test.yml
index 63cd2b7..8da3826 100644
--- a/.github/workflows/windows-test.yml
+++ b/.github/workflows/windows-test.yml
@@ -25,6 +25,10 @@ jobs:
   windows-test:
     runs-on: windows-latest
     steps:
+      - name: Set Git Line Endings
+        run: |
+          git config --global core.autocrlf false
+          git config --global core.eol lf
       - name: Checkout
         uses: actions/checkout@v2
         with:
diff --git a/internal/git/testing.go b/internal/git/testing.go
index bcf2b77..0b5ca6e 100644
--- a/internal/git/testing.go
+++ b/internal/git/testing.go
@@ -24,15 +24,20 @@ package git
 
 import (
 	"os"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/require"
 )
 
+const (
+	InitCommit = "initialise repo"
+)
+
 // InitRepo creates an empty git repository within a temporary directory. Once created
 // the current testing context will operate from within that directory until the calling
 // test has completed
-func InitRepo(t *testing.T) {
+func InitRepo(t *testing.T) string {
 	t.Helper()
 
 	MkTmpDir(t)
@@ -41,7 +46,7 @@ func InitRepo(t *testing.T) {
 	_, err := Run("init")
 	require.NoError(t, err)
 
-	EmptyCommit(t, "initialise repo")
+	return EmptyCommit(t, InitCommit)
 }
 
 // MkTmpDir creates an empty directory that is not a git repository. Once created the
@@ -62,7 +67,7 @@ func MkTmpDir(t *testing.T) {
 
 // EmptyCommit will create an empty commit without the need for modifying any existing files
 // within the repository
-func EmptyCommit(t *testing.T, commit string) {
+func EmptyCommit(t *testing.T, commit string) string {
 	t.Helper()
 
 	args := []string{
@@ -76,36 +81,49 @@ func EmptyCommit(t *testing.T, commit string) {
 		commit,
 	}
 
-	_, err := Run(args...)
+	out, err := Run(args...)
 	require.NoError(t, err)
+
+	return abbrevHash(out)
+}
+
+func abbrevHash(m string) string {
+	i := strings.Index(m, "]")
+	return m[i-7 : i]
 }
 
 // EmptyCommits will create any number of empty commits without the need for modifying any
 // existing files within the repository
-func EmptyCommits(t *testing.T, commits ...string) {
+func EmptyCommits(t *testing.T, commits ...string) []string {
 	t.Helper()
 
+	hs := make([]string, 0, len(commits))
 	for _, msg := range commits {
-		EmptyCommit(t, msg)
+		hs = append(hs, EmptyCommit(t, msg))
 	}
+	return hs
 }
 
 // EmptyCommitAndTag will create an empty commit with an associated tag. No existing files
 // will be modified within the repository
-func EmptyCommitAndTag(t *testing.T, tag, msg string) {
+func EmptyCommitAndTag(t *testing.T, tag, msg string) string {
 	t.Helper()
 
-	EmptyCommit(t, msg)
+	h := EmptyCommit(t, msg)
 	err := Tag(tag)
 	require.NoError(t, err)
+
+	return h
 }
 
 // EmptyCommitsAndTag will create any number of empty commits and associate them with a tag.
 // No existing files will be modified within the repository
-func EmptyCommitsAndTag(t *testing.T, tag string, msgs ...string) {
+func EmptyCommitsAndTag(t *testing.T, tag string, msgs ...string) []string {
 	t.Helper()
 
-	EmptyCommits(t, msgs...)
+	hs := EmptyCommits(t, msgs...)
 	err := Tag(tag)
 	require.NoError(t, err)
+
+	return hs
 }
diff --git a/internal/git/utils_test.go b/internal/git/utils_test.go
index 31fd282..5ad7fb3 100644
--- a/internal/git/utils_test.go
+++ b/internal/git/utils_test.go
@@ -36,11 +36,25 @@ func TestIsRepo(t *testing.T) {
 	assert.True(t, IsRepo())
 }
 
-func TestIsRepoDetectsNonGitRepo(t *testing.T) {
+func TestIsRepo_DetectsNonGitRepo(t *testing.T) {
 	MkTmpDir(t)
 	assert.False(t, IsRepo())
 }
 
+func TestAllTags(t *testing.T) {
+	InitRepo(t)
+
+	v1 := "v1.0.0"
+	EmptyCommitAndTag(t, v1, "first commit")
+	v2 := "v2.0.0"
+	EmptyCommitAndTag(t, v2, "second commit")
+	v3 := "v3.0.0"
+	EmptyCommitAndTag(t, v3, "third commit")
+
+	tags := AllTags()
+	assert.Equal(t, []string{v3, v2, v1}, tags)
+}
+
 func TestLatestTag(t *testing.T) {
 	InitRepo(t)
 
@@ -53,14 +67,14 @@ func TestLatestTag(t *testing.T) {
 	assert.Equal(t, v2, tag)
 }
 
-func TestLatestTagNoTagsExist(t *testing.T) {
+func TestLatestTag_NoTagsExist(t *testing.T) {
 	MkTmpDir(t)
 
 	tag := LatestTag()
 	assert.Equal(t, "", tag)
 }
 
-func TestLatestTagNoSemanticTags(t *testing.T) {
+func TestLatestTag_NoSemanticTags(t *testing.T) {
 	InitRepo(t)
 
 	v1 := "v1"
@@ -86,7 +100,7 @@ func TestLatestCommit(t *testing.T) {
 	assert.Equal(t, c.Message, m)
 }
 
-func TestLatestCommitMultipleCommits(t *testing.T) {
+func TestLatestCommit_MultipleCommits(t *testing.T) {
 	InitRepo(t)
 
 	m := "third commit"
@@ -181,3 +195,125 @@ func StagedFile(t *testing.T) string {
 
 	return file
 }
+
+func TestLogBetween_TwoTags(t *testing.T) {
+	InitRepo(t)
+	EmptyCommitAndTag(t, "1.0.0", "first commit")
+	EmptyCommitsAndTag(t, "2.0.0", "second commit", "third commit", "forth commit")
+
+	log, err := LogBetween("2.0.0", "1.0.0")
+	require.NoError(t, err)
+
+	require.Len(t, log, 3)
+	assert.Equal(t, log[0].Message, "forth commit")
+	assert.Equal(t, log[1].Message, "third commit")
+	assert.Equal(t, log[2].Message, "second commit")
+}
+
+func TestLogBetween_TwoHashes(t *testing.T) {
+	InitRepo(t)
+	h := EmptyCommits(t, "first commit", "second commit", "third commit", "forth commit")
+
+	log, err := LogBetween(h[2], h[1])
+	require.NoError(t, err)
+
+	require.Len(t, log, 1)
+	assert.Equal(t, log[0].Message, "third commit")
+}
+
+func TestLogBetween_FromSpecificTag(t *testing.T) {
+	InitRepo(t)
+	EmptyCommitsAndTag(t, "1.0.0", "first commit", "second commit")
+	EmptyCommit(t, "third commit")
+
+	log, err := LogBetween("1.0.0", "")
+	require.NoError(t, err)
+
+	require.Len(t, log, 3)
+	assert.Equal(t, log[0].Message, "second commit")
+	assert.Equal(t, log[1].Message, "first commit")
+	assert.Equal(t, log[2].Message, InitCommit)
+}
+
+func TestLogBetween_FromSpecificHash(t *testing.T) {
+	InitRepo(t)
+	h := EmptyCommits(t, "first commit", "second commit", "third commit", "forth commit")
+
+	log, err := LogBetween(h[2], "")
+	require.NoError(t, err)
+
+	require.Len(t, log, 4)
+	assert.Equal(t, log[0].Message, "third commit")
+	assert.Equal(t, log[1].Message, "second commit")
+	assert.Equal(t, log[2].Message, "first commit")
+	assert.Equal(t, log[3].Message, InitCommit)
+}
+
+func TestLogBetween_ToSpecificHash(t *testing.T) {
+	InitRepo(t)
+	h := EmptyCommits(t, "first commit", "second commit", "third commit", "forth commit")
+
+	log, err := LogBetween("", h[2])
+	require.NoError(t, err)
+
+	require.Len(t, log, 1)
+	assert.Equal(t, log[0].Message, "forth commit")
+}
+
+func TestLogBetween_ToSpecificTag(t *testing.T) {
+	InitRepo(t)
+	EmptyCommitsAndTag(t, "1.0.0", "first commit", "second commit")
+	EmptyCommit(t, "third commit")
+
+	log, err := LogBetween("", "1.0.0")
+	require.NoError(t, err)
+
+	require.Len(t, log, 1)
+	assert.Equal(t, log[0].Message, "third commit")
+}
+
+func TestLogBetween_All(t *testing.T) {
+	InitRepo(t)
+	EmptyCommits(t, "first commit", "second commit", "third commit")
+
+	log, err := LogBetween("", "")
+	require.NoError(t, err)
+
+	require.Len(t, log, 4)
+	assert.Equal(t, log[0].Message, "third commit")
+	assert.Equal(t, log[1].Message, "second commit")
+	assert.Equal(t, log[2].Message, "first commit")
+	assert.Equal(t, log[3].Message, InitCommit)
+}
+
+func TestLogBetween_ErrorInvalidRevision(t *testing.T) {
+	InitRepo(t)
+
+	_, err := LogBetween("1234567", "")
+	require.Error(t, err)
+}
+
+func TestStaged(t *testing.T) {
+	InitRepo(t)
+	ioutil.WriteFile("test1.txt", []byte(`testing`), 0644)
+	Stage("test1.txt")
+
+	ioutil.WriteFile("test2.txt", []byte(`testing`), 0644)
+	Stage("test2.txt")
+
+	stg, err := Staged()
+	require.NoError(t, err)
+
+	assert.Len(t, stg, 2)
+	assert.ElementsMatch(t, stg, []string{"test1.txt", "test2.txt"})
+}
+
+func TestStaged_NoFilesStaged(t *testing.T) {
+	InitRepo(t)
+	ioutil.WriteFile("test.txt", []byte(`testing`), 0644)
+
+	stg, err := Staged()
+	require.NoError(t, err)
+
+	assert.Len(t, stg, 0)
+}
diff --git a/internal/task/changelog/changelog_test.go b/internal/task/changelog/changelog_test.go
new file mode 100644
index 0000000..e08595e
--- /dev/null
+++ b/internal/task/changelog/changelog_test.go
@@ -0,0 +1,210 @@
+/*
+Copyright (c) 2021 Gemba Advantage
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+package changelog
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"github.com/gembaadvantage/uplift/internal/context"
+	"github.com/gembaadvantage/uplift/internal/git"
+	"github.com/gembaadvantage/uplift/internal/semver"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestRun_NoNextTag(t *testing.T) {
+	git.InitRepo(t)
+	git.EmptyCommits(t, "first commit", "second commit", "third commit")
+
+	err := Task{}.Run(&context.Context{})
+	require.NoError(t, err)
+
+	assert.False(t, changelogExists(t))
+}
+
+func changelogExists(t *testing.T) bool {
+	t.Helper()
+
+	current, err := os.Getwd()
+	require.NoError(t, err)
+
+	if _, err := os.Stat(filepath.Join(current, MarkdownFile)); err != nil {
+		if os.IsNotExist(err) {
+			return false
+		}
+		require.NoError(t, err)
+	}
+
+	return true
+}
+
+func TestRun_ChangelogCreatedIfNotExists(t *testing.T) {
+	git.InitRepo(t)
+	git.EmptyCommitsAndTag(t, "1.0.0", "first commit", "second commit")
+
+	ctx := &context.Context{
+		NextVersion: semver.Version{
+			Raw: "1.0.0",
+		},
+	}
+
+	err := Task{}.Run(ctx)
+	require.NoError(t, err)
+
+	assert.True(t, changelogExists(t))
+}
+
+func TestRun_ChangelogStaged(t *testing.T) {
+	git.InitRepo(t)
+	git.EmptyCommitsAndTag(t, "1.0.0", "first commit", "second commit")
+
+	ctx := &context.Context{
+		NextVersion: semver.Version{
+			Raw: "1.0.0",
+		},
+	}
+
+	err := Task{}.Run(ctx)
+	require.NoError(t, err)
+
+	stg, _ := git.Staged()
+	assert.Len(t, stg, 1)
+	assert.Equal(t, "CHANGELOG.md", stg[0])
+}
+
+func TestRun_AppendToUnsupportedTemplate(t *testing.T) {
+	git.InitRepo(t)
+	git.EmptyCommitsAndTag(t, "1.0.0", "first commit")
+
+	cl := `# Changelog
+This changelog is deliberately missing the append marker`
+	ioutil.WriteFile(MarkdownFile, []byte(cl), 0644)
+
+	ctx := &context.Context{
+		NextVersion: semver.Version{
+			Raw: "1.0.0",
+		},
+	}
+
+	err := Task{}.Run(ctx)
+	require.ErrorIs(t, err, ErrNoAppendHeader)
+}
+
+func TestRun_AppendToExistingChangelog(t *testing.T) {
+	ih := git.InitRepo(t)
+	h1 := git.EmptyCommitsAndTag(t, "1.0.0", "first commit")
+	h2 := git.EmptyCommitsAndTag(t, "1.1.0", "second commit", "third commit")
+
+	// Initial changelog
+	cl := fmt.Sprintf(`# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [Unreleased]
+
+## [1.0.0] - 2021-09-17
+
+%s first commit
+%s initialise repo
+`, h1[0], ih)
+	ioutil.WriteFile(MarkdownFile, []byte(cl), 0644)
+
+	ctx := &context.Context{
+		CurrentVersion: semver.Version{
+			Raw: "1.0.0",
+		},
+		NextVersion: semver.Version{
+			Raw: "1.1.0",
+		},
+	}
+
+	err := Task{}.Run(ctx)
+	require.NoError(t, err)
+
+	expected := fmt.Sprintf(`# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [Unreleased]
+
+## [1.1.0] - %s
+
+%s third commit
+%s second commit
+
+## [1.0.0] - 2021-09-17
+
+%s first commit
+%s %s
+`, changelogDate(t), h2[1], h2[0], h1[0], ih, git.InitCommit)
+
+	assert.Equal(t, expected, readChangelog(t))
+}
+
+func TestRun_ChangelogEntriesFromFirstTag(t *testing.T) {
+	ih := git.InitRepo(t)
+	h := git.EmptyCommitsAndTag(t, "1.0.0", "first commit", "second commit")
+
+	ctx := &context.Context{
+		NextVersion: semver.Version{
+			Raw: "1.0.0",
+		},
+	}
+
+	err := Task{}.Run(ctx)
+	require.NoError(t, err)
+
+	expected := fmt.Sprintf(`# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [Unreleased]
+
+## [1.0.0] - %s
+
+%s second commit
+%s first commit
+%s %s
+`, changelogDate(t), h[1], h[0], ih, git.InitCommit)
+
+	assert.Equal(t, expected, readChangelog(t))
+}
+
+func readChangelog(t *testing.T) string {
+	t.Helper()
+
+	data, err := ioutil.ReadFile(MarkdownFile)
+	require.NoError(t, err)
+
+	return string(data)
+}
+
+func changelogDate(t *testing.T) string {
+	t.Helper()
+	return time.Now().UTC().Format(ChangeDate)
+}
diff --git a/internal/task/gitpush/gitpush_test.go b/internal/task/gitpush/gitpush_test.go
new file mode 100644
index 0000000..49af3bd
--- /dev/null
+++ b/internal/task/gitpush/gitpush_test.go
@@ -0,0 +1,67 @@
+/*
+Copyright (c) 2021 Gemba Advantage
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+package gitpush
+
+import (
+	"io/ioutil"
+	"testing"
+
+	"github.com/gembaadvantage/uplift/internal/context"
+	"github.com/gembaadvantage/uplift/internal/git"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestRun(t *testing.T) {
+	git.InitRepo(t)
+	trackFile(t, "test.txt")
+
+	err := Task{}.Run(&context.Context{
+		CommitDetails: git.CommitDetails{
+			Author:  "uplift",
+			Email:   "uplift@test.com",
+			Message: "test commit",
+		},
+	})
+	require.NoError(t, err)
+
+	c, _ := git.LatestCommit()
+	assert.Equal(t, "uplift", c.Author)
+	assert.Equal(t, "uplift@test.com", c.Email)
+	assert.Equal(t, "test commit", c.Message)
+}
+
+func trackFile(t *testing.T, name string) {
+	err := ioutil.WriteFile(name, []byte(`hello, world`), 0644)
+	require.NoError(t, err)
+
+	err = git.Stage(name)
+	require.NoError(t, err)
+}
+
+func TestRun_NoGitRepository(t *testing.T) {
+	git.MkTmpDir(t)
+
+	err := Task{}.Run(&context.Context{})
+	require.Error(t, err)
+}
diff --git a/internal/task/lastcommit/lastcommit_test.go b/internal/task/lastcommit/lastcommit_test.go
new file mode 100644
index 0000000..81270e7
--- /dev/null
+++ b/internal/task/lastcommit/lastcommit_test.go
@@ -0,0 +1,52 @@
+/*
+Copyright (c) 2021 Gemba Advantage
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+package lastcommit
+
+import (
+	"testing"
+
+	"github.com/gembaadvantage/uplift/internal/context"
+	"github.com/gembaadvantage/uplift/internal/git"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestRun(t *testing.T) {
+	git.InitRepo(t)
+	git.EmptyCommit(t, "test commit")
+
+	ctx := &context.Context{}
+	err := Task{}.Run(ctx)
+
+	require.NoError(t, err)
+	assert.Equal(t, "uplift", ctx.CommitDetails.Author)
+	assert.Equal(t, "uplift@test.com", ctx.CommitDetails.Email)
+	assert.Equal(t, "test commit", ctx.CommitDetails.Message)
+}
+
+func TestRun_NoGitRepository(t *testing.T) {
+	git.MkTmpDir(t)
+
+	err := Task{}.Run(&context.Context{})
+	require.Error(t, err)
+}
diff --git a/internal/task/nextcommit/nextcommit_test.go b/internal/task/nextcommit/nextcommit_test.go
index 14c97a5..518484c 100644
--- a/internal/task/nextcommit/nextcommit_test.go
+++ b/internal/task/nextcommit/nextcommit_test.go
@@ -42,7 +42,7 @@ func TestRun_DefaultCommitMessage(t *testing.T) {
 
 	err := Task{}.Run(ctx)
 	require.NoError(t, err)
-	assert.Equal(t, "ci(bump): bumped version to 0.1.0", ctx.CommitDetails.Message)
+	assert.Equal(t, "ci(uplift): uplifted for version 0.1.0", ctx.CommitDetails.Message)
 }
 
 func TestRun_ImpersonatesAuthor(t *testing.T) {
diff --git a/internal/task/nextversion/nextversion_test.go b/internal/task/nextversion/nextversion_test.go
index a8c2b78..3cf5254 100644
--- a/internal/task/nextversion/nextversion_test.go
+++ b/internal/task/nextversion/nextversion_test.go
@@ -73,6 +73,9 @@ func TestRun(t *testing.T) {
 			}
 
 			ctx := &context.Context{
+				CommitDetails: git.CommitDetails{
+					Message: tt.commit,
+				},
 				CurrentVersion: semver.Version{
 					Raw: tt.curVer,
 				},
@@ -93,7 +96,11 @@ func TestRun_FirstTagDefault(t *testing.T) {
 	git.InitRepo(t)
 	git.EmptyCommit(t, "feat: a new feature")
 
-	ctx := &context.Context{}
+	ctx := &context.Context{
+		CommitDetails: git.CommitDetails{
+			Message: "feat: a new feature",
+		},
+	}
 	err := Task{}.Run(ctx)
 
 	require.NoError(t, err)
@@ -105,6 +112,9 @@ func TestRun_FirstTagDefaultFromConfig(t *testing.T) {
 	git.EmptyCommit(t, "feat: a new feature")
 
 	ctx := &context.Context{
+		CommitDetails: git.CommitDetails{
+			Message: "feat: a new feature",
+		},
 		Config: config.Uplift{
 			FirstVersion: "1.0.0",
 		},
@@ -114,10 +124,3 @@ func TestRun_FirstTagDefaultFromConfig(t *testing.T) {
 	require.NoError(t, err)
 	assert.Equal(t, ctx.Config.FirstVersion, ctx.NextVersion.Raw)
 }
-
-func TestRun_NoGitRepository(t *testing.T) {
-	git.MkTmpDir(t)
-
-	err := Task{}.Run(&context.Context{})
-	require.Error(t, err)
-}