Repository: gembaadvantage/uplift (go)

Issue:
[Feature]: support annotated git tags
### Describe your feature

Currently uplift only supports lightweight tags. These are effectively just a pointer to a commit at a certain point in time and are seen as temporary tags. Annotated tags are stored as full objects within the git database and support the following features:

1. Tagger details (name, email etc)
2. GPG signed tags

### Your potential solution

Support the options of lightweight/annotated tags within the uplift config YAML file

### Any additional information?

_No response_

### Code of Conduct

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

Pull request:
feat: support creation of annotated tags (#25)
@fixes #25 

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9d5b531..cdc3328 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,6 +32,7 @@ jobs:
         uses: actions/checkout@v2
         with:
           fetch-depth: 0
+          token: ${{ secrets.GH_UPLIFT }}
       - name: Setup Go
         uses: actions/setup-go@v2
         with:
diff --git a/README.md b/README.md
index 10963ac..fb7712b 100644
--- a/README.md
+++ b/README.md
@@ -65,8 +65,14 @@ Uplift can be configured through the existance of an optional `.uplift.yml` conf
 # Defaults to 0.1.0
 firstVersion: v1.0.0
 
+# Use annotated tags instead of lightweight tags when tagging a version bump. An
+# annotated tag is treated like a regular commit and contains both author details
+# and a commit message. Uses the same commit message and author details provided
+# Defaults to false
+annotatedTags: true
+
 # Changes the commit message when bumping files
-# Defaults to chore(release): release managed by uplift
+# Defaults to ci(bump): bumped version to $VERSION
 commitMessage: "chore: a custom commit message"
 
 # Changes the commit author when bumping files
diff --git a/internal/config/uplift.go b/internal/config/uplift.go
index 02936b9..75f5d87 100644
--- a/internal/config/uplift.go
+++ b/internal/config/uplift.go
@@ -35,6 +35,7 @@ type Uplift struct {
 	Bumps         []Bump       `yaml:"bumps"`
 	CommitMessage string       `yaml:"commitMessage"`
 	CommitAuthor  CommitAuthor `yaml:"commitAuthor"`
+	AnnotatedTags bool         `yaml:"annotatedTags"`
 }
 
 // Bump defines configuration for bumping indvidual files based
diff --git a/internal/git/utils.go b/internal/git/utils.go
index bc3fc91..7adcbb6 100644
--- a/internal/git/utils.go
+++ b/internal/git/utils.go
@@ -97,7 +97,7 @@ func LatestCommit() (CommitDetails, error) {
 	}, nil
 }
 
-// Tag the repository and push it to the origin
+// 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 {
 		return err
@@ -115,6 +115,36 @@ func Tag(tag string) error {
 	return nil
 }
 
