go-build/pkg/release/version.go
Snider febe858942 fix(ax): replace banned os imports and add usage example comments
- Remove `os` import from internal/ax/ax.go; replace os.Getwd() with
  syscall.Getwd(), os.MkdirAll() with coreio.Local.EnsureDir(), and
  os.Chmod() with syscall.Chmod()
- Remove `os` import from pkg/sdk/generators/typescript_test.go;
  replace os.PathListSeparator and os.Getenv() with core.Env("PS")
  and core.Env("PATH")
- Replace all "Usage example: call/declare ... from integrating code"
  placeholder comments with concrete code examples across 45 files
  covering build, release, sdk, signing, publishers, builders, and cmd

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-31 18:33:36 +01:00

222 lines
5.8 KiB
Go

// Package release provides release automation with changelog generation and publishing.
package release
import (
"context"
"regexp"
"strconv"
"dappco.re/go/core"
"dappco.re/go/core/build/internal/ax"
coreerr "dappco.re/go/core/log"
)
// semverRegex matches semantic version strings with or without 'v' prefix.
var semverRegex = regexp.MustCompile(`^v?(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9.-]+))?(?:\+([a-zA-Z0-9.-]+))?$`)
// DetermineVersion determines the version for a release.
// It checks in order:
// 1. Git tag on HEAD
// 2. Most recent tag + increment patch
// 3. Default to v0.0.1 if no tags exist
//
// version, err := release.DetermineVersion(".") // → "v1.2.4"
func DetermineVersion(dir string) (string, error) {
return DetermineVersionWithContext(context.Background(), dir)
}
// DetermineVersionWithContext determines the version while honouring caller cancellation.
// It checks in order:
// 1. Git tag on HEAD
// 2. Most recent tag + increment patch
// 3. Default to v0.0.1 if no tags exist
//
// version, err := release.DetermineVersionWithContext(ctx, ".") // → "v1.2.4"
func DetermineVersionWithContext(ctx context.Context, dir string) (string, error) {
// Check if HEAD has a tag
headTag, err := getTagOnHeadWithContext(ctx, dir)
if err == nil && headTag != "" {
return normalizeVersion(headTag), nil
}
if err != nil && ctx.Err() != nil {
return "", coreerr.E("release.DetermineVersionWithContext", "version lookup cancelled", ctx.Err())
}
// Get most recent tag
latestTag, err := getLatestTagWithContext(ctx, dir)
if err != nil && ctx.Err() != nil {
return "", coreerr.E("release.DetermineVersionWithContext", "version lookup cancelled", ctx.Err())
}
if err != nil || latestTag == "" {
// No tags exist, return default
return "v0.0.1", nil
}
// Increment patch version
return IncrementVersion(latestTag), nil
}
// IncrementVersion increments the patch version of a semver string.
// - "v1.2.3" → "v1.2.4"
// - "1.2.3" → "v1.2.4"
// - "v1.2.3-alpha" → "v1.2.4" (strips prerelease)
//
// next := release.IncrementVersion("v1.2.3") // → "v1.2.4"
func IncrementVersion(current string) string {
matches := semverRegex.FindStringSubmatch(current)
if matches == nil {
// Not a valid semver, return as-is with increment suffix
return current + ".1"
}
major, _ := strconv.Atoi(matches[1])
minor, _ := strconv.Atoi(matches[2])
patch, _ := strconv.Atoi(matches[3])
// Increment patch
patch++
return core.Sprintf("v%d.%d.%d", major, minor, patch)
}
// IncrementMinor increments the minor version of a semver string.
// - "v1.2.3" → "v1.3.0"
// - "1.2.3" → "v1.3.0"
//
// next := release.IncrementMinor("v1.2.3") // → "v1.3.0"
func IncrementMinor(current string) string {
matches := semverRegex.FindStringSubmatch(current)
if matches == nil {
return current + ".1"
}
major, _ := strconv.Atoi(matches[1])
minor, _ := strconv.Atoi(matches[2])
// Increment minor, reset patch
minor++
return core.Sprintf("v%d.%d.0", major, minor)
}
// IncrementMajor increments the major version of a semver string.
// - "v1.2.3" → "v2.0.0"
// - "1.2.3" → "v2.0.0"
//
// next := release.IncrementMajor("v1.2.3") // → "v2.0.0"
func IncrementMajor(current string) string {
matches := semverRegex.FindStringSubmatch(current)
if matches == nil {
return current + ".1"
}
major, _ := strconv.Atoi(matches[1])
// Increment major, reset minor and patch
major++
return core.Sprintf("v%d.0.0", major)
}
// ParseVersion parses a semver string into its components.
// Returns (major, minor, patch, prerelease, build, error).
//
// major, minor, patch, pre, build, err := release.ParseVersion("v1.2.3-alpha+001")
func ParseVersion(version string) (int, int, int, string, string, error) {
matches := semverRegex.FindStringSubmatch(version)
if matches == nil {
return 0, 0, 0, "", "", coreerr.E("release.ParseVersion", "invalid semver: "+version, nil)
}
major, _ := strconv.Atoi(matches[1])
minor, _ := strconv.Atoi(matches[2])
patch, _ := strconv.Atoi(matches[3])
prerelease := matches[4]
build := matches[5]
return major, minor, patch, prerelease, build, nil
}
// ValidateVersion checks if a string is a valid semver.
//
// if release.ValidateVersion("v1.2.3") { ... }
func ValidateVersion(version string) bool {
return semverRegex.MatchString(version)
}
// normalizeVersion ensures the version starts with 'v'.
func normalizeVersion(version string) string {
if !core.HasPrefix(version, "v") {
return "v" + version
}
return version
}
func getTagOnHeadWithContext(ctx context.Context, dir string) (string, error) {
output, err := ax.RunDir(ctx, dir, "git", "describe", "--tags", "--exact-match", "HEAD")
if err != nil {
return "", err
}
return core.Trim(output), nil
}
func getLatestTagWithContext(ctx context.Context, dir string) (string, error) {
output, err := ax.RunDir(ctx, dir, "git", "describe", "--tags", "--abbrev=0")
if err != nil {
return "", err
}
return core.Trim(output), nil
}
// CompareVersions compares two semver strings.
// Returns -1 if a < b, 0 if a == b, 1 if a > b.
//
// result := release.CompareVersions("v1.2.3", "v1.2.4") // → -1
func CompareVersions(a, b string) int {
aMajor, aMinor, aPatch, _, _, errA := ParseVersion(a)
bMajor, bMinor, bPatch, _, _, errB := ParseVersion(b)
// Invalid versions are considered less than valid ones
if errA != nil && errB != nil {
switch {
case a < b:
return -1
case a > b:
return 1
default:
return 0
}
}
if errA != nil {
return -1
}
if errB != nil {
return 1
}
// Compare major
if aMajor != bMajor {
if aMajor < bMajor {
return -1
}
return 1
}
// Compare minor
if aMinor != bMinor {
if aMinor < bMinor {
return -1
}
return 1
}
// Compare patch
if aPatch != bPatch {
if aPatch < bPatch {
return -1
}
return 1
}
return 0
}