diff --git a/README.md b/README.md index 907acad..19a24a9 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,16 @@ jobs: - uses: actions/setup-go@v5 with: go-version: '1.23' - - uses: https://gitea.com/your-user/gitbump@master + - uses: https://lab.zlymeda.dynu.net/actions/gitbump@master +``` + +For a fixed base version with an incrementing fork suffix: + +```yaml +- uses: https://lab.zlymeda.dynu.net/actions/gitbump@master + with: + bump: suffix + suffix: meda ``` ### Inputs @@ -27,7 +36,8 @@ jobs: | Input | Description | Default | |-------|-------------|---------| | `dry-run` | Show what would be done without making changes | `false` | -| `bump` | Force specific bump type (`major`, `minor`, or `patch`) | | +| `bump` | Force specific bump type (`major`, `minor`, `patch`, or `suffix`) | | +| `suffix` | Suffix label used when creating the first suffix | | | `prefix` | Version prefix | `v` | | `no-push` | Create tag but don't push to remote | `false` | | `major-patterns` | Comma-separated list for major version bumps | `bump major,breaking change` | diff --git a/action.yml b/action.yml index 4a9c11c..a96ab11 100644 --- a/action.yml +++ b/action.yml @@ -6,7 +6,10 @@ inputs: required: false default: 'false' bump: - description: 'Force specific bump type (major, minor, or patch)' + description: 'Force specific bump type (major, minor, patch, or suffix)' + required: false + suffix: + description: 'Suffix label used when creating the first suffix' required: false prefix: description: 'Version prefix (default "v")' diff --git a/main.go b/main.go index 64fbc60..1772339 100644 --- a/main.go +++ b/main.go @@ -22,10 +22,20 @@ type Config struct { dryRun bool forcedBump string versionPrefix string + suffixLabel string patterns Patterns noPush bool } +type taggedVersion struct { + major int + minor int + patch int + suffixLabel string + suffixRevision int + hasSuffix bool +} + type Patterns struct { major []string minor []string @@ -74,11 +84,11 @@ func main() { return } - major, minor, patch, err := parseVersion(tag, config.versionPrefix) + version, err := parseTaggedVersion(tag, config.versionPrefix) if err != nil { log.Fatalf("Error parsing version: %v", err) } - fmt.Println("version:", major, minor, patch) + fmt.Println("version:", version.major, version.minor, version.patch) bumpType := getBumpType(commits, config) fmt.Println("bump type:", bumpType) @@ -87,7 +97,18 @@ func main() { return } - newVersion := calculateNewVersion(major, minor, patch, bumpType, config.versionPrefix) + var newVersion string + if bumpType == "suffix" { + newVersion, err = calculateSuffixVersion(version, config.suffixLabel, config.versionPrefix) + } else { + if version.hasSuffix { + log.Fatalf("Cannot apply %s bump to suffixed version %s", bumpType, tag) + } + newVersion = calculateNewVersion(version.major, version.minor, version.patch, bumpType, config.versionPrefix) + } + if err != nil { + log.Fatalf("Error calculating version: %v", err) + } fmt.Println("new version:", newVersion) if config.dryRun { @@ -109,8 +130,9 @@ func parseFlags() Config { flag.BoolVar(&config.printVersion, "version", false, "Just print version") flag.BoolVar(&config.dryRun, "dry-run", getEnvBool("DRY_RUN", false), "Show what would be done without making changes") - flag.StringVar(&config.forcedBump, "bump", os.Getenv("BUMP"), "Force specific bump type (major, minor, or patch)") + flag.StringVar(&config.forcedBump, "bump", getEnv("BUMP", ""), "Force specific bump type (major, minor, patch, or suffix)") flag.StringVar(&config.versionPrefix, "prefix", getEnv("PREFIX", "v"), "Version prefix (default 'v')") + flag.StringVar(&config.suffixLabel, "suffix", getEnv("SUFFIX", ""), "Suffix label used when creating the first suffix") flag.BoolVar(&config.noPush, "no-push", getEnvBool("NO_PUSH", false), "Create tag but don't push to remote") majorPatterns := flag.String("major-patterns", getEnv("MAJOR_PATTERNS", "bump major,breaking change"), "Comma-separated list will be joined with '|' for major version bumps") @@ -127,20 +149,20 @@ func parseFlags() Config { chore: strings.Split(*chorePatterns, ","), } - if config.forcedBump != "" && !contains([]string{"major", "minor", "patch"}, config.forcedBump) { - log.Fatalf("Invalid bump type: %s. Must be one of: major, minor, patch", config.forcedBump) + if config.forcedBump != "" && !contains([]string{"major", "minor", "patch", "suffix"}, config.forcedBump) { + log.Fatalf("Invalid bump type: %s. Must be one of: major, minor, patch, suffix", config.forcedBump) } return config } func getEnv(key, fallback string) string { - if value, ok := os.LookupEnv(key); ok { + if value, ok := os.LookupEnv(key); ok && value != "" { return value } // Check for Gitea/GitHub Actions input prefix inputKey := "INPUT_" + strings.ReplaceAll(strings.ToUpper(key), "-", "_") - if value, ok := os.LookupEnv(inputKey); ok { + if value, ok := os.LookupEnv(inputKey); ok && value != "" { return value } return fallback @@ -214,29 +236,60 @@ func getCommitsSinceTag(tag string) ([]string, error) { } func parseVersion(tag, prefix string) (int, int, int, error) { - tag = strings.TrimPrefix(tag, prefix) + version, err := parseTaggedVersion(tag, prefix) + if err != nil { + return 0, 0, 0, err + } + if version.hasSuffix { + return 0, 0, 0, fmt.Errorf("version has a suffix: %s", tag) + } + return version.major, version.minor, version.patch, nil +} - parts := strings.Split(tag, ".") +func parseTaggedVersion(tag, prefix string) (taggedVersion, error) { + rawVersion := strings.TrimPrefix(tag, prefix) + baseVersion := rawVersion + suffix := "" + if separator := strings.IndexByte(rawVersion, '-'); separator >= 0 { + baseVersion = rawVersion[:separator] + suffix = rawVersion[separator+1:] + } + + parts := strings.Split(baseVersion, ".") if len(parts) != 3 { - return 0, 0, 0, fmt.Errorf("invalid version format: %s", tag) + return taggedVersion{}, fmt.Errorf("invalid version format: %s", rawVersion) } major, err := strconv.Atoi(parts[0]) - if err != nil { - return 0, 0, 0, err + if err != nil || major < 0 { + return taggedVersion{}, fmt.Errorf("invalid major version: %s", parts[0]) } - minor, err := strconv.Atoi(parts[1]) - if err != nil { - return 0, 0, 0, err + if err != nil || minor < 0 { + return taggedVersion{}, fmt.Errorf("invalid minor version: %s", parts[1]) } - patch, err := strconv.Atoi(parts[2]) - if err != nil { - return 0, 0, 0, err + if err != nil || patch < 0 { + return taggedVersion{}, fmt.Errorf("invalid patch version: %s", parts[2]) } - return major, minor, patch, nil + version := taggedVersion{major: major, minor: minor, patch: patch} + if suffix == "" { + return version, nil + } + + suffixParts := strings.Split(suffix, ".") + if len(suffixParts) != 2 || suffixParts[0] == "" { + return taggedVersion{}, fmt.Errorf("invalid version suffix: %s", suffix) + } + revision, err := strconv.Atoi(suffixParts[1]) + if err != nil || revision < 1 { + return taggedVersion{}, fmt.Errorf("invalid version suffix revision: %s", suffix) + } + version.suffixLabel = suffixParts[0] + version.suffixRevision = revision + version.hasSuffix = true + return version, nil } func getBumpType(commits []string, config Config) string { @@ -289,6 +342,32 @@ func calculateNewVersion(major, minor, patch int, bumpType, prefix string) strin return fmt.Sprintf("%s%d.%d.%d", prefix, major, minor, patch) } +func calculateSuffixVersion(version taggedVersion, suffixLabel, prefix string) (string, error) { + if version.hasSuffix { + if suffixLabel != "" && suffixLabel != version.suffixLabel { + return "", fmt.Errorf("suffix label %q does not match %q", suffixLabel, version.suffixLabel) + } + version.suffixRevision++ + return formatTaggedVersion(version, prefix), nil + } + + if suffixLabel == "" { + return "", fmt.Errorf("suffix label is required for a base version") + } + version.suffixLabel = suffixLabel + version.suffixRevision = 1 + version.hasSuffix = true + return formatTaggedVersion(version, prefix), nil +} + +func formatTaggedVersion(version taggedVersion, prefix string) string { + base := fmt.Sprintf("%s%d.%d.%d", prefix, version.major, version.minor, version.patch) + if version.hasSuffix { + return fmt.Sprintf("%s-%s.%d", base, version.suffixLabel, version.suffixRevision) + } + return base +} + func createAndPushTag(version string, noPush bool) error { if out, err := git("tag " + version); err != nil { return fmt.Errorf("error creating tag: %v; stdout: %v", err, out) diff --git a/main_test.go b/main_test.go index 0ff141c..5860db1 100644 --- a/main_test.go +++ b/main_test.go @@ -12,3 +12,89 @@ func TestParseVersion(t *testing.T) { t.Fatal("version does not match") } } + +func TestParseTaggedVersion(t *testing.T) { + version, err := parseTaggedVersion("v0.10.50-meda.2", "v") + if err != nil { + t.Fatal(err) + } + + if version.major != 0 || version.minor != 10 || version.patch != 50 { + t.Fatalf("base version does not match: %+v", version) + } + if !version.hasSuffix || version.suffixLabel != "meda" || version.suffixRevision != 2 { + t.Fatalf("suffix does not match: %+v", version) + } +} + +func TestParseVersionRejectsSuffix(t *testing.T) { + if _, _, _, err := parseVersion("v0.10.50-meda.2", "v"); err == nil { + t.Fatal("expected suffixed version to be rejected") + } +} + +func TestGetEnvActionInput(t *testing.T) { + t.Setenv("BUMP", "") + t.Setenv("SUFFIX", "") + t.Setenv("INPUT_BUMP", "suffix") + t.Setenv("INPUT_SUFFIX", "meda") + + if value := getEnv("BUMP", ""); value != "suffix" { + t.Fatalf("expected suffix bump, got %s", value) + } + if value := getEnv("SUFFIX", ""); value != "meda" { + t.Fatalf("expected meda suffix, got %s", value) + } +} + +func TestCalculateSuffixVersion(t *testing.T) { + tests := []struct { + name string + version taggedVersion + suffix string + expected string + expectsErr bool + }{ + { + name: "first suffix", + version: taggedVersion{major: 0, minor: 10, patch: 50}, + suffix: "meda", + expected: "v0.10.50-meda.1", + }, + { + name: "increment suffix", + version: taggedVersion{major: 0, minor: 10, patch: 50, suffixLabel: "meda", suffixRevision: 1, hasSuffix: true}, + suffix: "meda", + expected: "v0.10.50-meda.2", + }, + { + name: "missing label", + version: taggedVersion{major: 0, minor: 10, patch: 50}, + expectsErr: true, + }, + { + name: "label mismatch", + version: taggedVersion{major: 0, minor: 10, patch: 50, suffixLabel: "meda", suffixRevision: 1, hasSuffix: true}, + suffix: "other", + expectsErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, err := calculateSuffixVersion(test.version, test.suffix, "v") + if test.expectsErr { + if err == nil { + t.Fatal("expected an error") + } + return + } + if err != nil { + t.Fatal(err) + } + if actual != test.expected { + t.Fatalf("expected %s, got %s", test.expected, actual) + } + }) + } +}