carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// Semver targets and counter iterations (release versioning spec,
2// 2026-08-08). A TARGET is a bare MAJOR.MINOR.PATCH ("0.5.0"); an ITERATION
3// is a shipped build under a target ("0.5.0-3"). Both are ordinary version
4// strings to everything else in this package — ValidVersion accepts them,
5// pointers and manifests carry them unchanged. This file only teaches the
6// package to read the structure back OUT of them.
7package release
8
9import (
10 "strconv"
11 "strings"
12)
13
14// SemVer is a parsed MAJOR.MINOR.PATCH. No prerelease/build fields: the
15// iteration counter is this package's only prerelease shape, and
16// SplitIteration owns it.
17type SemVer struct{ Major, Minor, Patch int }
18
19// ParseSemVer parses a strict bare semver: three dot-separated decimal
20// numbers, no leading zeros, nothing else. Refusals are (zero, false) —
21// version strings outside this shape are legal everywhere, they just are
22// not targets.
23func ParseSemVer(s string) (SemVer, bool) {
24 parts := strings.Split(s, ".")
25 if len(parts) != 3 {
26 return SemVer{}, false
27 }
28 var n [3]int
29 for i, p := range parts {
30 v, ok := parseCounter(p, true)
31 if !ok {
32 return SemVer{}, false
33 }
34 n[i] = v
35 }
36 return SemVer{n[0], n[1], n[2]}, true
37}
38
39// parseCounter parses a plain decimal with no sign and no leading zeros.
40// zeroOK distinguishes semver components (0 is fine) from iteration
41// counters (they start at 1).
42func parseCounter(s string, zeroOK bool) (int, bool) {
43 if s == "" || len(s) > 9 {
44 return 0, false
45 }
46 for _, c := range s {
47 if c < '0' || c > '9' {
48 return 0, false
49 }
50 }
51 if len(s) > 1 && s[0] == '0' {
52 return 0, false
53 }
54 v, err := strconv.Atoi(s)
55 if err != nil || (!zeroOK && v == 0) {
56 return 0, false
57 }
58 return v, true
59}
60
61func (v SemVer) String() string {
62 return strconv.Itoa(v.Major) + "." + strconv.Itoa(v.Minor) + "." + strconv.Itoa(v.Patch)
63}
64
65// Compare is < 0 / 0 / > 0, semver precedence (major, then minor, then patch).
66func (v SemVer) Compare(o SemVer) int {
67 switch {
68 case v.Major != o.Major:
69 return v.Major - o.Major
70 case v.Minor != o.Minor:
71 return v.Minor - o.Minor
72 default:
73 return v.Patch - o.Patch
74 }
75}
76
77// NextPatch is where the target auto-advances after a tag is cut.
78func (v SemVer) NextPatch() SemVer { return SemVer{v.Major, v.Minor, v.Patch + 1} }
79
80// SplitIteration reads "<target>-<n>" back out of an iteration version:
81// "0.5.0-3" -> (0.5.0, 3, true). Counters start at 1 and carry no leading
82// zeros, so every iteration version has exactly one spelling. Anything
83// else — a sha, a bare target, "dev" — is (zero, 0, false), which callers
84// read as "not an iteration; no tag business here".
85func SplitIteration(version string) (SemVer, int, bool) {
86 base, cnt, found := strings.Cut(version, "-")
87 if !found || strings.Contains(cnt, "-") {
88 return SemVer{}, 0, false
89 }
90 t, ok := ParseSemVer(base)
91 if !ok {
92 return SemVer{}, 0, false
93 }
94 n, ok := parseCounter(cnt, false)
95 if !ok {
96 return SemVer{}, 0, false
97 }
98 return t, n, true
99}
100
101// IterationVersion is SplitIteration's inverse: the version string minted
102// for iteration n under a target.
103func IterationVersion(t SemVer, n int) string {
104 return t.String() + "-" + strconv.Itoa(n)
105}
106