Skip to content

Commit a7be038

Browse files
authored
feat: sign release tags when git signing is enabled (#5)
1 parent 65003e0 commit a7be038

2 files changed

Lines changed: 92 additions & 1 deletion

File tree

vcs/tags.go

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package vcs
22

33
import (
44
"errors"
5+
"fmt"
56
"github.com/go-git/go-git/v5"
67
"github.com/go-git/go-git/v5/plumbing"
78
"github.com/go-git/go-git/v5/plumbing/object"
89
"github.com/rogpeppe/go-internal/semver"
910
"github.com/sirupsen/logrus"
11+
"os/exec"
1012
"strings"
1113
"time"
1214
)
@@ -177,8 +179,23 @@ func compareSemantically(v *plumbing.Reference, w *plumbing.Reference) int {
177179
return semver.Compare(a, b)
178180
}
179181

180-
// TagRelease tags the repository with the given version.
182+
// TagRelease tags the repository with the given version. When git is
183+
// configured to sign tags (via tag.gpgSign or tag.forceSignAnnotated) an
184+
// annotated, signed tag is created instead of a lightweight one.
181185
func TagRelease(repoPath string, hash string, version string) error {
186+
shouldSign, err := isTagSigningEnabled(repoPath)
187+
if err != nil {
188+
return fmt.Errorf("failed to check tag signing config: %w", err)
189+
}
190+
191+
if shouldSign {
192+
if err := createSignedTag(repoPath, hash, version); err != nil {
193+
return err
194+
}
195+
logrus.Debugf("signed tag %s created for %s", version, hash)
196+
return nil
197+
}
198+
182199
r, err := git.PlainOpen(repoPath)
183200
if err != nil {
184201
return err
@@ -190,3 +207,35 @@ func TagRelease(repoPath string, hash string, version string) error {
190207
logrus.Debugf("tagged %s with %s", hash, version)
191208
return nil
192209
}
210+
211+
// isTagSigningEnabled returns true if git is configured to sign tags in any
212+
// config scope. Checks tag.gpgSign and tag.forceSignAnnotated.
213+
func isTagSigningEnabled(repoPath string) (bool, error) {
214+
for _, key := range []string{"tag.gpgSign", "tag.forceSignAnnotated"} {
215+
cmd := exec.Command("git", "-C", repoPath, "config", "--bool", "--get", key)
216+
out, err := cmd.Output()
217+
if err != nil {
218+
var exitErr *exec.ExitError
219+
if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 {
220+
// key is not set in any scope
221+
continue
222+
}
223+
return false, err
224+
}
225+
if strings.TrimSpace(string(out)) == "true" {
226+
return true, nil
227+
}
228+
}
229+
return false, nil
230+
}
231+
232+
// createSignedTag creates a signed, annotated tag by delegating to the git
233+
// CLI, which handles GPG/SSH key lookup and passphrase prompting.
234+
func createSignedTag(repoPath, hash, version string) error {
235+
cmd := exec.Command("git", "-C", repoPath, "tag", "-s", "-m", version, version, hash)
236+
out, err := cmd.CombinedOutput()
237+
if err != nil {
238+
return fmt.Errorf("git tag -s failed: %s: %w", strings.TrimSpace(string(out)), err)
239+
}
240+
return nil
241+
}

vcs/tags_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,52 @@ import (
55
"github.com/go-git/go-git/v5/plumbing/object"
66
"os"
77
"path"
8+
"strings"
89
"testing"
910
"time"
1011
)
1112

13+
func Test_isTagSigningEnabled(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
key string
17+
value string
18+
want bool
19+
}{
20+
{name: "unset", key: "", want: false},
21+
{name: "tag.gpgSign=true", key: "tag.gpgSign", value: "true", want: true},
22+
{name: "tag.gpgSign=false", key: "tag.gpgSign", value: "false", want: false},
23+
{name: "tag.forceSignAnnotated=true", key: "tag.forceSignAnnotated", value: "true", want: true},
24+
}
25+
for _, tt := range tests {
26+
t.Run(tt.name, func(t *testing.T) {
27+
repoDir := createTestRepo(t)
28+
if tt.key != "" {
29+
repo, err := git.PlainOpen(repoDir)
30+
if err != nil {
31+
t.Fatal(err)
32+
}
33+
cfg, err := repo.Config()
34+
if err != nil {
35+
t.Fatal(err)
36+
}
37+
cfg.Raw.Section("tag").SetOption(strings.TrimPrefix(tt.key, "tag."), tt.value)
38+
if err := repo.SetConfig(cfg); err != nil {
39+
t.Fatal(err)
40+
}
41+
}
42+
43+
got, err := isTagSigningEnabled(repoDir)
44+
if err != nil {
45+
t.Fatalf("isTagSigningEnabled() error = %v", err)
46+
}
47+
if got != tt.want {
48+
t.Errorf("isTagSigningEnabled() = %v, want %v", got, tt.want)
49+
}
50+
})
51+
}
52+
}
53+
1254
func Test_getEndTag(t *testing.T) {
1355
repoDir := createTestRepo(t)
1456

0 commit comments

Comments
 (0)