Files
stamp/internal/config/config.go
Thomas 8049c505a0 Add TUI add and preview commands
Use charmbracelet/huh for an interactive add flow. Add a preview
subcommand to enter/exit per-project pre-release tags. Move stamp.toml
into .stamp/ and add ConfigPath helper. Prefer --snapshot then project
PreTag when computing versions and promote bumps to pre-release when
appropriate. Export CurrentVersion and add required TUI deps to go.mod.
2026-03-12 22:59:20 +01:00

158 lines
4.0 KiB
Go

package config
import (
"fmt"
"os"
"path/filepath"
"github.com/BurntSushi/toml"
)
const (
DefaultChangesetDir = ".stamp"
ConfigFileName = "stamp.toml"
)
// ConfigPath returns the path to stamp.toml inside the changeset directory.
func ConfigPath(repoRoot string) string {
return filepath.Join(repoRoot, DefaultChangesetDir, ConfigFileName)
}
// Config is the root configuration parsed from stamp.toml.
type Config struct {
Config GlobalConfig `toml:"config"`
Projects []Project `toml:"projects"`
}
// GlobalConfig holds repo-level settings.
type GlobalConfig struct {
BaseBranch string `toml:"base_branch,omitempty"`
}
// Project represents a single project in the monorepo.
type Project struct {
Name string `toml:"name"`
Path string `toml:"path"`
Version string `toml:"version"`
Changelog string `toml:"changelog,omitempty"`
PreTag string `toml:"pre_tag,omitempty"`
Publish PublishConfig `toml:"publish,omitempty"`
}
// PublishConfig controls what happens during stamp publish.
type PublishConfig struct {
Tags *bool `toml:"tags"`
Releases *bool `toml:"releases"`
Artifacts []string `toml:"artifacts"`
}
// ChangesetDir always returns the default changeset directory.
// The config file lives inside this directory, so it is always known.
func (c *Config) ChangesetDir() string {
return DefaultChangesetDir
}
// BaseBranch returns the configured base branch, defaulting to "main".
func (c *Config) BaseBranch() string {
if c.Config.BaseBranch != "" {
return c.Config.BaseBranch
}
return "main"
}
// FindProject returns the project with the given name, or nil if not found.
func (c *Config) FindProject(name string) *Project {
for i := range c.Projects {
if c.Projects[i].Name == name {
return &c.Projects[i]
}
}
return nil
}
// ProjectNames returns all project names.
func (c *Config) ProjectNames() []string {
names := make([]string, len(c.Projects))
for i, p := range c.Projects {
names[i] = p.Name
}
return names
}
// ChangelogPath returns the path to CHANGELOG.md for a project, relative to repoRoot.
func (p *Project) ChangelogPath(repoRoot string) string {
changelog := p.Changelog
if changelog == "" {
changelog = "CHANGELOG.md"
}
return filepath.Join(repoRoot, p.Path, changelog)
}
// PublishTags returns true unless explicitly disabled.
func (p *PublishConfig) PublishTags() bool {
return p.Tags == nil || *p.Tags
}
// PublishReleases returns true unless explicitly disabled.
func (p *PublishConfig) PublishReleases() bool {
return p.Releases == nil || *p.Releases
}
// Load reads and validates stamp.toml from inside the changeset directory.
func Load(repoRoot string) (*Config, error) {
path := ConfigPath(repoRoot)
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", path, err)
}
var cfg Config
if err := toml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parsing %s: %w", path, err)
}
if err := validate(&cfg); err != nil {
return nil, fmt.Errorf("invalid %s: %w", path, err)
}
return &cfg, nil
}
// Save writes the config back to stamp.toml inside the changeset directory.
func Save(repoRoot string, cfg *Config) error {
dir := filepath.Join(repoRoot, DefaultChangesetDir)
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("creating %s: %w", dir, err)
}
path := ConfigPath(repoRoot)
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("creating %s: %w", path, err)
}
defer f.Close()
enc := toml.NewEncoder(f)
return enc.Encode(cfg)
}
func validate(cfg *Config) error {
if len(cfg.Projects) == 0 {
return fmt.Errorf("at least one [[projects]] entry is required")
}
seen := map[string]bool{}
for i, p := range cfg.Projects {
if p.Name == "" {
return fmt.Errorf("projects[%d]: name is required", i)
}
if seen[p.Name] {
return fmt.Errorf("projects[%d]: duplicate project name %q", i, p.Name)
}
seen[p.Name] = true
if p.Version == "" {
return fmt.Errorf("project %q: version is required", p.Name)
}
}
return nil
}