+// AnnotatedTag will create an annotated tag against the repository and push it to the origin
+func AnnotatedTag(tag string, cd CommitDetails) error {
+	args := []string{
+		"-c",
+		fmt.Sprintf("user.name='%s'", cd.Author),
+		"-c",
+		fmt.Sprintf("user.email='%s'", cd.Email),
+		"tag",
+		"-a",
+		tag,
+		"-m",
+		cd.Message,
+	}
+
+	if _, err := Clean(Run(args...)); err != nil {
+		return err
+	}
+
+	// Inspect the repo for an origin. If no origin exists, then skip the push
+	if _, err := Clean(Run("remote", "show", "origin")); err != nil {
+		return nil
+	}
+
+	if _, err := Clean(Run("push", "origin", tag)); err != nil {
+		return err
+	}
+
+	return nil
+}
+
 // Commit will generate a commit against the repository and push it to the origin.
 // The commit will be associated with the provided author and email address
 func Commit(cd CommitDetails) error {
diff --git a/internal/semver/bump.go b/internal/semver/bump.go
index f1c3c06..3e1c4b4 100644
--- a/internal/semver/bump.go
+++ b/internal/semver/bump.go
@@ -125,7 +125,7 @@ func (b Bumper) Bump() error {
 	}
 
 	// Ensure any files that are bumped are associated with the expected commit
-	commit = b.buildCommit(commit)
+	commit = b.buildCommit(ver, commit)
 
 	if err := b.bumpFiles(ver, commit); err != nil {
 		return err
@@ -137,8 +137,14 @@ func (b Bumper) Bump() error {
 		return nil
 	}
 
-	if err := git.Tag(ver); err != nil {
-		return err
+	if b.config.AnnotatedTags {
+		if err := git.AnnotatedTag(ver, commit); err != nil {
+			return err
+		}
+	} else {
+		if err := git.Tag(ver); err != nil {
+			return err
+		}
 	}
 
 	b.logger.Out(ver)
@@ -227,11 +233,11 @@ func (b Bumper) bumpFiles(v string, commit git.CommitDetails) error {
 	return git.Push()
 }
 
-func (b Bumper) buildCommit(commit git.CommitDetails) git.CommitDetails {
+func (b Bumper) buildCommit(ver string, commit git.CommitDetails) git.CommitDetails {
 	c := git.CommitDetails{
 		Author:  commit.Author,
 		Email:   commit.Email,
-		Message: "chore(release): release managed by uplift",
+		Message: fmt.Sprintf("ci(bump): bumped version to %s", ver),
 	}
 
 	if b.config.CommitAuthor.Name != "" {


Test patch:
diff --git a/internal/git/utils_test.go b/internal/git/utils_test.go
index 03f11af..31fd282 100644
--- a/internal/git/utils_test.go
+++ b/internal/git/utils_test.go
@@ -109,6 +109,25 @@ func TestTag(t *testing.T) {
 	require.NoError(t, err)
 }
 
+func TestAnnotatedTag(t *testing.T) {
+	InitRepo(t)
+
+	v := "v1.0.0"
+	cd := CommitDetails{
+		Author:  "joe.bloggs",
+		Email:   "joe.bloggs@gmail.com",
+		Message: "a tag commit message",
+	}
+
+	err := AnnotatedTag(v, cd)
+	require.NoError(t, err)
+
+	out, _ := Clean(Run("for-each-ref", fmt.Sprintf("refs/tags/%s", v),
+		"--format='%(taggername):%(taggeremail):%(contents)'"))
+
+	require.Contains(t, out, fmt.Sprintf("%s:<%s>:%s", cd.Author, cd.Email, cd.Message))
+}
+
 func TestStage(t *testing.T) {
 	InitRepo(t)
 	file := UnstagedFile(t)
diff --git a/internal/semver/bump_test.go b/internal/semver/bump_test.go
index 88bb160..d14da4b 100644
--- a/internal/semver/bump_test.go
+++ b/internal/semver/bump_test.go
@@ -165,6 +165,27 @@ func TestBumpAlwaysUseLatestCommit(t *testing.T) {
 	assert.Equal(t, "", git.LatestTag())
 }
 
+func TestBumpWithAnnotatedTag(t *testing.T) {
+	git.InitRepo(t)
+	git.EmptyCommitAndTag(t, "0.1.0", "feat: Lorem ipsum dolor sit amet")
+
+	b := NewBumper(io.Discard, BumpOptions{
+		Config: config.Uplift{
+			AnnotatedTags: true,
+			CommitMessage: "this is an annotated tag",
+		},
+	})
+	err := b.Bump()
+
+	require.NoError(t, err)
+	assert.Equal(t, "0.2.0", git.LatestTag())
+
+	out, _ := git.Clean(git.Run("for-each-ref", "refs/tags/0.2.0",
+		"--format='%(contents)'"))
+
+	assert.Contains(t, out, "this is an annotated tag")
+}
+
 func TestBumpFile(t *testing.T) {
 	tests := []struct {
 		name     string
@@ -382,7 +403,7 @@ func TestBumpFileDefaultCommitMessage(t *testing.T) {
 
 	assert.Equal(t, "uplift", commit.Author)
 	assert.Equal(t, "uplift@test.com", commit.Email)
-	assert.Equal(t, "chore(release): release managed by uplift", commit.Message)
+	assert.Equal(t, "ci(bump): bumped version to 0.1.1", commit.Message)
 }
 
 func TestBumpFileWithCustomisedCommit(t *testing.T) {