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 }