| | | 1 | | package app |
| | | 2 | | |
| | | 3 | | import ( |
| | | 4 | | "fmt" |
| | | 5 | | |
| | | 6 | | "github.com/jedi-knights/go-semantic-release/internal/domain" |
| | | 7 | | "github.com/jedi-knights/go-semantic-release/internal/ports" |
| | | 8 | | ) |
| | | 9 | | |
| | | 10 | | // Compile-time interface compliance check. |
| | | 11 | | var _ ports.VersionCalculator = (*VersionCalculatorService)(nil) |
| | | 12 | | |
| | | 13 | | // VersionCalculatorService implements ports.VersionCalculator. |
| | | 14 | | type VersionCalculatorService struct{} |
| | | 15 | | |
| | | 16 | | // NewVersionCalculatorService creates a new version calculator. |
| | | 17 | | func NewVersionCalculatorService() *VersionCalculatorService { |
| | | 18 | | return &VersionCalculatorService{} |
| | | 19 | | } |
| | | 20 | | |
| | | 21 | | func (s *VersionCalculatorService) Calculate( |
| | | 22 | | current domain.Version, |
| | | 23 | | commits []domain.Commit, |
| | | 24 | | policy *domain.BranchPolicy, |
| | | 25 | | typeMapping map[string]domain.ReleaseType, |
| | | 26 | | prereleaseCounter int, |
| | 16 | 27 | | ) (domain.Version, domain.ReleaseType, error) { |
| | 16 | 28 | | bump := aggregateBump(commits, typeMapping) |
| | 2 | 29 | | if !bump.IsReleasable() { |
| | 2 | 30 | | return current, domain.ReleaseNone, nil |
| | 2 | 31 | | } |
| | | 32 | | |
| | | 33 | | // For maintenance branches, constrain the allowed bump type. |
| | 5 | 34 | | if policy != nil && policy.IsMaintenance() { |
| | 5 | 35 | | original := bump |
| | 5 | 36 | | bump = constrainMaintenanceBump(bump, policy) |
| | 4 | 37 | | if !bump.IsReleasable() { |
| | 4 | 38 | | return current, domain.ReleaseNone, |
| | 4 | 39 | | fmt.Errorf("commit requires %s bump but maintenance branch %q does not allow it", |
| | 4 | 40 | | original, policy.Name) |
| | 4 | 41 | | } |
| | | 42 | | } |
| | | 43 | | |
| | 10 | 44 | | next := current.Bump(bump) |
| | 10 | 45 | | |
| | 10 | 46 | | // Validate maintenance range. |
| | 1 | 47 | | if policy != nil && policy.IsMaintenance() { |
| | 0 | 48 | | if err := domain.ValidateMaintenanceVersion(next, *policy); err != nil { |
| | 0 | 49 | | return current, domain.ReleaseNone, err |
| | 0 | 50 | | } |
| | | 51 | | } |
| | | 52 | | |
| | | 53 | | // Apply prerelease identifier. |
| | 4 | 54 | | if policy != nil && policy.IsPrerelease() { |
| | 4 | 55 | | pre := buildPrereleaseID(policy.Channel, prereleaseCounter) |
| | 4 | 56 | | next = next.WithPrerelease(pre) |
| | 4 | 57 | | } |
| | | 58 | | |
| | 10 | 59 | | return next, bump, nil |
| | | 60 | | } |
| | | 61 | | |
| | | 62 | | // constrainMaintenanceBump limits the bump type based on the maintenance range. |
| | | 63 | | // A "N.N.x" range only allows patch bumps; "N.x" allows patch and minor. |
| | | 64 | | // |
| | | 65 | | // NOTE: for "N.x" ranges this function permits minor bumps but does not verify |
| | | 66 | | // that the resulting version stays within the major boundary. That upper-bound |
| | | 67 | | // check is performed by ValidateMaintenanceVersion (called by Calculate after |
| | | 68 | | // Bump). Callers must invoke both functions in sequence; calling this function |
| | | 69 | | // alone is not sufficient to enforce the full maintenance constraint. |
| | | 70 | | func constrainMaintenanceBump(bump domain.ReleaseType, policy *domain.BranchPolicy) domain.ReleaseType { |
| | | 71 | | _, maxVer, err := policy.MaintenanceRange() |
| | | 72 | | if err != nil { |
| | | 73 | | return domain.ReleaseNone |
| | | 74 | | } |
| | | 75 | | |
| | | 76 | | // Major bumps are never allowed on any maintenance branch. |
| | | 77 | | if bump == domain.ReleaseMajor { |
| | | 78 | | return domain.ReleaseNone |
| | | 79 | | } |
| | | 80 | | |
| | | 81 | | // "N.N.x" range (max differs only in minor): only patch is allowed. |
| | | 82 | | // "N.x" range (max differs in major): patch and minor are both allowed. |
| | | 83 | | if maxVer.Minor > 0 && maxVer.Patch == 0 { |
| | | 84 | | // Range like "1.2.x" → max is "1.3.0" → only patch allowed. |
| | | 85 | | if bump > domain.ReleasePatch { |
| | | 86 | | return domain.ReleaseNone |
| | | 87 | | } |
| | | 88 | | } |
| | | 89 | | |
| | | 90 | | return bump |
| | | 91 | | } |
| | | 92 | | |
| | | 93 | | func aggregateBump(commits []domain.Commit, typeMapping map[string]domain.ReleaseType) domain.ReleaseType { |
| | | 94 | | highest := domain.ReleaseNone |
| | | 95 | | for i := range commits { |
| | | 96 | | rt := commits[i].ReleaseType(typeMapping) |
| | | 97 | | highest = highest.Higher(rt) |
| | | 98 | | } |
| | | 99 | | return highest |
| | | 100 | | } |
| | | 101 | | |
| | | 102 | | func buildPrereleaseID(channel string, counter int) string { |
| | | 103 | | if channel == "" { |
| | | 104 | | channel = "pre" |
| | | 105 | | } |
| | | 106 | | if counter < 0 { |
| | | 107 | | counter = 0 |
| | | 108 | | } |
| | | 109 | | return fmt.Sprintf("%s.%d", channel, counter) |
| | | 110 | | } |