package config import ( "fmt" "os" "path/filepath" "github.com/BurntSushi/toml" ) const DefaultChangesetDir = ".stamp" const ConfigFileName = "stamp.toml" // 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"` ChangesetDir string `toml:"changeset_dir,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"` 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 returns the configured changeset directory, defaulting to .stamp. func (c *Config) ChangesetDir() string { if c.Config.ChangesetDir != "" { return c.Config.ChangesetDir } 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 the given directory. func Load(dir string) (*Config, error) { path := filepath.Join(dir, ConfigFileName) 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 in the given directory. func Save(dir string, cfg *Config) error { path := filepath.Join(dir, ConfigFileName) 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 }