feat: initial stamp implementation

- stamp add: create .stamp/*.md changeset files (interactive + --no-interactive)
- stamp status: show pending stamps and projected next versions (plain + --json)
- stamp version: consume stamps, bump semver, update CHANGELOGs, commit
- stamp publish: create git tags, GitHub/Gitea releases, upload artifacts
- stamp comment: post/update idempotent PR comments via GitHub/Gitea API
- Supports monorepos via stamp.toml [[projects]] blocks
- Changeset files support YAML (---) and TOML (+++) frontmatter
- Semver + pre-release support (alpha, beta, rc)
- Examples and workflow documentation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Thomas
2026-03-08 20:56:23 +01:00
commit 77462f5e8a
25 changed files with 2518 additions and 0 deletions

123
internal/semver/semver.go Normal file
View File

@@ -0,0 +1,123 @@
package semver
import (
"fmt"
"strings"
goSemver "github.com/Masterminds/semver/v3"
"github.com/thokra/stamp/internal/changeset"
)
// Bump computes the next version given the current version string and a bump type.
// preID is the pre-release identifier (e.g. "alpha", "beta", "rc") used for pre-* bumps.
func Bump(current string, bump changeset.BumpType, preID string) (string, error) {
v, err := goSemver.NewVersion(current)
if err != nil {
return "", fmt.Errorf("invalid version %q: %w", current, err)
}
if preID == "" {
preID = "0"
}
switch bump {
case changeset.BumpMajor:
next := v.IncMajor()
return next.Original(), nil
case changeset.BumpMinor:
next := v.IncMinor()
return next.Original(), nil
case changeset.BumpPatch:
next := v.IncPatch()
return next.Original(), nil
case changeset.BumpPreMajor:
next := v.IncMajor()
pre, err := goSemver.NewVersion(fmt.Sprintf("%d.%d.%d-%s.0", next.Major(), next.Minor(), next.Patch(), preID))
if err != nil {
return "", err
}
return pre.Original(), nil
case changeset.BumpPreMinor:
next := v.IncMinor()
pre, err := goSemver.NewVersion(fmt.Sprintf("%d.%d.%d-%s.0", next.Major(), next.Minor(), next.Patch(), preID))
if err != nil {
return "", err
}
return pre.Original(), nil
case changeset.BumpPrePatch:
next := v.IncPatch()
pre, err := goSemver.NewVersion(fmt.Sprintf("%d.%d.%d-%s.0", next.Major(), next.Minor(), next.Patch(), preID))
if err != nil {
return "", err
}
return pre.Original(), nil
case changeset.BumpPreRelease:
// If already a pre-release with matching identifier, increment the pre-release number.
// Otherwise start a fresh pre-release on current patch.
preStr := v.Prerelease()
if preStr != "" && strings.HasPrefix(preStr, preID+".") {
parts := strings.SplitN(preStr, ".", 2)
var num int
fmt.Sscanf(parts[1], "%d", &num)
next, err := goSemver.NewVersion(fmt.Sprintf("%d.%d.%d-%s.%d",
v.Major(), v.Minor(), v.Patch(), preID, num+1))
if err != nil {
return "", err
}
return next.Original(), nil
}
// Start a new pre-release on next patch.
patch := v.IncPatch()
next, err := goSemver.NewVersion(fmt.Sprintf("%d.%d.%d-%s.0",
patch.Major(), patch.Minor(), patch.Patch(), preID))
if err != nil {
return "", err
}
return next.Original(), nil
default:
return "", fmt.Errorf("unknown bump type %q", bump)
}
}
// HighestBump returns the highest-priority bump type from a set of bumps.
// Priority: major > minor > patch > premajor > preminor > prepatch > prerelease.
func HighestBump(bumps []changeset.BumpType) changeset.BumpType {
priority := map[changeset.BumpType]int{
changeset.BumpMajor: 6,
changeset.BumpMinor: 5,
changeset.BumpPatch: 4,
changeset.BumpPreMajor: 3,
changeset.BumpPreMinor: 2,
changeset.BumpPrePatch: 1,
changeset.BumpPreRelease: 0,
}
best := changeset.BumpPreRelease
bestP := -1
for _, b := range bumps {
if p, ok := priority[b]; ok && p > bestP {
best = b
bestP = p
}
}
return best
}
// ProjectBumps aggregates bump types per project from a list of changesets.
func ProjectBumps(changesets []*changeset.Changeset) map[string][]changeset.BumpType {
result := map[string][]changeset.BumpType{}
for _, cs := range changesets {
for project, bump := range cs.Bumps {
result[project] = append(result[project], bump)
}
}
return result
}