Skip to content

Commit e101647

Browse files
authored
fix(tagmanager): commit modified files before creating tag (#214)
* fix(core): add GitCommitOperations interface for commit abstraction * fix(tagmanager): commit modified files before creating tag * fix(config): add commit-message-template to tag-manager config * fix(bump): commit files before tagging in auto-create flow * test(tagmanager): add tests for commit-before-tag behavior * docs(tagmanager): update tag-manager docs and example config
1 parent 1794a3b commit e101647

File tree

14 files changed

+1055
-62
lines changed

14 files changed

+1055
-62
lines changed

docs/plugins/examples/tag-manager.yaml

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,29 @@ path: .version
66
plugins:
77
tag-manager:
88
enabled: true
9-
# Set to true for automatic tagging during bump
10-
# Default is false - use `sley tag create` for manual workflow
11-
auto-create: false
9+
10+
# Tag settings
1211
prefix: "v"
1312
annotate: true
14-
push: false
1513
# Set to true to also create tags for pre-release versions
1614
tag-prereleases: false
1715

18-
# GPG Signing (optional)
19-
# Requires git to be configured with a GPG signing key
20-
sign: false
21-
signing-key: "" # Optional: specific GPG key ID (uses git default if empty)
16+
# Automatic tagging during bump
17+
# Default is false - use `sley tag create` for manual workflow
18+
auto-create: false
19+
push: false
2220

23-
# Custom Message Template (optional)
21+
# Custom Commit Message Template (optional)
22+
# Used when auto-create is true to format the commit message before tagging.
2423
# Available placeholders: {version}, {tag}, {prefix}, {date},
2524
# {major}, {minor}, {patch}, {prerelease}, {build}
25+
commit-message-template: "chore(release): {tag}"
26+
27+
# Custom Tag Message Template (optional)
28+
# Supports the same placeholders as commit-message-template.
2629
message-template: "Release {version}"
30+
31+
# GPG Signing (optional)
32+
# Requires git to be configured with a GPG signing key
33+
sign: false
34+
signing-key: "" # Optional: specific GPG key ID (uses git default if empty)
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
package bump
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"testing"
7+
8+
"github.com/indaco/sley/internal/plugins"
9+
"github.com/indaco/sley/internal/plugins/tagmanager"
10+
"github.com/indaco/sley/internal/semver"
11+
)
12+
13+
/* ------------------------------------------------------------------------- */
14+
/* COMMIT AND TAG AFTER BUMP TESTS */
15+
/* ------------------------------------------------------------------------- */
16+
17+
// newTestTagManagerPlugin creates a TagManagerPlugin with mock ops for testing.
18+
func newTestTagManagerPlugin(cfg *tagmanager.Config, gitOps *tagmanager.MockGitTagOperations, commitOps *tagmanager.MockGitCommitOperations) *tagmanager.TagManagerPlugin {
19+
return tagmanager.NewTagManagerWithOps(cfg, gitOps, commitOps)
20+
}
21+
22+
func TestCommitAndTagAfterBump_AutoCreateDisabled(t *testing.T) {
23+
version := semver.SemVersion{Major: 1, Minor: 2, Patch: 3}
24+
plugin := newTestTagManagerPlugin(&tagmanager.Config{
25+
Enabled: true,
26+
AutoCreate: false,
27+
Prefix: "v",
28+
}, &tagmanager.MockGitTagOperations{}, &tagmanager.MockGitCommitOperations{})
29+
30+
registry := plugins.NewPluginRegistry()
31+
if err := registry.RegisterTagManager(plugin); err != nil {
32+
t.Fatalf("failed to register tag manager: %v", err)
33+
}
34+
35+
err := commitAndTagAfterBump(registry, version, "patch", "")
36+
if err != nil {
37+
t.Errorf("expected nil error for disabled auto-create, got %v", err)
38+
}
39+
}
40+
41+
func TestCommitAndTagAfterBump_Success(t *testing.T) {
42+
version := semver.SemVersion{Major: 1, Minor: 2, Patch: 3}
43+
var stagedFiles []string
44+
var commitMsg string
45+
var createdTag string
46+
47+
plugin := newTestTagManagerPlugin(&tagmanager.Config{
48+
Enabled: true,
49+
AutoCreate: true,
50+
Prefix: "v",
51+
Annotate: true,
52+
}, &tagmanager.MockGitTagOperations{
53+
TagExistsFn: func(name string) (bool, error) { return false, nil },
54+
CreateAnnotatedTagFn: func(name, message string) error {
55+
createdTag = name
56+
return nil
57+
},
58+
}, &tagmanager.MockGitCommitOperations{
59+
GetModifiedFilesFn: func() ([]string, error) { return []string{}, nil },
60+
StageFilesFn: func(files ...string) error {
61+
stagedFiles = append(stagedFiles, files...)
62+
return nil
63+
},
64+
CommitFn: func(message string) error {
65+
commitMsg = message
66+
return nil
67+
},
68+
})
69+
70+
registry := plugins.NewPluginRegistry()
71+
if err := registry.RegisterTagManager(plugin); err != nil {
72+
t.Fatalf("failed to register tag manager: %v", err)
73+
}
74+
75+
err := commitAndTagAfterBump(registry, version, "patch", ".version")
76+
if err != nil {
77+
t.Errorf("expected nil error, got %v", err)
78+
}
79+
if len(stagedFiles) != 1 || stagedFiles[0] != ".version" {
80+
t.Errorf("expected staged files [.version], got %v", stagedFiles)
81+
}
82+
if commitMsg != "chore(release): v1.2.3" {
83+
t.Errorf("expected commit message %q, got %q", "chore(release): v1.2.3", commitMsg)
84+
}
85+
if createdTag != "v1.2.3" {
86+
t.Errorf("expected tag v1.2.3, got %q", createdTag)
87+
}
88+
}
89+
90+
func TestCommitAndTagAfterBump_WithModifiedFiles(t *testing.T) {
91+
version := semver.SemVersion{Major: 1, Minor: 2, Patch: 3}
92+
var stagedFiles []string
93+
94+
plugin := newTestTagManagerPlugin(&tagmanager.Config{
95+
Enabled: true,
96+
AutoCreate: true,
97+
Prefix: "v",
98+
Annotate: true,
99+
}, &tagmanager.MockGitTagOperations{
100+
TagExistsFn: func(name string) (bool, error) { return false, nil },
101+
CreateAnnotatedTagFn: func(name, message string) error { return nil },
102+
}, &tagmanager.MockGitCommitOperations{
103+
GetModifiedFilesFn: func() ([]string, error) {
104+
return []string{"CHANGELOG.md", "package.json"}, nil
105+
},
106+
StageFilesFn: func(files ...string) error {
107+
stagedFiles = append(stagedFiles, files...)
108+
return nil
109+
},
110+
CommitFn: func(message string) error { return nil },
111+
})
112+
113+
registry := plugins.NewPluginRegistry()
114+
if err := registry.RegisterTagManager(plugin); err != nil {
115+
t.Fatalf("failed to register tag manager: %v", err)
116+
}
117+
118+
err := commitAndTagAfterBump(registry, version, "patch", ".version")
119+
if err != nil {
120+
t.Errorf("expected nil error, got %v", err)
121+
}
122+
// Should stage bumpedPath + auto-detected modified files
123+
if len(stagedFiles) != 3 {
124+
t.Errorf("expected 3 staged files, got %d: %v", len(stagedFiles), stagedFiles)
125+
}
126+
}
127+
128+
func TestCommitAndTagAfterBump_CommitFails(t *testing.T) {
129+
version := semver.SemVersion{Major: 1, Minor: 2, Patch: 3}
130+
plugin := newTestTagManagerPlugin(&tagmanager.Config{
131+
Enabled: true,
132+
AutoCreate: true,
133+
Prefix: "v",
134+
}, &tagmanager.MockGitTagOperations{}, &tagmanager.MockGitCommitOperations{
135+
GetModifiedFilesFn: func() ([]string, error) { return []string{}, nil },
136+
StageFilesFn: func(files ...string) error { return nil },
137+
CommitFn: func(message string) error { return fmt.Errorf("commit failed") },
138+
})
139+
140+
registry := plugins.NewPluginRegistry()
141+
if err := registry.RegisterTagManager(plugin); err != nil {
142+
t.Fatalf("failed to register tag manager: %v", err)
143+
}
144+
145+
err := commitAndTagAfterBump(registry, version, "patch", ".version")
146+
if err == nil {
147+
t.Error("expected error, got nil")
148+
}
149+
if !strings.Contains(err.Error(), "failed to commit release changes") {
150+
t.Errorf("expected commit error, got: %v", err)
151+
}
152+
}
153+
154+
func TestCommitAndTagAfterBump_TagCreationFails(t *testing.T) {
155+
version := semver.SemVersion{Major: 1, Minor: 2, Patch: 3}
156+
plugin := newTestTagManagerPlugin(&tagmanager.Config{
157+
Enabled: true,
158+
AutoCreate: true,
159+
Prefix: "v",
160+
Annotate: true,
161+
}, &tagmanager.MockGitTagOperations{
162+
TagExistsFn: func(name string) (bool, error) { return false, nil },
163+
CreateAnnotatedTagFn: func(name, message string) error { return fmt.Errorf("tag creation failed") },
164+
}, &tagmanager.MockGitCommitOperations{
165+
GetModifiedFilesFn: func() ([]string, error) { return []string{}, nil },
166+
StageFilesFn: func(files ...string) error { return nil },
167+
CommitFn: func(message string) error { return nil },
168+
})
169+
170+
registry := plugins.NewPluginRegistry()
171+
if err := registry.RegisterTagManager(plugin); err != nil {
172+
t.Fatalf("failed to register tag manager: %v", err)
173+
}
174+
175+
err := commitAndTagAfterBump(registry, version, "patch", ".version")
176+
if err == nil {
177+
t.Error("expected error, got nil")
178+
}
179+
if !strings.Contains(err.Error(), "failed to create tag") {
180+
t.Errorf("expected tag creation error, got: %v", err)
181+
}
182+
}
183+
184+
func TestCommitAndTagAfterBump_WithoutBumpedPath(t *testing.T) {
185+
version := semver.SemVersion{Major: 1, Minor: 2, Patch: 3}
186+
plugin := newTestTagManagerPlugin(&tagmanager.Config{
187+
Enabled: true,
188+
AutoCreate: true,
189+
Prefix: "v",
190+
Annotate: true,
191+
}, &tagmanager.MockGitTagOperations{
192+
TagExistsFn: func(name string) (bool, error) { return false, nil },
193+
CreateAnnotatedTagFn: func(name, message string) error { return nil },
194+
}, &tagmanager.MockGitCommitOperations{
195+
GetModifiedFilesFn: func() ([]string, error) { return []string{".version"}, nil },
196+
StageFilesFn: func(files ...string) error { return nil },
197+
CommitFn: func(message string) error { return nil },
198+
})
199+
200+
registry := plugins.NewPluginRegistry()
201+
if err := registry.RegisterTagManager(plugin); err != nil {
202+
t.Fatalf("failed to register tag manager: %v", err)
203+
}
204+
205+
// Call with empty bumpedPath (via createTagAfterBump)
206+
err := createTagAfterBump(registry, version, "patch")
207+
if err != nil {
208+
t.Errorf("expected nil error, got %v", err)
209+
}
210+
}
211+
212+
func TestCommitAndTagAfterBump_WithPush(t *testing.T) {
213+
version := semver.SemVersion{Major: 1, Minor: 2, Patch: 3}
214+
var pushedTag string
215+
216+
plugin := newTestTagManagerPlugin(&tagmanager.Config{
217+
Enabled: true,
218+
AutoCreate: true,
219+
Prefix: "v",
220+
Annotate: true,
221+
Push: true,
222+
}, &tagmanager.MockGitTagOperations{
223+
TagExistsFn: func(name string) (bool, error) { return false, nil },
224+
CreateAnnotatedTagFn: func(name, message string) error { return nil },
225+
PushTagFn: func(name string) error {
226+
pushedTag = name
227+
return nil
228+
},
229+
}, &tagmanager.MockGitCommitOperations{
230+
GetModifiedFilesFn: func() ([]string, error) { return []string{}, nil },
231+
StageFilesFn: func(files ...string) error { return nil },
232+
CommitFn: func(message string) error { return nil },
233+
})
234+
235+
registry := plugins.NewPluginRegistry()
236+
if err := registry.RegisterTagManager(plugin); err != nil {
237+
t.Fatalf("failed to register tag manager: %v", err)
238+
}
239+
240+
err := commitAndTagAfterBump(registry, version, "patch", ".version")
241+
if err != nil {
242+
t.Errorf("expected nil error, got %v", err)
243+
}
244+
if pushedTag != "v1.2.3" {
245+
t.Errorf("expected pushed tag v1.2.3, got %q", pushedTag)
246+
}
247+
}
248+
249+
func TestCommitAndTagAfterBump_CustomCommitMessageTemplate(t *testing.T) {
250+
version := semver.SemVersion{Major: 1, Minor: 2, Patch: 3}
251+
var commitMsg string
252+
253+
plugin := newTestTagManagerPlugin(&tagmanager.Config{
254+
Enabled: true,
255+
AutoCreate: true,
256+
Prefix: "v",
257+
Annotate: true,
258+
CommitMessageTemplate: "release: bump to {version}",
259+
}, &tagmanager.MockGitTagOperations{
260+
TagExistsFn: func(name string) (bool, error) { return false, nil },
261+
CreateAnnotatedTagFn: func(name, message string) error { return nil },
262+
}, &tagmanager.MockGitCommitOperations{
263+
GetModifiedFilesFn: func() ([]string, error) { return []string{}, nil },
264+
StageFilesFn: func(files ...string) error { return nil },
265+
CommitFn: func(message string) error {
266+
commitMsg = message
267+
return nil
268+
},
269+
})
270+
271+
registry := plugins.NewPluginRegistry()
272+
if err := registry.RegisterTagManager(plugin); err != nil {
273+
t.Fatalf("failed to register tag manager: %v", err)
274+
}
275+
276+
err := commitAndTagAfterBump(registry, version, "patch", ".version")
277+
if err != nil {
278+
t.Errorf("expected nil error, got %v", err)
279+
}
280+
if commitMsg != "release: bump to 1.2.3" {
281+
t.Errorf("expected commit message %q, got %q", "release: bump to 1.2.3", commitMsg)
282+
}
283+
}

