Skip to content

Commit ac0fdb3

Browse files
authored
refactor(bump): delegate single-module bumps to operations.BumpOperation (#227)
Unify version bump logic so single-module and multi-module paths share the same BumpOperation implementation.
1 parent be0c9fb commit ac0fdb3

10 files changed

Lines changed: 250 additions & 274 deletions

File tree

internal/commands/bump/bump_release_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ func TestCLI_BumpReleaseCommand_SaveVersionFails(t *testing.T) {
103103
t.Fatal("expected error due to save failure, got nil")
104104
}
105105

106-
if !strings.Contains(err.Error(), "failed to save version") {
107-
t.Errorf("expected error message to contain 'failed to save version', got: %v", err)
106+
if !strings.Contains(err.Error(), "failed to write version") {
107+
t.Errorf("expected error message to contain 'failed to write version', got: %v", err)
108108
}
109109
}
110110

internal/commands/bump/common.go

Lines changed: 28 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,31 @@ package bump
22

33
import (
44
"context"
5+
"fmt"
56

67
"github.com/indaco/sley/internal/clix"
78
"github.com/indaco/sley/internal/commands/depsync"
89
"github.com/indaco/sley/internal/config"
10+
"github.com/indaco/sley/internal/core"
911
"github.com/indaco/sley/internal/operations"
1012
"github.com/indaco/sley/internal/plugins"
1113
"github.com/indaco/sley/internal/semver"
1214
"github.com/urfave/cli/v3"
1315
)
1416

15-
// versionCalculator is a function that calculates the new version from the previous version.
16-
// It receives the previous version and returns the new version.
17-
type versionCalculator func(prev semver.SemVersion) semver.SemVersion
18-
1917
// bumpParams holds all parameters needed for a bump operation.
2018
type bumpParams struct {
2119
pre string
2220
meta string
2321
preserveMeta bool
2422
skipHooks bool
2523
bumpType string
26-
versionCalc versionCalculator
24+
opBumpType operations.BumpType
2725
}
2826

2927
// executeSingleModuleBump is the unified execution pipeline for single-module bump operations.
30-
// It handles all common logic: validation, hooks, update, and post-actions.
28+
// It delegates version calculation and writing to operations.BumpOperation,
29+
// the same implementation used by multi-module bumps, ensuring consistent behavior.
3130
func executeSingleModuleBump(
3231
ctx context.Context,
3332
cmd *cli.Command,
@@ -41,57 +40,55 @@ func executeSingleModuleBump(
4140
return err
4241
}
4342

44-
// Read current version
45-
previousVersion, err := semver.ReadVersion(execCtx.Path)
43+
// Create BumpOperation - the same path used by multi-module bumps
44+
fs := core.NewOSFileSystem()
45+
bumper := newVersionBumper()
46+
op := operations.NewBumpOperation(
47+
fs, bumper, params.opBumpType,
48+
params.pre, params.meta, params.preserveMeta,
49+
)
50+
51+
// Preview: calculate new version without writing
52+
result, err := op.Preview(ctx, execCtx.Path)
4653
if err != nil {
4754
return err
4855
}
4956

50-
// Calculate new version using the provided calculator
51-
newVersion := params.versionCalc(previousVersion)
52-
53-
// Apply pre-release and build metadata
54-
newVersion.Build = calculateNewBuild(params.meta, params.preserveMeta, previousVersion.Build)
55-
5657
// Run pre-bump extension hooks first - extensions may set up state that plugins need to validate
57-
if err := runPreBumpExtensionHooks(ctx, cfg, execCtx.Path, newVersion.String(), previousVersion.String(), params.bumpType, params.skipHooks); err != nil {
58+
if err := runPreBumpExtensionHooks(ctx, cfg, execCtx.Path, result.NewVersion.String(), result.PreviousVersion.String(), params.bumpType, params.skipHooks); err != nil {
5859
return err
5960
}
6061

61-
// Re-read the current version in case an extension modified the .version file
62-
// (e.g., an extension that fetches the latest release from GitHub and updates .version)
62+
// Re-preview in case an extension modified the .version file
6363
if !params.skipHooks {
64-
previousVersion, err = semver.ReadVersion(execCtx.Path)
64+
result, err = op.Preview(ctx, execCtx.Path)
6565
if err != nil {
6666
return err
6767
}
68-
// Recalculate the new version based on the potentially updated previous version
69-
newVersion = params.versionCalc(previousVersion)
70-
newVersion.Build = calculateNewBuild(params.meta, params.preserveMeta, previousVersion.Build)
7168
}
7269

7370
// Execute all pre-bump validations after extensions have run
74-
if err := executePreBumpValidations(registry, newVersion, previousVersion, params.bumpType); err != nil {
71+
if err := executePreBumpValidations(registry, result.NewVersion, result.PreviousVersion, params.bumpType); err != nil {
7572
return err
7673
}
7774

78-
// Update the version file
79-
if err := semver.UpdateVersion(execCtx.Path, params.bumpType, params.pre, params.meta, params.preserveMeta); err != nil {
80-
return err
75+
// Write the new version using BumpOperation
76+
if err := op.Write(ctx, execCtx.Path, result.NewVersion); err != nil {
77+
return fmt.Errorf("failed to write version: %w", err)
8178
}
8279

8380
// Execute all post-bump actions
84-
if err := executePostBumpActions(registry, newVersion, previousVersion, params.bumpType, execCtx.Path); err != nil {
81+
if err := executePostBumpActions(registry, result.NewVersion, result.PreviousVersion, params.bumpType, execCtx.Path); err != nil {
8582
return err
8683
}
8784

8885
// Run post-bump extension hooks
89-
if err := runPostBumpExtensionHooks(ctx, cfg, execCtx.Path, previousVersion.String(), params.bumpType, params.skipHooks); err != nil {
86+
if err := runPostBumpExtensionHooks(ctx, cfg, execCtx.Path, result.PreviousVersion.String(), params.bumpType, params.skipHooks); err != nil {
9087
return err
9188
}
9289

9390
// Commit (if auto-commit enabled) and create tag after successful bump
94-
return commitAndTagAfterBump(registry, newVersion, params.bumpType, execCtx.Path)
91+
return commitAndTagAfterBump(registry, result.NewVersion, params.bumpType, execCtx.Path)
9592
}
9693

9794
// executePreBumpValidations runs all validation checks before performing a bump.
@@ -135,50 +132,15 @@ func executePostBumpActions(registry *plugins.PluginRegistry, newVersion, previo
135132
return recordAuditLogEntry(registry, newVersion, previousVersion, bumpType)
136133
}
137134

138-
// makePatchCalculator returns a version calculator for patch bumps.
139-
func makePatchCalculator(pre, meta string, preserveMeta bool) versionCalculator {
140-
return func(prev semver.SemVersion) semver.SemVersion {
141-
next := prev
142-
next.Patch++
143-
next.PreRelease = pre
144-
next.Build = calculateNewBuild(meta, preserveMeta, prev.Build)
145-
return next
146-
}
147-
}
148-
149-
// makeMinorCalculator returns a version calculator for minor bumps.
150-
func makeMinorCalculator(pre, meta string, preserveMeta bool) versionCalculator {
151-
return func(prev semver.SemVersion) semver.SemVersion {
152-
next := prev
153-
next.Minor++
154-
next.Patch = 0
155-
next.PreRelease = pre
156-
next.Build = calculateNewBuild(meta, preserveMeta, prev.Build)
157-
return next
158-
}
159-
}
160-
161-
// makeMajorCalculator returns a version calculator for major bumps.
162-
func makeMajorCalculator(pre, meta string, preserveMeta bool) versionCalculator {
163-
return func(prev semver.SemVersion) semver.SemVersion {
164-
next := prev
165-
next.Major++
166-
next.Minor = 0
167-
next.Patch = 0
168-
next.PreRelease = pre
169-
next.Build = calculateNewBuild(meta, preserveMeta, prev.Build)
170-
return next
171-
}
172-
}
173-
174135
// extractBumpParams extracts common bump parameters from CLI command.
175-
func extractBumpParams(cmd *cli.Command, bumpType string) bumpParams {
136+
func extractBumpParams(cmd *cli.Command, bumpType string, opBumpType operations.BumpType) bumpParams {
176137
return bumpParams{
177138
pre: cmd.String("pre"),
178139
meta: cmd.String("meta"),
179140
preserveMeta: cmd.Bool("preserve-meta"),
180141
skipHooks: cmd.Bool("skip-hooks"),
181142
bumpType: bumpType,
143+
opBumpType: opBumpType,
182144
}
183145
}
184146

@@ -190,7 +152,6 @@ func executeStandardBump(
190152
cfg *config.Config,
191153
registry *plugins.PluginRegistry,
192154
params bumpParams,
193-
multiModuleOp operations.BumpType,
194155
) error {
195156
execCtx, err := clix.GetExecutionContext(ctx, cmd, cfg)
196157
if err != nil {
@@ -199,7 +160,7 @@ func executeStandardBump(
199160

200161
if !execCtx.IsSingleModule() {
201162
// Multi-module mode delegates to runMultiModuleBump
202-
return runMultiModuleBump(ctx, cmd, execCtx, registry, multiModuleOp, params.pre, params.meta, params.preserveMeta)
163+
return runMultiModuleBump(ctx, cmd, execCtx, registry, params.opBumpType, params.pre, params.meta, params.preserveMeta)
203164
}
204165

205166
// Single-module mode uses the unified executor

internal/commands/bump/major.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ func runBumpMajor(ctx context.Context, cmd *cli.Command, cfg *config.Config, reg
3434
return err
3535
}
3636

37-
params := extractBumpParams(cmd, "major")
38-
params.versionCalc = makeMajorCalculator(params.pre, params.meta, params.preserveMeta)
37+
params := extractBumpParams(cmd, "major", operations.BumpMajor)
3938

40-
return executeStandardBump(ctx, cmd, cfg, registry, params, operations.BumpMajor)
39+
return executeStandardBump(ctx, cmd, cfg, registry, params)
4140
}

internal/commands/bump/minor.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ func runBumpMinor(ctx context.Context, cmd *cli.Command, cfg *config.Config, reg
3434
return err
3535
}
3636

37-
params := extractBumpParams(cmd, "minor")
38-
params.versionCalc = makeMinorCalculator(params.pre, params.meta, params.preserveMeta)
37+
params := extractBumpParams(cmd, "minor", operations.BumpMinor)
3938

40-
return executeStandardBump(ctx, cmd, cfg, registry, params, operations.BumpMinor)
39+
return executeStandardBump(ctx, cmd, cfg, registry, params)
4140
}

internal/commands/bump/patch.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ func runBumpPatch(ctx context.Context, cmd *cli.Command, cfg *config.Config, reg
3434
return err
3535
}
3636

37-
params := extractBumpParams(cmd, "patch")
38-
params.versionCalc = makePatchCalculator(params.pre, params.meta, params.preserveMeta)
37+
params := extractBumpParams(cmd, "patch", operations.BumpPatch)
3938

40-
return executeStandardBump(ctx, cmd, cfg, registry, params, operations.BumpPatch)
39+
return executeStandardBump(ctx, cmd, cfg, registry, params)
4140
}

internal/commands/bump/pre.go

Lines changed: 11 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@ package bump
22

33
import (
44
"context"
5-
"fmt"
65

76
"github.com/indaco/sley/internal/cliflags"
87
"github.com/indaco/sley/internal/clix"
98
"github.com/indaco/sley/internal/config"
109
"github.com/indaco/sley/internal/hooks"
1110
"github.com/indaco/sley/internal/operations"
1211
"github.com/indaco/sley/internal/plugins"
13-
"github.com/indaco/sley/internal/semver"
1412
"github.com/urfave/cli/v3"
1513
)
1614

@@ -64,98 +62,18 @@ func runBumpPre(ctx context.Context, cmd *cli.Command, cfg *config.Config, regis
6462
return err
6563
}
6664

67-
// Handle single-module mode
68-
if execCtx.IsSingleModule() {
69-
return runSingleModulePreBump(ctx, cmd, cfg, registry, execCtx, label, meta, isPreserveMeta, isSkipHooks)
65+
if !execCtx.IsSingleModule() {
66+
return runMultiModuleBump(ctx, cmd, execCtx, registry, operations.BumpPre, label, meta, isPreserveMeta)
7067
}
7168

72-
// Handle multi-module mode
73-
// For pre-release bump, the label is used as the pre-release identifier
74-
return runMultiModuleBump(ctx, cmd, execCtx, registry, operations.BumpPre, label, meta, isPreserveMeta)
75-
}
76-
77-
// runSingleModulePreBump handles pre-release bump for single-module mode.
78-
func runSingleModulePreBump(ctx context.Context, cmd *cli.Command, cfg *config.Config, registry *plugins.PluginRegistry, execCtx *clix.ExecutionContext, label, meta string, isPreserveMeta, isSkipHooks bool) error {
79-
if _, err := clix.FromCommandFn(cmd); err != nil {
80-
return err
81-
}
82-
83-
previousVersion, err := semver.ReadVersion(execCtx.Path)
84-
if err != nil {
85-
return err
86-
}
87-
88-
// Calculate new version
89-
newVersion := previousVersion
90-
if label != "" {
91-
newVersion.PreRelease = semver.IncrementPreRelease(previousVersion.PreRelease, label)
92-
} else {
93-
if previousVersion.PreRelease == "" {
94-
return fmt.Errorf("current version has no pre-release; use --label to specify one")
95-
}
96-
base := extractPreReleaseBase(previousVersion.PreRelease)
97-
newVersion.PreRelease = semver.IncrementPreRelease(previousVersion.PreRelease, base)
98-
}
99-
newVersion.Build = calculateNewBuild(meta, isPreserveMeta, previousVersion.Build)
100-
101-
// Execute all pre-bump validations
102-
if err := executePreBumpValidations(registry, newVersion, previousVersion, "pre"); err != nil {
103-
return err
104-
}
105-
106-
if err := runPreBumpExtensionHooks(ctx, cfg, execCtx.Path, newVersion.String(), previousVersion.String(), "pre", isSkipHooks); err != nil {
107-
return err
108-
}
109-
110-
if err := semver.UpdatePreRelease(execCtx.Path, label, meta, isPreserveMeta); err != nil {
111-
return err
112-
}
113-
114-
// Execute all post-bump actions
115-
if err := executePostBumpActions(registry, newVersion, previousVersion, "pre", execCtx.Path); err != nil {
116-
return err
117-
}
118-
119-
if err := runPostBumpExtensionHooks(ctx, cfg, execCtx.Path, previousVersion.String(), "pre", isSkipHooks); err != nil {
120-
return err
69+
// Single-module: use the unified path via BumpOperation
70+
params := bumpParams{
71+
pre: label,
72+
meta: meta,
73+
preserveMeta: isPreserveMeta,
74+
skipHooks: isSkipHooks,
75+
bumpType: "pre",
76+
opBumpType: operations.BumpPre,
12177
}
122-
123-
// Create tag after successful bump
124-
return createTagAfterBump(registry, newVersion, "pre")
125-
}
126-
127-
// extractPreReleaseBase extracts the base label from a pre-release string.
128-
// e.g., "rc.1" -> "rc", "beta.2" -> "beta", "alpha" -> "alpha", "rc1" -> "rc"
129-
func extractPreReleaseBase(pre string) string {
130-
// First, check for dot followed by a number
131-
for i := len(pre) - 1; i >= 0; i-- {
132-
if pre[i] == '.' {
133-
// Check if everything after the dot is numeric
134-
suffix := pre[i+1:]
135-
isNumeric := true
136-
for _, c := range suffix {
137-
if c < '0' || c > '9' {
138-
isNumeric = false
139-
break
140-
}
141-
}
142-
if isNumeric && len(suffix) > 0 {
143-
return pre[:i]
144-
}
145-
}
146-
}
147-
148-
// Check for trailing digits without dot (e.g., "rc1" -> "rc")
149-
lastNonDigit := -1
150-
for i := len(pre) - 1; i >= 0; i-- {
151-
if pre[i] < '0' || pre[i] > '9' {
152-
lastNonDigit = i
153-
break
154-
}
155-
}
156-
if lastNonDigit >= 0 && lastNonDigit < len(pre)-1 {
157-
return pre[:lastNonDigit+1]
158-
}
159-
160-
return pre
78+
return executeSingleModuleBump(ctx, cmd, cfg, registry, execCtx, params)
16179
}

0 commit comments

Comments
 (0)