Skip to content

Commit 422c4d2

Browse files
committed
auto version
1 parent 0e54cfc commit 422c4d2

File tree

2 files changed

+203
-24
lines changed

2 files changed

+203
-24
lines changed

cmd/tag-release-tui/README.md

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,47 @@ go build -o bin/tag-release-tui ./cmd/tag-release-tui
1212
## Usage
1313

1414
```bash
15-
# Basic usage with default remote (origin)
15+
# Auto-increment version from latest tag on default remote (origin)
16+
./bin/tag-release-tui
17+
18+
# Auto-increment version with specific remote
19+
./bin/tag-release-tui --remote omgitsads
20+
21+
# Auto-increment version in test mode
22+
./bin/tag-release-tui --test
23+
24+
# Manual version specification (same as before)
1625
./bin/tag-release-tui v1.2.3
1726

18-
# Specify a different remote (e.g., your fork)
27+
# Manual version with different remote
1928
./bin/tag-release-tui v1.2.3 --remote omgitsads
2029

21-
# Run in test mode (validation only, no actual changes)
30+
# Manual version in test mode
2231
./bin/tag-release-tui v1.2.3 --test
2332

24-
# Test with a specific remote
33+
# Combine flags (manual version with remote and test mode)
2534
./bin/tag-release-tui v1.2.3 --remote omgitsads --test
2635
```
2736

37+
## Auto-Versioning
38+
39+
When no version is specified, the tool will automatically determine the next version:
40+
41+
1. **Fetches all tags** from the specified remote (default: origin)
42+
2. **Finds the latest semantic version** tag (e.g., v1.2.3)
43+
3. **Increments the minor version** and resets patch to 0 (e.g., v1.2.3 → v1.3.0)
44+
4. **Falls back to v0.1.0** if no semantic version tags exist
45+
46+
Examples:
47+
- If latest tag is `v1.5.2`, next version will be `v1.6.0`
48+
- If latest tag is `v0.4.1`, next version will be `v0.5.0`
49+
- If no semantic version tags exist, first version will be `v0.1.0`
50+
51+
This follows semantic versioning conventions and is perfect for automated releases or when you want to maintain consistent version incrementing.
52+
2853
## Features
2954

55+
- **Auto-Versioning**: Automatically increments minor version from latest tag when no version specified
3056
- **Interactive Validation**: Shows real-time validation of release requirements
3157
- **Test Mode**: Run with `--test` flag to validate without making any actual changes
3258
- **Remote Selection**: Specify git remote with `--remote` flag (default: origin)
@@ -36,6 +62,7 @@ go build -o bin/tag-release-tui ./cmd/tag-release-tui
3662
- **Automatic Release Detection**: Automatically polls GitHub releases page and provides URL when available
3763
- **Post-Release Instructions**: Provides next steps after successful release creation
3864
- **Safe Testing**: Perfect for testing against your fork without affecting upstream
65+
- **Semantic Versioning**: Supports and validates semantic version format (vX.Y.Z)
3966

4067
## Flow
4168

cmd/tag-release-tui/main.go

Lines changed: 172 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"os"
77
"os/exec"
88
"regexp"
9+
"sort"
10+
"strconv"
911
"strings"
1012
"time"
1113

@@ -645,33 +647,183 @@ type pollAttemptMsg struct {
645647
attempt int
646648
}
647649

