mirror of
https://github.com/lukaszraczylo/semver-generator.git
synced 2026-06-05 22:49:25 +00:00
fix(semver): skip non-semver tags when picking latest existing tag
Rolling tags like 'v1' (and any other tag that doesn't parse as x.y.z after prefix stripping) used to enter the latest-tag candidate set in CalculateSemver. When such a tag shared a commit with a real semver tag (e.g. v1 and v1.16.3 both pointing at the same release commit), go-git's alphabetical tag iteration made 'v1' win, ParseExistingSemver bailed out because '1' has only one component, and the calculator silently reset the baseline to 0.0.0 — producing nonsense like 0.0.5 on this branch. ListExistingTags now filters tags through a new IsParseableSemverTag helper before recording them, so non-semver tags never participate in latest-tag selection. The behavior change is invisible for repos that only use proper vX.Y.Z tags, and it's covered by a new test table.
This commit is contained in:
+1
-1
@@ -129,7 +129,7 @@ func main() {
|
|||||||
|
|
||||||
// List existing tags if needed
|
// List existing tags if needed
|
||||||
if params.varExisting || repo.Config.Force.Existing {
|
if params.varExisting || repo.Config.Force.Existing {
|
||||||
utils.ListExistingTags(&repo.GitRepo)
|
utils.ListExistingTags(&repo.GitRepo, repo.Config.TagPrefixes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply forced versioning
|
// Apply forced versioning
|
||||||
|
|||||||
+11
-6
@@ -163,10 +163,13 @@ func ListCommits(repo *GitRepository) ([]CommitDetails, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListExistingTags lists all tags in the repository
|
// ListExistingTags lists all tags in the repository
|
||||||
func ListExistingTags(repo *GitRepository) {
|
// ListExistingTags lists all tags in the repository.
|
||||||
|
// Tags that don't parse as proper semver (rolling tags like "v1" or "latest")
|
||||||
|
// are skipped so they can't out-rank real semver tags pointing to the same
|
||||||
|
// commit during latest-tag selection.
|
||||||
|
func ListExistingTags(repo *GitRepository, tagPrefixes []string) {
|
||||||
Debug("Listing existing tags", nil)
|
Debug("Listing existing tags", nil)
|
||||||
|
|
||||||
// Check if Handler is nil to avoid panic
|
|
||||||
if repo.Handler == nil {
|
if repo.Handler == nil {
|
||||||
Debug("Repository handler is nil, skipping tag listing", nil)
|
Debug("Repository handler is nil, skipping tag listing", nil)
|
||||||
return
|
return
|
||||||
@@ -180,15 +183,17 @@ func ListExistingTags(repo *GitRepository) {
|
|||||||
|
|
||||||
if err := refs.ForEach(func(ref *plumbing.Reference) error {
|
if err := refs.ForEach(func(ref *plumbing.Reference) error {
|
||||||
tagName := ref.Name().Short()
|
tagName := ref.Name().Short()
|
||||||
commitHash := ref.Hash().String()
|
|
||||||
|
|
||||||
// For annotated tags, dereference to get the actual commit hash
|
if !IsParseableSemverTag(tagName, tagPrefixes) {
|
||||||
|
Debug("Skipping non-semver tag", map[string]interface{}{"tag": tagName})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
commitHash := ref.Hash().String()
|
||||||
tagObj, err := repo.Handler.TagObject(ref.Hash())
|
tagObj, err := repo.Handler.TagObject(ref.Hash())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// This is an annotated tag - get the commit it points to
|
|
||||||
commitHash = tagObj.Target.String()
|
commitHash = tagObj.Target.String()
|
||||||
}
|
}
|
||||||
// If err != nil, it's a lightweight tag - ref.Hash() is already the commit hash
|
|
||||||
|
|
||||||
repo.Tags = append(repo.Tags, TagDetails{
|
repo.Tags = append(repo.Tags, TagDetails{
|
||||||
Name: tagName,
|
Name: tagName,
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ func TestListExistingTags(t *testing.T) {
|
|||||||
repo := &GitRepository{}
|
repo := &GitRepository{}
|
||||||
|
|
||||||
// Now we can safely call ListExistingTags since we've added a nil check
|
// Now we can safely call ListExistingTags since we've added a nil check
|
||||||
ListExistingTags(repo)
|
ListExistingTags(repo, nil)
|
||||||
|
|
||||||
// Verify no tags were added
|
// Verify no tags were added
|
||||||
assert.Empty(t, repo.Tags, "Should have no tags after calling with nil Handler")
|
assert.Empty(t, repo.Tags, "Should have no tags after calling with nil Handler")
|
||||||
|
|||||||
@@ -83,6 +83,29 @@ func StripTagPrefix(tagName string, prefixes []string) string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsParseableSemverTag reports whether tagName looks like a proper semver tag
|
||||||
|
// (X.Y.Z, optionally vX.Y.Z, optionally with -rc.N suffix) after configured
|
||||||
|
// prefixes are stripped. Rolling tags like "v1" or "latest" return false so the
|
||||||
|
// calculator can skip them when picking the latest existing tag — preventing a
|
||||||
|
// rolling tag tied to the same commit as a real semver tag from "winning" the
|
||||||
|
// alphabetical iteration in go-git and resetting the baseline to 0.0.0.
|
||||||
|
func IsParseableSemverTag(tagName string, prefixes []string) bool {
|
||||||
|
clean := StripTagPrefix(tagName, prefixes)
|
||||||
|
if idx := strings.Index(clean, "-rc."); idx != -1 {
|
||||||
|
clean = clean[:idx]
|
||||||
|
}
|
||||||
|
parts := strings.Split(clean, ".")
|
||||||
|
if len(parts) < 3 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, p := range parts[:3] {
|
||||||
|
if len(extractNumber.FindAllString(p, -1)) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// ParseExistingSemver parses a semantic version from a tag name
|
// ParseExistingSemver parses a semantic version from a tag name
|
||||||
func ParseExistingSemver(tagName string, currentSemver SemVer, prefixes []string) SemVer {
|
func ParseExistingSemver(tagName string, currentSemver SemVer, prefixes []string) SemVer {
|
||||||
Debug("Parsing existing semver", map[string]interface{}{"tag": tagName})
|
Debug("Parsing existing semver", map[string]interface{}{"tag": tagName})
|
||||||
|
|||||||
@@ -189,6 +189,34 @@ func TestParseExistingSemver(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func TestIsParseableSemverTag(t *testing.T) {
|
||||||
|
InitLogger(false)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
tag string
|
||||||
|
prefixes []string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "plain x.y.z", tag: "1.2.3", want: true},
|
||||||
|
{name: "v prefix", tag: "v1.16.5", want: true},
|
||||||
|
{name: "v prefix with rc", tag: "v2.0.0-rc.3", want: true},
|
||||||
|
{name: "configured app- prefix", tag: "app-1.2.3", prefixes: []string{"app-"}, want: true},
|
||||||
|
|
||||||
|
{name: "rolling v1 tag", tag: "v1", want: false},
|
||||||
|
{name: "rolling latest tag", tag: "latest", want: false},
|
||||||
|
{name: "two-component", tag: "1.2", want: false},
|
||||||
|
{name: "empty", tag: "", want: false},
|
||||||
|
{name: "non-numeric major", tag: "vX.Y.Z", want: false},
|
||||||
|
{name: "just text", tag: "release-day", want: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.want, IsParseableSemverTag(tt.tag, tt.prefixes))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCheckMatches(t *testing.T) {
|
func TestCheckMatches(t *testing.T) {
|
||||||
// Initialize logger for tests
|
// Initialize logger for tests
|
||||||
|
|||||||
Reference in New Issue
Block a user