internal/commands/bump/bump_plugins_test.go

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -328,23 +328,6 @@ func TestCreateTagAfterBump_Enabled(t *testing.T) {
328328

329329
version := semver.SemVersion{Major: 1, Minor: 2, Patch: 3}
330330

331-
t.Run("enabled plugin creates tag successfully", func(t *testing.T) {
332-
plugin := tagmanager.NewTagManager(&tagmanager.Config{
333-
Enabled: true,
334-
AutoCreate: true,
335-
Prefix: "v",
336-
})
337-
tagmanager.GetTagManagerFn = func() tagmanager.TagManager { return plugin }
338-
339-
// Mock CreateTag to succeed
340-
registry := plugins.NewPluginRegistry()
341-
err := createTagAfterBump(registry, version, "patch")
342-
// This will fail in test environment without git, but we're testing the code path
343-
if err != nil && !strings.Contains(err.Error(), "failed to create tag") {
344-
t.Errorf("unexpected error type: %v", err)
345-
}
346-
})
347-
348331
t.Run("disabled plugin returns nil", func(t *testing.T) {
349332
registry := plugins.NewPluginRegistry()
350333
plugin := tagmanager.NewTagManager(&tagmanager.Config{

internal/commands/bump/common.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ func executeSingleModuleBump(
9090
return err
9191
}
9292

93-
// Create tag after successful bump
94-
return createTagAfterBump(registry, newVersion, params.bumpType)
93+
// Commit (if auto-commit enabled) and create tag after successful bump
94+
return commitAndTagAfterBump(registry, newVersion, params.bumpType, execCtx.Path)
9595
}
9696

9797
// executePreBumpValidations runs all validation checks before performing a bump.

internal/commands/bump/helpers.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ func validateTagAvailable(registry *plugins.PluginRegistry, version semver.SemVe
102102

103103
// createTagAfterBump creates a git tag for the version if tag manager is enabled.
104104
func createTagAfterBump(registry *plugins.PluginRegistry, version semver.SemVersion, bumpType string) error {
105+
return commitAndTagAfterBump(registry, version, bumpType, "")
106+
}
107+
108+
// commitAndTagAfterBump commits bump-modified files and creates a git tag.
109+
// When auto-create is enabled, it stages and commits the bumpedPath and any other
110+
// modified files before creating the tag so the tag points to the correct release commit.
111+
func commitAndTagAfterBump(registry *plugins.PluginRegistry, version semver.SemVersion, bumpType string, bumpedPath string) error {
105112
tm := registry.GetTagManager()
106113
if tm == nil {
107114
return nil
@@ -113,6 +120,17 @@ func createTagAfterBump(registry *plugins.PluginRegistry, version semver.SemVers
113120
return nil
114121
}
115122

123+
// Commit bump-modified files before creating the tag
124+
var extraFiles []string
125+
if bumpedPath != "" {
126+
extraFiles = []string{bumpedPath}
127+
}
128+
if err := plugin.CommitChanges(version, extraFiles); err != nil {
129+
return fmt.Errorf("failed to commit release changes: %w", err)
130+
}
131+
printer.PrintSuccess(fmt.Sprintf("Committed release changes for %s", version.String()))
132+
133+
// Create tag on the new commit
116134
message := fmt.Sprintf("Release %s (%s bump)", version.String(), bumpType)
117135
if err := tm.CreateTag(version, message); err != nil {
118136
return fmt.Errorf("failed to create tag: %w", err)

0 commit comments

Comments
 (0)