feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
// Package release provides release automation with changelog generation and publishing.
|
|
|
|
|
package release
|
|
|
|
|
|
|
|
|
|
import (
|
2026-03-26 17:41:53 +00:00
|
|
|
"context"
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
"regexp"
|
|
|
|
|
"strconv"
|
2026-03-16 21:03:21 +00:00
|
|
|
|
2026-03-26 14:58:13 +00:00
|
|
|
"dappco.re/go/core"
|
2026-03-26 17:41:53 +00:00
|
|
|
"dappco.re/go/core/build/internal/ax"
|
2026-03-22 01:34:37 +00:00
|
|
|
coreerr "dappco.re/go/core/log"
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 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
|
2026-03-26 17:41:53 +00:00
|
|
|
//
|
2026-03-31 18:33:36 +01:00
|
|
|
// version, err := release.DetermineVersion(".") // → "v1.2.4"
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
func DetermineVersion(dir string) (string, error) {
|
2026-03-30 05:46:27 +00:00
|
|
|
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
|
|
|
|
|
//
|
2026-03-31 18:33:36 +01:00
|
|
|
// version, err := release.DetermineVersionWithContext(ctx, ".") // → "v1.2.4"
|
2026-03-30 05:46:27 +00:00
|
|
|
func DetermineVersionWithContext(ctx context.Context, dir string) (string, error) {
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
// Check if HEAD has a tag
|
2026-03-30 05:46:27 +00:00
|
|
|
headTag, err := getTagOnHeadWithContext(ctx, dir)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
if err == nil && headTag != "" {
|
|
|
|
|
return normalizeVersion(headTag), nil
|
|
|
|
|
}
|
2026-03-30 05:46:27 +00:00
|
|
|
if err != nil && ctx.Err() != nil {
|
|
|
|
|
return "", coreerr.E("release.DetermineVersionWithContext", "version lookup cancelled", ctx.Err())
|
|
|
|
|
}
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
|
|
|
|
|
// Get most recent tag
|
2026-03-30 05:46:27 +00:00
|
|
|
latestTag, err := getLatestTagWithContext(ctx, dir)
|
|
|
|
|
if err != nil && ctx.Err() != nil {
|
|
|
|
|
return "", coreerr.E("release.DetermineVersionWithContext", "version lookup cancelled", ctx.Err())
|
|
|
|
|
}
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
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.
|
2026-03-31 18:33:36 +01:00
|
|
|
// - "v1.2.3" → "v1.2.4"
|
|
|
|
|
// - "1.2.3" → "v1.2.4"
|
|
|
|
|
// - "v1.2.3-alpha" → "v1.2.4" (strips prerelease)
|
2026-03-26 17:41:53 +00:00
|
|
|
//
|
2026-03-31 18:33:36 +01:00
|
|
|
// next := release.IncrementVersion("v1.2.3") // → "v1.2.4"
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
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++
|
|
|
|
|
|
2026-03-26 14:58:13 +00:00
|
|
|
return core.Sprintf("v%d.%d.%d", major, minor, patch)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IncrementMinor increments the minor version of a semver string.
|
2026-03-31 18:33:36 +01:00
|
|
|
// - "v1.2.3" → "v1.3.0"
|
|
|
|
|
// - "1.2.3" → "v1.3.0"
|
2026-03-26 17:41:53 +00:00
|
|
|
//
|
2026-03-31 18:33:36 +01:00
|
|
|
// next := release.IncrementMinor("v1.2.3") // → "v1.3.0"
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
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++
|
|
|
|
|
|
2026-03-26 14:58:13 +00:00
|
|
|
return core.Sprintf("v%d.%d.0", major, minor)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IncrementMajor increments the major version of a semver string.
|
2026-03-31 18:33:36 +01:00
|
|
|
// - "v1.2.3" → "v2.0.0"
|
|
|
|
|
// - "1.2.3" → "v2.0.0"
|
2026-03-26 17:41:53 +00:00
|
|
|
//
|
2026-03-31 18:33:36 +01:00
|
|
|
// next := release.IncrementMajor("v1.2.3") // → "v2.0.0"
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
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++
|
|
|
|
|
|
2026-03-26 14:58:13 +00:00
|
|
|
return core.Sprintf("v%d.0.0", major)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ParseVersion parses a semver string into its components.
|
|
|
|
|
// Returns (major, minor, patch, prerelease, build, error).
|
2026-03-31 18:33:36 +01:00
|
|
|
//
|
|
|
|
|
// major, minor, patch, pre, build, err := release.ParseVersion("v1.2.3-alpha+001")
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
func ParseVersion(version string) (int, int, int, string, string, error) {
|
|
|
|
|
matches := semverRegex.FindStringSubmatch(version)
|
|
|
|
|
if matches == nil {
|
2026-03-16 21:03:21 +00:00
|
|
|
return 0, 0, 0, "", "", coreerr.E("release.ParseVersion", "invalid semver: "+version, nil)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.
|
2026-03-31 18:33:36 +01:00
|
|
|
//
|
|
|
|
|
// if release.ValidateVersion("v1.2.3") { ... }
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
func ValidateVersion(version string) bool {
|
|
|
|
|
return semverRegex.MatchString(version)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// normalizeVersion ensures the version starts with 'v'.
|
|
|
|
|
func normalizeVersion(version string) string {
|
2026-03-26 14:58:13 +00:00
|
|
|
if !core.HasPrefix(version, "v") {
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
return "v" + version
|
|
|
|
|
}
|
|
|
|
|
return version
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 05:46:27 +00:00
|
|
|
func getTagOnHeadWithContext(ctx context.Context, dir string) (string, error) {
|
|
|
|
|
output, err := ax.RunDir(ctx, dir, "git", "describe", "--tags", "--exact-match", "HEAD")
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
2026-03-26 17:41:53 +00:00
|
|
|
return core.Trim(output), nil
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 05:46:27 +00:00
|
|
|
func getLatestTagWithContext(ctx context.Context, dir string) (string, error) {
|
|
|
|
|
output, err := ax.RunDir(ctx, dir, "git", "describe", "--tags", "--abbrev=0")
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
2026-03-26 17:41:53 +00:00
|
|
|
return core.Trim(output), nil
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CompareVersions compares two semver strings.
|
2026-03-31 18:33:36 +01:00
|
|
|
// Returns -1 if a < b, 0 if a == b, 1 if a > b.
|
2026-03-26 17:41:53 +00:00
|
|
|
//
|
2026-03-31 18:33:36 +01:00
|
|
|
// result := release.CompareVersions("v1.2.3", "v1.2.4") // → -1
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
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 {
|
2026-03-26 14:58:13 +00:00
|
|
|
switch {
|
|
|
|
|
case a < b:
|
|
|
|
|
return -1
|
|
|
|
|
case a > b:
|
|
|
|
|
return 1
|
|
|
|
|
default:
|
|
|
|
|
return 0
|
|
|
|
|
}
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
}
|