Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion vcs/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package vcs

import (
"errors"
"fmt"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/rogpeppe/go-internal/semver"
"github.com/sirupsen/logrus"
"os/exec"
"strings"
"time"
)
Expand Down Expand Up @@ -177,8 +179,23 @@ func compareSemantically(v *plumbing.Reference, w *plumbing.Reference) int {
return semver.Compare(a, b)
}

// TagRelease tags the repository with the given version.
// TagRelease tags the repository with the given version. When git is
// configured to sign tags (via tag.gpgSign or tag.forceSignAnnotated) an
// annotated, signed tag is created instead of a lightweight one.
func TagRelease(repoPath string, hash string, version string) error {
shouldSign, err := isTagSigningEnabled(repoPath)
if err != nil {
return fmt.Errorf("failed to check tag signing config: %w", err)
}

if shouldSign {
if err := createSignedTag(repoPath, hash, version); err != nil {
return err
}
logrus.Debugf("signed tag %s created for %s", version, hash)
return nil
}

r, err := git.PlainOpen(repoPath)
if err != nil {
return err
Expand All @@ -190,3 +207,35 @@ func TagRelease(repoPath string, hash string, version string) error {
logrus.Debugf("tagged %s with %s", hash, version)
return nil
}

// isTagSigningEnabled returns true if git is configured to sign tags in any
// config scope. Checks tag.gpgSign and tag.forceSignAnnotated.
func isTagSigningEnabled(repoPath string) (bool, error) {
for _, key := range []string{"tag.gpgSign", "tag.forceSignAnnotated"} {
cmd := exec.Command("git", "-C", repoPath, "config", "--bool", "--get", key)
out, err := cmd.Output()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 {
// key is not set in any scope
continue
}
return false, err
}
if strings.TrimSpace(string(out)) == "true" {
return true, nil
}
}
return false, nil
}

// createSignedTag creates a signed, annotated tag by delegating to the git
// CLI, which handles GPG/SSH key lookup and passphrase prompting.
func createSignedTag(repoPath, hash, version string) error {
cmd := exec.Command("git", "-C", repoPath, "tag", "-s", "-m", version, version, hash)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("git tag -s failed: %s: %w", strings.TrimSpace(string(out)), err)
}
return nil
}
42 changes: 42 additions & 0 deletions vcs/tags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,52 @@ import (
"github.com/go-git/go-git/v5/plumbing/object"
"os"
"path"
"strings"
"testing"
"time"
)

func Test_isTagSigningEnabled(t *testing.T) {
tests := []struct {
name string
key string
value string
want bool
}{
{name: "unset", key: "", want: false},
{name: "tag.gpgSign=true", key: "tag.gpgSign", value: "true", want: true},
{name: "tag.gpgSign=false", key: "tag.gpgSign", value: "false", want: false},
{name: "tag.forceSignAnnotated=true", key: "tag.forceSignAnnotated", value: "true", want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repoDir := createTestRepo(t)
if tt.key != "" {
repo, err := git.PlainOpen(repoDir)
if err != nil {
t.Fatal(err)
}
cfg, err := repo.Config()
if err != nil {
t.Fatal(err)
}
cfg.Raw.Section("tag").SetOption(strings.TrimPrefix(tt.key, "tag."), tt.value)
if err := repo.SetConfig(cfg); err != nil {
t.Fatal(err)
}
}

got, err := isTagSigningEnabled(repoDir)
if err != nil {
t.Fatalf("isTagSigningEnabled() error = %v", err)
}
if got != tt.want {
t.Errorf("isTagSigningEnabled() = %v, want %v", got, tt.want)
}
})
}
}

func Test_getEndTag(t *testing.T) {
repoDir := createTestRepo(t)

Expand Down
Loading