648-
func main() {
649-
if len(os.Args) < 2 {
650-
fmt.Println("Error: No tag specified.")
651-
fmt.Println("Usage: tag-release-tui vX.Y.Z [--remote <remote-name>] [--test]")
652-
fmt.Println(" --remote: Specify git remote name (default: origin)")
653-
fmt.Println(" --test: Run in test mode (validation only, no actual changes)")
654-
os.Exit(1)
650+
// Semantic version parsing and incrementing functions
651+
652+
type semVersion struct {
653+
major, minor, patch int
654+
prefix string // v prefix if present
655+
}
656+
657+
func parseSemanticVersion(version string) (*semVersion, error) {
658+
// Handle v prefix
659+
prefix := ""
660+
if strings.HasPrefix(version, "v") {
661+
prefix = "v"
662+
version = version[1:]
663+
}
664+
665+
parts := strings.Split(version, ".")
666+
if len(parts) != 3 {
667+
return nil, fmt.Errorf("invalid semantic version format: %s", version)
668+
}
669+
670+
major, err := strconv.Atoi(parts[0])
671+
if err != nil {
672+
return nil, fmt.Errorf("invalid major version: %s", parts[0])
673+
}
674+
675+
minor, err := strconv.Atoi(parts[1])
676+
if err != nil {
677+
return nil, fmt.Errorf("invalid minor version: %s", parts[1])
678+
}
679+
680+
patch, err := strconv.Atoi(parts[2])
681+
if err != nil {
682+
return nil, fmt.Errorf("invalid patch version: %s", parts[2])
655683
}
656684

657-
tag := os.Args[1]
685+
return &semVersion{
686+
major: major,
687+
minor: minor,
688+
patch: patch,
689+
prefix: prefix,
690+
}, nil
691+
}
692+
693+
func (v *semVersion) incrementMinor() *semVersion {
694+
return &semVersion{
695+
major: v.major,
696+
minor: v.minor + 1,
697+
patch: 0, // reset patch to 0 when incrementing minor
698+
prefix: v.prefix,
699+
}
700+
}
701+
702+
func (v *semVersion) toString() string {
703+
return fmt.Sprintf("%s%d.%d.%d", v.prefix, v.major, v.minor, v.patch)
704+
}
705+
706+
func getNextVersion(remote string) (string, error) {
707+
// Get all tags from the remote
708+
cmd := exec.Command("git", "ls-remote", "--tags", remote)
709+
output, err := cmd.Output()
710+
if err != nil {
711+
return "", fmt.Errorf("failed to list remote tags: %v", err)
712+
}
713+
714+
var versions []*semVersion
715+
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
716+
717+
for _, line := range lines {
718+
if line == "" {
719+
continue
720+
}
721+
722+
// Parse line format: hash refs/tags/vX.Y.Z
723+
parts := strings.Fields(line)
724+
if len(parts) < 2 {
725+
continue
726+
}
727+
728+
ref := parts[1]
729+
if !strings.HasPrefix(ref, "refs/tags/") {
730+
continue
731+
}
732+
733+
tag := strings.TrimPrefix(ref, "refs/tags/")
734+
735+
// Skip annotated tag refs (ending with ^{})
736+
if strings.HasSuffix(tag, "^{}") {
737+
continue
738+
}
739+
740+
// Try to parse as semantic version
741+
version, err := parseSemanticVersion(tag)
742+
if err != nil {
743+
// Skip non-semantic version tags
744+
continue
745+
}
746+
747+
versions = append(versions, version)
748+
}
749+
750+
// If no versions found, start at v0.1.0
751+
if len(versions) == 0 {
752+
return "v0.1.0", nil
753+
}
754+
755+
// Sort versions to find the latest
756+
sort.Slice(versions, func(i, j int) bool {
757+
a, b := versions[i], versions[j]
758+
if a.major != b.major {
759+
return a.major < b.major
760+
}
761+
if a.minor != b.minor {
762+
return a.minor < b.minor
763+
}
764+
return a.patch < b.patch
765+
})
766+
767+
// Get latest version and increment minor
768+
latest := versions[len(versions)-1]
769+
next := latest.incrementMinor()
770+
771+
return next.toString(), nil
772+
}
773+
774+
func main() {
775+
var tag string
658776
testMode := false
659777
remote := "origin" // default remote
660778

661-
// Parse flags
662-
for i := 2; i < len(os.Args); i++ {
663-
switch os.Args[i] {
664-
case "--test", "-t":
665-
testMode = true
666-
case "--remote", "-r":
667-
if i+1 < len(os.Args) {
668-
remote = os.Args[i+1]
669-
i++ // skip next arg
670-
} else {
671-
fmt.Println("Error: --remote flag requires a value")
672-
os.Exit(1)
779+
// Check if tag is provided as first argument
780+
if len(os.Args) >= 2 && !strings.HasPrefix(os.Args[1], "--") {
781+
tag = os.Args[1]
782+
// Parse remaining flags starting from index 2
783+
for i := 2; i < len(os.Args); i++ {
784+
switch os.Args[i] {
785+
case "--test", "-t":
786+
testMode = true
787+
case "--remote", "-r":
788+
if i+1 < len(os.Args) {
789+
remote = os.Args[i+1]
790+
i++ // skip next arg
791+
} else {
792+
fmt.Println("Error: --remote flag requires a value")
793+
os.Exit(1)
794+
}
795+
}
796+
}
797+
} else {
798+
// No tag provided, parse flags first
799+
for i := 1; i < len(os.Args); i++ {
800+
switch os.Args[i] {
801+
case "--test", "-t":
802+
testMode = true
803+
case "--remote", "-r":
804+
if i+1 < len(os.Args) {
805+
remote = os.Args[i+1]
806+
i++ // skip next arg
807+
} else {
808+
fmt.Println("Error: --remote flag requires a value")
809+
os.Exit(1)
810+
}
673811
}
674812
}
813+
814+
// Auto-generate tag from latest release
815+
fmt.Printf("No version specified. Determining next version from remote '%s'...\n", remote)
816+
var err error
817+
tag, err = getNextVersion(remote)
818+
if err != nil {
819+
fmt.Printf("Error determining next version: %v\n", err)
820+
fmt.Println("\nUsage: tag-release-tui [vX.Y.Z] [--remote <remote-name>] [--test]")
821+
fmt.Println(" vX.Y.Z: Version tag (if not provided, auto-increments from latest)")
822+
fmt.Println(" --remote: Specify git remote name (default: origin)")
823+
fmt.Println(" --test: Run in test mode (validation only, no actual changes)")
824+
os.Exit(1)
825+
}
826+
fmt.Printf("Next version determined: %s\n", tag)
675827
}
676828

677829
if testMode {

0 commit comments

Comments
 (0)