From d141fe2c0cd89817fc443a5a09e7f1a60e423b14 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 7 Jun 2026 05:29:17 +0000 Subject: [PATCH 1/4] feat(tag): remove --project, resolve from CWD, add --major/--minor/--patch bump - Remove --project flag; resolve project alias from CWD via project.ResolveProjectAlias - Add --major, --minor, --patch flags for automatic version bumping - Preserve +suffix (e.g. +0.74.1) from latest tag when bumping - Positional version arg still works for explicit tags - Reject pre-release tags for bump (must be vMAJOR.MINOR.PATCH[+suffix]) --- cmd/tag.go | 131 ++++++++++++++++++++++++++++++---- cmd/tag_test.go | 185 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 303 insertions(+), 13 deletions(-) diff --git a/cmd/tag.go b/cmd/tag.go index f392e854..793f3688 100644 --- a/cmd/tag.go +++ b/cmd/tag.go @@ -2,7 +2,11 @@ package cmd import ( "fmt" + "os" + "os/exec" "regexp" + "strconv" + "strings" "github.com/spf13/cobra" "github.com/tta-lab/ttal-cli/internal/daemon" @@ -14,31 +18,70 @@ import ( // to keep validation simple — use dots as separators: v1.0.0-rc.1 not v1.0.0-rc-1). var semverRe = regexp.MustCompile(`^v\d+\.\d+\.\d+(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?(\+[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?$`) +// semverBaseRe captures vMAJOR.MINOR.PATCH and optional +suffix from a tag. +// Groups: 1=full base (v1.2.3), 2=MAJOR, 3=MINOR, 4=PATCH, 5=+suffix (including +). +var semverBaseRe = regexp.MustCompile(`^v(\d+)\.(\d+)\.(\d+)(\+[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?$`) + var tagCmd = &cobra.Command{ - Use: "tag --project ", + Use: "tag [ | --major | --minor | --patch]", Short: "Create and push a git tag via daemon (no credentials needed in worker)", Long: `Creates a lightweight git tag and pushes it to origin through the daemon. The daemon injects credentials — workers don't need tokens in their environment. -The tag must be a valid semver version prefixed with 'v' (e.g. v1.0.0, v2.1.0-rc.1). +Resolves the project from the current working directory. No --project flag needed. + +With --major, --minor, or --patch, automatically bumps the largest existing version +tag in the repo. Existing +suffix (e.g. +0.74.1) is preserved on bump. + +With a positional version argument, tags that exact version directly. Examples: - ttal tag v1.0.0 --project ttal-cli - ttal tag v2.1.0-rc.1 --project fn-agent - ttal tag v1.1.1-guion.1 --project fn-cli`, - Args: cobra.ExactArgs(1), + ttal tag --patch # v1.2.3 → v1.2.4 + ttal tag --minor # v1.2.3 → v1.3.0 + ttal tag v2.0.0 # explicit version + ttal tag v1.6.1+0.74.1 # explicit with +suffix`, + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - tag := args[0] - if !semverRe.MatchString(tag) { - return fmt.Errorf("invalid semver tag %q — expected format: v1.0.0, v2.1.0-rc.1", tag) + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("getwd: %w", err) } - projectAlias, _ := cmd.Flags().GetString("project") + projectAlias := project.ResolveProjectAlias(cwd) + if projectAlias == "" { + return fmt.Errorf("current directory %q is not under a registered ttal project", cwd) + } projectPath, err := project.GetProjectPath(projectAlias) if err != nil { return err } + major, _ := cmd.Flags().GetBool("major") + minor, _ := cmd.Flags().GetBool("minor") + patch, _ := cmd.Flags().GetBool("patch") + bump := major || minor || patch + + var tag string + + if bump && len(args) > 0 { + return fmt.Errorf("--major/--minor/--patch and a positional version are mutually exclusive") + } + + if bump { + tag, err = computeBumpedTag(projectPath, major, minor) + if err != nil { + return err + } + } else { + if len(args) == 0 { + return fmt.Errorf("either a version argument or one of --major/--minor/--patch is required") + } + tag = args[0] + if !semverRe.MatchString(tag) { + return fmt.Errorf("invalid semver tag %q — expected format: v1.0.0, v2.1.0-rc.1", tag) + } + } + resp, err := daemon.GitTag(daemon.GitTagRequest{ WorkDir: projectPath, Tag: tag, @@ -57,7 +100,71 @@ Examples: } func init() { - tagCmd.Flags().StringP("project", "p", "", "project alias (required)") - _ = tagCmd.MarkFlagRequired("project") + tagCmd.Flags().Bool("major", false, "bump major version") + tagCmd.Flags().Bool("minor", false, "bump minor version") + tagCmd.Flags().Bool("patch", false, "bump patch version") rootCmd.AddCommand(tagCmd) } + +// latestTag returns the largest semver tag in the repo, or "" if none exist. +func latestTag(workDir string) (string, error) { + cmd := exec.Command("git", "-C", workDir, "tag", "--sort=-version:refname") + out, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("git tag: %w", err) + } + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + if len(lines) == 0 || lines[0] == "" { + return "", nil + } + return lines[0], nil +} + +const ( + initialMajor = "v1.0.0" + initialMinor = "v0.1.0" + initialPatch = "v0.0.1" +) + +// computeBumpedTag finds the largest tag, bumps the specified segment, and returns the new tag. +// The +suffix from the latest tag is preserved. +func computeBumpedTag(workDir string, major, minor bool) (string, error) { + latest, err := latestTag(workDir) + if err != nil { + return "", err + } + if latest == "" { + if major { + return initialMajor, nil + } + if minor { + return initialMinor, nil + } + return initialPatch, nil + } + + matches := semverBaseRe.FindStringSubmatch(latest) + if matches == nil { + return "", fmt.Errorf( + "latest tag %q is not a plain semver with optional +suffix (has pre-release: %s)", + latest, latest) + } + + maj, _ := strconv.Atoi(matches[1]) + min, _ := strconv.Atoi(matches[2]) + pat, _ := strconv.Atoi(matches[3]) + suffix := matches[4] // includes leading +, or "" if absent + + if major { + maj++ + min = 0 + pat = 0 + } else if minor { + min++ + pat = 0 + } else { + pat++ + } + + return fmt.Sprintf("v%d.%d.%d%s", maj, min, pat, suffix), nil +} diff --git a/cmd/tag_test.go b/cmd/tag_test.go index 961a450d..c9753666 100644 --- a/cmd/tag_test.go +++ b/cmd/tag_test.go @@ -1,6 +1,10 @@ package cmd -import "testing" +import ( + "os" + "os/exec" + "testing" +) func TestSemverRe(t *testing.T) { tests := []struct { @@ -40,3 +44,182 @@ func TestSemverRe(t *testing.T) { }) } } + +func TestSemverBaseRe(t *testing.T) { + tests := []struct { + tag string + matches bool + major string + minor string + patch string + suffix string + }{ + {"v1.0.0", true, "1", "0", "0", ""}, + {"v0.1.0", true, "0", "1", "0", ""}, + {"v10.20.30", true, "10", "20", "30", ""}, + {"v1.0.0+build.123", true, "1", "0", "0", "+build.123"}, + {"v1.6.1+0.74.1", true, "1", "6", "1", "+0.74.1"}, + {"v1.0.0-rc.1", false, "", "", "", ""}, // pre-release not matched + {"v1.0.0-rc.1+build.456", false, "", "", "", ""}, + } + + for _, tt := range tests { + t.Run(tt.tag, func(t *testing.T) { + matches := semverBaseRe.FindStringSubmatch(tt.tag) + if tt.matches && matches == nil { + t.Fatalf("semverBaseRe.FindStringSubmatch(%q) = nil, want match", tt.tag) + } + if !tt.matches && matches != nil { + t.Fatalf("semverBaseRe.FindStringSubmatch(%q) = %v, want no match", tt.tag, matches) + } + if matches != nil { + if matches[1] != tt.major { + t.Errorf("major: got %q, want %q", matches[1], tt.major) + } + if matches[2] != tt.minor { + t.Errorf("minor: got %q, want %q", matches[2], tt.minor) + } + if matches[3] != tt.patch { + t.Errorf("patch: got %q, want %q", matches[3], tt.patch) + } + if matches[4] != tt.suffix { + t.Errorf("suffix: got %q, want %q", matches[4], tt.suffix) + } + } + }) + } +} + +func testBumpedTag(t *testing.T, workDir string, major, minor bool, want string) { + t.Helper() + got, err := computeBumpedTag(workDir, major, minor) + if err != nil { + t.Fatalf("computeBumpedTag(%v, %v): %v", major, minor, err) + } + if got != want { + t.Errorf("computeBumpedTag(%v, %v) = %q, want %q", major, minor, got, want) + } +} + +func testBumpedTagError(t *testing.T, workDir string, major, minor bool) { + t.Helper() + got, err := computeBumpedTag(workDir, major, minor) + if err == nil { + t.Errorf("expected error for computeBumpedTag(%v, %v), got %q", major, minor, got) + } + if got != "" { + t.Errorf("expected empty tag on error, got %q", got) + } +} + +func TestComputeBumpedTagNoTags(t *testing.T) { + dir, gitDir := setupBumpTestRepo(t) + defer func() { _ = os.RemoveAll(dir) }() + _ = gitDir + + testBumpedTag(t, dir, false, false, "v0.0.1") + testBumpedTag(t, dir, false, true, "v0.1.0") + testBumpedTag(t, dir, true, false, "v1.0.0") +} + +func TestComputeBumpedTagSimple(t *testing.T) { + dir := setupBumpTestRepoWithTag(t, "v1.2.3") + defer func() { _ = os.RemoveAll(dir) }() + + testBumpedTag(t, dir, false, false, "v1.2.4") + testBumpedTag(t, dir, false, true, "v1.3.0") + testBumpedTag(t, dir, true, false, "v2.0.0") +} + +func TestComputeBumpedTagSuffix(t *testing.T) { + dir := setupBumpTestRepoWithTag(t, "v1.6.1+0.74.1") + defer func() { _ = os.RemoveAll(dir) }() + + testBumpedTag(t, dir, false, false, "v1.6.2+0.74.1") + testBumpedTag(t, dir, false, true, "v1.7.0+0.74.1") + testBumpedTag(t, dir, true, false, "v2.0.0+0.74.1") +} + +func TestComputeBumpedTagPreRelease(t *testing.T) { + dir := setupBumpTestRepoWithTag(t, "v2.0.0-rc.1") + defer func() { _ = os.RemoveAll(dir) }() + + testBumpedTagError(t, dir, false, false) +} + +func setupBumpTestRepo(t *testing.T) (dir string, runGit func(...string)) { + dir, err := os.MkdirTemp("", "ttal-tag-test-*") + if err != nil { + t.Fatal(err) + } + runGit = func(args ...string) { + cmd := exec.Command("git", append([]string{"-C", dir}, args...)...) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("git %v: %s — %v", args, string(out), err) + } + } + runGit("init") + runGit("commit", "--allow-empty", "-m", "init") + return dir, runGit +} + +func setupBumpTestRepoWithTag(t *testing.T, tag string) string { + dir, runGit := setupBumpTestRepo(t) + runGit("tag", tag) + return dir +} + +func TestTagCmdArgValidation(t *testing.T) { + // Simulate the arg validation logic from tagCmd.RunE + + // Test mutex: bump flag + positional arg fails + major := true + args := []string{"v1.0.0"} + bump := major + if bump && len(args) > 0 { + // expected error + } else { + t.Fatal("expected mutex error") + } + + // Test no args + no flags fails + major = false + minor := false + patch := false + args = []string{} + bump = major || minor || patch + if !bump && len(args) == 0 { + // expected error + } else { + t.Fatal("expected error for no flags and no args") + } + + // Test positional + no flags OK + major = false + minor = false + patch = false + args = []string{"v1.0.0"} + bump = major || minor || patch + if bump { + t.Fatal("should not be bump") + } + if len(args) == 0 { + t.Fatal("should have arg") + } + // should reach here without error + + // Test bump only + no positional OK + major = false + minor = false + patch = true + args = []string{} + bump = major || minor || patch + if !bump { + t.Fatal("should be bump") + } + if len(args) > 0 { + t.Fatal("should not have positional") + } + // should reach here without error +} From 0c922d53e67b7ccd08a14157d0c3ace9e9d5c5df Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 7 Jun 2026 05:46:32 +0000 Subject: [PATCH 2/4] refactor(tag): switch to --bump flag with level values Replace --major/--minor/--patch with single --bump flag. Cleaner API, natural mutual exclusion, better naming. --- cmd/tag.go | 64 +++++++++++++++++++----------------- cmd/tag_test.go | 86 ++++++++++++++++++++++++------------------------- 2 files changed, 76 insertions(+), 74 deletions(-) diff --git a/cmd/tag.go b/cmd/tag.go index 793f3688..eebac60d 100644 --- a/cmd/tag.go +++ b/cmd/tag.go @@ -23,23 +23,24 @@ var semverRe = regexp.MustCompile(`^v\d+\.\d+\.\d+(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+) var semverBaseRe = regexp.MustCompile(`^v(\d+)\.(\d+)\.(\d+)(\+[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?$`) var tagCmd = &cobra.Command{ - Use: "tag [ | --major | --minor | --patch]", + Use: "tag [ | --bump ]", Short: "Create and push a git tag via daemon (no credentials needed in worker)", Long: `Creates a lightweight git tag and pushes it to origin through the daemon. The daemon injects credentials — workers don't need tokens in their environment. Resolves the project from the current working directory. No --project flag needed. -With --major, --minor, or --patch, automatically bumps the largest existing version -tag in the repo. Existing +suffix (e.g. +0.74.1) is preserved on bump. +With --bump, automatically bumps the largest existing version tag in the repo. +Existing +suffix (e.g. +0.74.1) is preserved on bump. With a positional version argument, tags that exact version directly. Examples: - ttal tag --patch # v1.2.3 → v1.2.4 - ttal tag --minor # v1.2.3 → v1.3.0 - ttal tag v2.0.0 # explicit version - ttal tag v1.6.1+0.74.1 # explicit with +suffix`, + ttal tag --bump patch # v1.2.3 → v1.2.4 + ttal tag --bump minor # v1.2.3 → v1.3.0 + ttal tag --bump major # v1.2.3 → v2.0.0 + ttal tag v2.0.0 # explicit version + ttal tag v1.6.1+0.74.1 # explicit with +suffix`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() @@ -56,25 +57,28 @@ Examples: return err } - major, _ := cmd.Flags().GetBool("major") - minor, _ := cmd.Flags().GetBool("minor") - patch, _ := cmd.Flags().GetBool("patch") - bump := major || minor || patch + bump, _ := cmd.Flags().GetString("bump") + isBump := bump != "" - var tag string - - if bump && len(args) > 0 { - return fmt.Errorf("--major/--minor/--patch and a positional version are mutually exclusive") + if isBump && len(args) > 0 { + return fmt.Errorf("--bump and a positional version are mutually exclusive") } - if bump { - tag, err = computeBumpedTag(projectPath, major, minor) + var tag string + + if isBump { + switch bump { + case "major", "minor", "patch": + default: + return fmt.Errorf("invalid --bump value %q — must be major, minor, or patch", bump) + } + tag, err = computeBumpedTag(projectPath, bump) if err != nil { return err } } else { if len(args) == 0 { - return fmt.Errorf("either a version argument or one of --major/--minor/--patch is required") + return fmt.Errorf("either a version argument or --bump is required") } tag = args[0] if !semverRe.MatchString(tag) { @@ -100,9 +104,7 @@ Examples: } func init() { - tagCmd.Flags().Bool("major", false, "bump major version") - tagCmd.Flags().Bool("minor", false, "bump minor version") - tagCmd.Flags().Bool("patch", false, "bump patch version") + tagCmd.Flags().String("bump", "", "bump version: major, minor, or patch") rootCmd.AddCommand(tagCmd) } @@ -126,21 +128,22 @@ const ( initialPatch = "v0.0.1" ) -// computeBumpedTag finds the largest tag, bumps the specified segment, and returns the new tag. +// computeBumpedTag finds the largest tag, bumps the specified level, and returns the new tag. // The +suffix from the latest tag is preserved. -func computeBumpedTag(workDir string, major, minor bool) (string, error) { +func computeBumpedTag(workDir, level string) (string, error) { latest, err := latestTag(workDir) if err != nil { return "", err } if latest == "" { - if major { + switch level { + case "major": return initialMajor, nil - } - if minor { + case "minor": return initialMinor, nil + default: + return initialPatch, nil } - return initialPatch, nil } matches := semverBaseRe.FindStringSubmatch(latest) @@ -155,14 +158,15 @@ func computeBumpedTag(workDir string, major, minor bool) (string, error) { pat, _ := strconv.Atoi(matches[3]) suffix := matches[4] // includes leading +, or "" if absent - if major { + switch level { + case "major": maj++ min = 0 pat = 0 - } else if minor { + case "minor": min++ pat = 0 - } else { + default: pat++ } diff --git a/cmd/tag_test.go b/cmd/tag_test.go index c9753666..04aed1e4 100644 --- a/cmd/tag_test.go +++ b/cmd/tag_test.go @@ -90,22 +90,22 @@ func TestSemverBaseRe(t *testing.T) { } } -func testBumpedTag(t *testing.T, workDir string, major, minor bool, want string) { +func testBumpedTag(t *testing.T, workDir, level, want string) { t.Helper() - got, err := computeBumpedTag(workDir, major, minor) + got, err := computeBumpedTag(workDir, level) if err != nil { - t.Fatalf("computeBumpedTag(%v, %v): %v", major, minor, err) + t.Fatalf("computeBumpedTag(%q): %v", level, err) } if got != want { - t.Errorf("computeBumpedTag(%v, %v) = %q, want %q", major, minor, got, want) + t.Errorf("computeBumpedTag(%q) = %q, want %q", level, got, want) } } -func testBumpedTagError(t *testing.T, workDir string, major, minor bool) { +func testBumpedTagError(t *testing.T, workDir, level string) { t.Helper() - got, err := computeBumpedTag(workDir, major, minor) + got, err := computeBumpedTag(workDir, level) if err == nil { - t.Errorf("expected error for computeBumpedTag(%v, %v), got %q", major, minor, got) + t.Errorf("expected error for computeBumpedTag(%q), got %q", level, got) } if got != "" { t.Errorf("expected empty tag on error, got %q", got) @@ -113,38 +113,44 @@ func testBumpedTagError(t *testing.T, workDir string, major, minor bool) { } func TestComputeBumpedTagNoTags(t *testing.T) { - dir, gitDir := setupBumpTestRepo(t) + dir, _ := setupBumpTestRepo(t) defer func() { _ = os.RemoveAll(dir) }() - _ = gitDir - testBumpedTag(t, dir, false, false, "v0.0.1") - testBumpedTag(t, dir, false, true, "v0.1.0") - testBumpedTag(t, dir, true, false, "v1.0.0") + testBumpedTag(t, dir, "patch", "v0.0.1") + testBumpedTag(t, dir, "minor", "v0.1.0") + testBumpedTag(t, dir, "major", "v1.0.0") } func TestComputeBumpedTagSimple(t *testing.T) { dir := setupBumpTestRepoWithTag(t, "v1.2.3") defer func() { _ = os.RemoveAll(dir) }() - testBumpedTag(t, dir, false, false, "v1.2.4") - testBumpedTag(t, dir, false, true, "v1.3.0") - testBumpedTag(t, dir, true, false, "v2.0.0") + testBumpedTag(t, dir, "patch", "v1.2.4") + testBumpedTag(t, dir, "minor", "v1.3.0") + testBumpedTag(t, dir, "major", "v2.0.0") } func TestComputeBumpedTagSuffix(t *testing.T) { dir := setupBumpTestRepoWithTag(t, "v1.6.1+0.74.1") defer func() { _ = os.RemoveAll(dir) }() - testBumpedTag(t, dir, false, false, "v1.6.2+0.74.1") - testBumpedTag(t, dir, false, true, "v1.7.0+0.74.1") - testBumpedTag(t, dir, true, false, "v2.0.0+0.74.1") + testBumpedTag(t, dir, "patch", "v1.6.2+0.74.1") + testBumpedTag(t, dir, "minor", "v1.7.0+0.74.1") + testBumpedTag(t, dir, "major", "v2.0.0+0.74.1") } func TestComputeBumpedTagPreRelease(t *testing.T) { dir := setupBumpTestRepoWithTag(t, "v2.0.0-rc.1") defer func() { _ = os.RemoveAll(dir) }() - testBumpedTagError(t, dir, false, false) + testBumpedTagError(t, dir, "patch") +} + +func TestComputeBumpedTagInvalidLevel(t *testing.T) { + _, err := computeBumpedTag("/tmp", "foo") + if err == nil { + t.Fatal("expected error for invalid level") + } } func setupBumpTestRepo(t *testing.T) (dir string, runGit func(...string)) { @@ -173,53 +179,45 @@ func setupBumpTestRepoWithTag(t *testing.T, tag string) string { func TestTagCmdArgValidation(t *testing.T) { // Simulate the arg validation logic from tagCmd.RunE - // Test mutex: bump flag + positional arg fails - major := true + // Test mutex: --bump + positional arg fails + bump := "patch" args := []string{"v1.0.0"} - bump := major - if bump && len(args) > 0 { + isBump := bump != "" + if isBump && len(args) > 0 { // expected error } else { t.Fatal("expected mutex error") } - // Test no args + no flags fails - major = false - minor := false - patch := false + // Test no args + no bump fails + bump = "" args = []string{} - bump = major || minor || patch - if !bump && len(args) == 0 { + isBump = bump != "" + if !isBump && len(args) == 0 { // expected error } else { - t.Fatal("expected error for no flags and no args") + t.Fatal("expected error for no bump and no args") } - // Test positional + no flags OK - major = false - minor = false - patch = false + // Test positional + no bump OK + bump = "" args = []string{"v1.0.0"} - bump = major || minor || patch - if bump { + isBump = bump != "" + if isBump { t.Fatal("should not be bump") } if len(args) == 0 { t.Fatal("should have arg") } - // should reach here without error - // Test bump only + no positional OK - major = false - minor = false - patch = true + // Test bump only OK + bump = "patch" args = []string{} - bump = major || minor || patch - if !bump { + isBump = bump != "" + if !isBump { t.Fatal("should be bump") } if len(args) > 0 { t.Fatal("should not have positional") } - // should reach here without error } From 52c760a4220ffa27c7a4afd15a2a5b4128b9e124 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 7 Jun 2026 05:49:03 +0000 Subject: [PATCH 3/4] chore(tag): use constants for bump level strings --- cmd/tag.go | 14 +++++++++----- cmd/tag_test.go | 24 ++++++++++++------------ 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/cmd/tag.go b/cmd/tag.go index eebac60d..7e4847c7 100644 --- a/cmd/tag.go +++ b/cmd/tag.go @@ -68,7 +68,7 @@ Examples: if isBump { switch bump { - case "major", "minor", "patch": + case bumpMajor, bumpMinor, bumpPatch: default: return fmt.Errorf("invalid --bump value %q — must be major, minor, or patch", bump) } @@ -126,6 +126,10 @@ const ( initialMajor = "v1.0.0" initialMinor = "v0.1.0" initialPatch = "v0.0.1" + + bumpMajor = "major" + bumpMinor = "minor" + bumpPatch = "patch" ) // computeBumpedTag finds the largest tag, bumps the specified level, and returns the new tag. @@ -137,9 +141,9 @@ func computeBumpedTag(workDir, level string) (string, error) { } if latest == "" { switch level { - case "major": + case bumpMajor: return initialMajor, nil - case "minor": + case bumpMinor: return initialMinor, nil default: return initialPatch, nil @@ -159,11 +163,11 @@ func computeBumpedTag(workDir, level string) (string, error) { suffix := matches[4] // includes leading +, or "" if absent switch level { - case "major": + case bumpMajor: maj++ min = 0 pat = 0 - case "minor": + case bumpMinor: min++ pat = 0 default: diff --git a/cmd/tag_test.go b/cmd/tag_test.go index 04aed1e4..6c4aabee 100644 --- a/cmd/tag_test.go +++ b/cmd/tag_test.go @@ -116,34 +116,34 @@ func TestComputeBumpedTagNoTags(t *testing.T) { dir, _ := setupBumpTestRepo(t) defer func() { _ = os.RemoveAll(dir) }() - testBumpedTag(t, dir, "patch", "v0.0.1") - testBumpedTag(t, dir, "minor", "v0.1.0") - testBumpedTag(t, dir, "major", "v1.0.0") + testBumpedTag(t, dir, bumpPatch, "v0.0.1") + testBumpedTag(t, dir, bumpMinor, "v0.1.0") + testBumpedTag(t, dir, bumpMajor, "v1.0.0") } func TestComputeBumpedTagSimple(t *testing.T) { dir := setupBumpTestRepoWithTag(t, "v1.2.3") defer func() { _ = os.RemoveAll(dir) }() - testBumpedTag(t, dir, "patch", "v1.2.4") - testBumpedTag(t, dir, "minor", "v1.3.0") - testBumpedTag(t, dir, "major", "v2.0.0") + testBumpedTag(t, dir, bumpPatch, "v1.2.4") + testBumpedTag(t, dir, bumpMinor, "v1.3.0") + testBumpedTag(t, dir, bumpMajor, "v2.0.0") } func TestComputeBumpedTagSuffix(t *testing.T) { dir := setupBumpTestRepoWithTag(t, "v1.6.1+0.74.1") defer func() { _ = os.RemoveAll(dir) }() - testBumpedTag(t, dir, "patch", "v1.6.2+0.74.1") - testBumpedTag(t, dir, "minor", "v1.7.0+0.74.1") - testBumpedTag(t, dir, "major", "v2.0.0+0.74.1") + testBumpedTag(t, dir, bumpPatch, "v1.6.2+0.74.1") + testBumpedTag(t, dir, bumpMinor, "v1.7.0+0.74.1") + testBumpedTag(t, dir, bumpMajor, "v2.0.0+0.74.1") } func TestComputeBumpedTagPreRelease(t *testing.T) { dir := setupBumpTestRepoWithTag(t, "v2.0.0-rc.1") defer func() { _ = os.RemoveAll(dir) }() - testBumpedTagError(t, dir, "patch") + testBumpedTagError(t, dir, bumpPatch) } func TestComputeBumpedTagInvalidLevel(t *testing.T) { @@ -180,7 +180,7 @@ func TestTagCmdArgValidation(t *testing.T) { // Simulate the arg validation logic from tagCmd.RunE // Test mutex: --bump + positional arg fails - bump := "patch" + bump := bumpPatch args := []string{"v1.0.0"} isBump := bump != "" if isBump && len(args) > 0 { @@ -211,7 +211,7 @@ func TestTagCmdArgValidation(t *testing.T) { } // Test bump only OK - bump = "patch" + bump = bumpPatch args = []string{} isBump = bump != "" if !isBump { From 76176305e5e383374aeabce6af92e96471dee54a Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 7 Jun 2026 06:00:06 +0000 Subject: [PATCH 4/4] fix(tag): configure git user in test repo for CI compat --- cmd/tag_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/tag_test.go b/cmd/tag_test.go index 6c4aabee..98fb866c 100644 --- a/cmd/tag_test.go +++ b/cmd/tag_test.go @@ -166,6 +166,8 @@ func setupBumpTestRepo(t *testing.T) (dir string, runGit func(...string)) { } } runGit("init") + runGit("config", "user.name", "test") + runGit("config", "user.email", "test@test") runGit("commit", "--allow-empty", "-m", "init") return dir, runGit }