Files
stamp/cmd/stamp/cmd_version.go
Thomas 77462f5e8a 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>
2026-03-08 20:56:23 +01:00

139 lines
3.5 KiB
Go

package main
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"github.com/urfave/cli/v3"
"github.com/thokra/stamp/internal/changeset"
"github.com/thokra/stamp/internal/changelog"
"github.com/thokra/stamp/internal/config"
"github.com/thokra/stamp/internal/git"
"github.com/thokra/stamp/internal/semver"
)
func versionCmd() *cli.Command {
return &cli.Command{
Name: "version",
Usage: "consume stamps, bump versions, and update changelogs",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "snapshot",
Usage: "pre-release identifier (e.g. alpha, beta, rc) for snapshot releases",
},
&cli.BoolFlag{
Name: "no-commit",
Usage: "skip the git commit after versioning",
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
repoRoot, err := findRepoRoot(".")
if err != nil {
return err
}
cfg, err := config.Load(repoRoot)
if err != nil {
return err
}
stampDir := filepath.Join(repoRoot, cfg.ChangesetDir())
changesets, err := changeset.ReadAll(stampDir)
if err != nil && !os.IsNotExist(err) {
return err
}
if len(changesets) == 0 {
fmt.Println("No pending stamps found.")
return nil
}
preID := cmd.String("snapshot")
projectBumps := semver.ProjectBumps(changesets)
now := time.Now()
var changedFiles []string
var bumped []string
for i := range cfg.Projects {
project := &cfg.Projects[i]
bumps := projectBumps[project.Name]
if len(bumps) == 0 {
continue
}
highest := semver.HighestBump(bumps)
if preID != "" {
highest = changeset.BumpPreRelease
}
nextVer, err := semver.Bump(project.Version, highest, preID)
if err != nil {
return fmt.Errorf("bumping %s: %w", project.Name, err)
}
// Build changelog body from all matching changesets.
var body string
for _, cs := range changesets {
if _, ok := cs.Bumps[project.Name]; ok {
if cs.Description != "" {
body += "- " + cs.Description + "\n"
}
}
}
clPath := project.ChangelogPath(repoRoot)
if err := changelog.Append(clPath, changelog.Entry{
Version: nextVer,
Date: now,
Description: body,
}); err != nil {
return fmt.Errorf("updating changelog for %s: %w", project.Name, err)
}
changedFiles = append(changedFiles, clPath)
fmt.Printf(" %s: %s → %s\n", project.Name, project.Version, nextVer)
project.Version = nextVer
bumped = append(bumped, project.Name)
}
if len(bumped) == 0 {
fmt.Println("No projects were bumped.")
return nil
}
// Update stamp.toml with new versions.
cfgPath := filepath.Join(repoRoot, config.ConfigFileName)
if err := config.Save(repoRoot, cfg); err != nil {
return fmt.Errorf("saving stamp.toml: %w", err)
}
changedFiles = append(changedFiles, cfgPath)
// Delete consumed stamp files.
if err := changeset.DeleteAll(stampDir); err != nil {
return fmt.Errorf("deleting stamp files: %w", err)
}
changedFiles = append(changedFiles, stampDir)
if cmd.Bool("no-commit") {
fmt.Println("\nVersioning complete (no commit).")
return nil
}
// Stage and commit.
if err := git.Add(repoRoot, changedFiles...); err != nil {
return fmt.Errorf("staging files: %w", err)
}
commitMsg := fmt.Sprintf("chore: version packages\n\nBumped: %v", bumped)
if err := git.Commit(repoRoot, commitMsg); err != nil {
return fmt.Errorf("committing: %w", err)
}
fmt.Println("\n✓ Committed version bump.")
return nil
},
}
}