diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..cd8ae4e --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,16 @@ +# scripts +The package contains various scripts + + +## versionbump +This Go script can automatically bump the semantic version number defined in a Go source file. It parses the specified Go source file with `go/ast`, finds the given variable (which is assumed to contain a semantic version string), increments the specified part of the version number (major, minor, or patch) with `github.com/Masterminds/semver/v3`, and rewrites the file with the updated version. + +``` +go run versionbump.go -file /path/to/your/file.go -var YourVersionVariable +``` + +By default, the patch version is incremented. To increment the major or minor versions instead, specify -part major or -part minor respectively: + +``` +go run versionbump.go -file /path/to/your/file.go -var YourVersionVariable -part minor +``` diff --git a/scripts/versionbump/versionbump.go b/scripts/versionbump/versionbump.go new file mode 100644 index 0000000..75ec331 --- /dev/null +++ b/scripts/versionbump/versionbump.go @@ -0,0 +1,104 @@ +package main + +import ( + "flag" + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "os" + "path/filepath" + "strconv" + + semver "github.com/Masterminds/semver/v3" +) + +func bumpVersion(fileName, varName, part string) (string, string, error) { + absPath, err := filepath.Abs(fileName) + if err != nil { + return "", "", fmt.Errorf("unable to get absolute path: %v", err) + } + + fset := token.NewFileSet() + node, err := parser.ParseFile(fset, absPath, nil, parser.ParseComments) + if err != nil { + return "", "", fmt.Errorf("could not parse file: %w", err) + } + + var oldVersion, newVersion string + + ast.Inspect(node, func(n ast.Node) bool { + if v, ok := n.(*ast.GenDecl); ok { + for _, spec := range v.Specs { + if s, ok := spec.(*ast.ValueSpec); ok { + for idx, id := range s.Names { + if id.Name == varName { + oldVersion, _ = strconv.Unquote(s.Values[idx].(*ast.BasicLit).Value) + v, err := semver.NewVersion(oldVersion) + if err != nil || v.String() == "" { + return false + } + var vInc func() semver.Version + switch part { + case "major": + vInc = v.IncMajor + case "minor": + vInc = v.IncMinor + case "", "patch": + vInc = v.IncPatch + default: + return false + } + newVersion = "v" + vInc().String() + s.Values[idx].(*ast.BasicLit).Value = fmt.Sprintf("`%s`", newVersion) + return false + } + } + } + } + } + return true + }) + + if newVersion == "" { + return oldVersion, newVersion, fmt.Errorf("failed to update the version") + } + + f, err := os.OpenFile(fileName, os.O_RDWR, 0666) + if err != nil { + return oldVersion, newVersion, fmt.Errorf("could not open file: %w", err) + } + defer f.Close() + + if err := printer.Fprint(f, fset, node); err != nil { + return oldVersion, newVersion, fmt.Errorf("could not write to file: %w", err) + } + + return oldVersion, newVersion, nil +} + +func main() { + var ( + fileName string + varName string + part string + ) + + flag.StringVar(&fileName, "file", "", "Go source file to parse") + flag.StringVar(&varName, "var", "", "Variable to update") + flag.StringVar(&part, "part", "patch", "Version part to increment (major, minor, patch)") + + flag.Parse() + + if fileName == "" || varName == "" { + fmt.Println("Error: Both -file and -var are required") + os.Exit(1) + } + oldVersion, newVersion, err := bumpVersion(fileName, varName, part) + if err != nil { + fmt.Printf("Error bumping version: %v\n", err) + os.Exit(1) + } + fmt.Printf("Bump from %s to %s\n", oldVersion, newVersion) +} diff --git a/scripts/versionbump/versionbump_test.go b/scripts/versionbump/versionbump_test.go new file mode 100644 index 0000000..baa929b --- /dev/null +++ b/scripts/versionbump/versionbump_test.go @@ -0,0 +1,135 @@ +package main + +import ( + "os" + "os/exec" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBumpVersionCLI(t *testing.T) { + testCases := []struct { + name string + fileContent string + varName string + part string + expectedErr bool + expectedOutput string + }{ + { + name: "Test v1.0.0", + fileContent: "package main\n\nvar version = \"v1.0.0\"", + varName: "version", + part: "patch", + expectedErr: false, + expectedOutput: "Bump from v1.0.0 to v1.0.1\n", + }, + { + name: "Test 1.0.8", + fileContent: "package main\n\nvar version = \"1.0.8\"", + varName: "version", + part: "patch", + expectedErr: false, + expectedOutput: "Bump from 1.0.8 to v1.0.9\n", + }, + { + name: "Test v1.0.9-dev", + fileContent: "package main\n\nvar version = \"v1.0.9-dev\"", + varName: "version", + part: "patch", + expectedErr: false, + expectedOutput: "Bump from v1.0.9-dev to v1.0.9\n", + }, + { + name: "Test 1.0.85.1", + fileContent: "package main\n\nvar version = \"1.0.85.1\"", + varName: "version", + part: "patch", + expectedErr: true, + }, + { + name: "Test with unwanted prefixes/suffixes: with x prefix", + fileContent: "package main\n\nvar version = \"x1.0.0\"", + varName: "version", + part: "patch", + expectedErr: true, + }, + { + name: "Test with unwanted prefixes/suffixes: with x suffix", + fileContent: "package main\n\nvar version = \"1.0.0x\"", + varName: "version", + part: "patch", + expectedErr: true, + }, + { + name: "Test with unwanted prefixes/suffixes: with new lines", + fileContent: "package main\n\nvar version = \"\n\n1.0.0\"", + varName: "version", + part: "patch", + expectedErr: true, + }, + { + name: "Test with unwanted prefixes/suffixes: with space", + fileContent: "package main\n\nvar version = \" 1.0.0\"", + varName: "version", + part: "patch", + expectedErr: true, + }, + { + name: "Test with unwanted prefixes/suffixes: with multiple dot", + fileContent: "package main\n\nvar version = \"1.0..0\"", + varName: "version", + part: "patch", + expectedErr: true, + }, + { + name: "Test with negative numbers", + fileContent: "package main\n\nvar version = \"-1.0.0\"", + varName: "version", + part: "patch", + expectedErr: true, + }, + { + name: "Test with unparseable numbers", + fileContent: "package main\n\nvar version = \"1.0.X\"", + varName: "version", + part: "patch", + expectedErr: true, + }, + { + name: "Test with big numbers", + fileContent: "package main\n\nvar version = \"1.0.111111111111111111111111111111111111111111111111111111111111111111111111\"", + varName: "minor", + part: "patch", + expectedErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create a temporary file and write the content to it + tempFile, err := os.CreateTemp(os.TempDir(), "prefix-") + if err != nil { + t.Fatalf("Cannot create temporary file: %s", err) + } + + defer os.Remove(tempFile.Name()) + + _, err = tempFile.Write([]byte(tc.fileContent)) + if err != nil { + t.Fatalf("Failed to write to temporary file: %s", err) + } + + cmd := exec.Command("go", "run", "versionbump.go", "-file", tempFile.Name(), "-var", tc.varName, "-part", tc.part) + + output, err := cmd.CombinedOutput() + if tc.expectedErr { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + assert.Contains(t, string(output), tc.expectedOutput) + } + }) + } +}