dfdf2b019a
Add internal/gradle package with ReadVersion and WriteVersion for build.gradle (Groovy DSL, single-quoted) and build.gradle.kts (Kotlin DSL, double-quoted). Quote style is preserved on write. regexp.QuoteMeta ensures version strings with dots or special characters are safe. Config follows the established multi-value pattern: - gradle.build_file: single path (opt-in, no default) - gradle.build_files: list for multi-module projects (overrides build_file) - --gradle flag overrides build_file and clears build_files 100% per-package statement coverage maintained across all 13 packages; FuzzReadVersion and FuzzWriteVersion added per fuzzing guidelines. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
288 lines
7.8 KiB
Go
288 lines
7.8 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"git.k3nny.fr/releaser/internal/branch"
|
|
)
|
|
|
|
const filename = ".releaser.yml"
|
|
|
|
type Config struct {
|
|
Git GitConfig `yaml:"git"`
|
|
Maven MavenConfig `yaml:"maven"`
|
|
Node NodeConfig `yaml:"node"`
|
|
Gradle GradleConfig `yaml:"gradle"`
|
|
GitLab GitLabConfig `yaml:"gitlab"`
|
|
GitHub GitHubConfig `yaml:"github"`
|
|
}
|
|
|
|
type GitConfig struct {
|
|
TagPrefix string `yaml:"tag_prefix"`
|
|
BranchPattern string `yaml:"branch_pattern"`
|
|
CommitMessage string `yaml:"commit_message"`
|
|
AuthorName string `yaml:"author_name"`
|
|
AuthorEmail string `yaml:"author_email"`
|
|
ReleasableTypes []string `yaml:"releasable_types"`
|
|
BumpRules BumpRulesConfig `yaml:"bump_rules"`
|
|
}
|
|
|
|
// BumpRulesConfig controls what version component each commit type bumps.
|
|
// Valid values: "patch" (default) or "minor".
|
|
type BumpRulesConfig struct {
|
|
Breaking string `yaml:"breaking"`
|
|
Feat string `yaml:"feat"`
|
|
Fix string `yaml:"fix"`
|
|
}
|
|
|
|
type MavenConfig struct {
|
|
PomPath string `yaml:"pom_path"` // single path (default: "pom.xml")
|
|
PomPaths []string `yaml:"pom_paths"` // multiple paths; overrides PomPath when set
|
|
}
|
|
|
|
// EffectivePomPaths returns the list of pom.xml paths to process.
|
|
// PomPaths takes precedence over PomPath; falls back to ["pom.xml"].
|
|
func (m MavenConfig) EffectivePomPaths() []string {
|
|
if len(m.PomPaths) > 0 {
|
|
return m.PomPaths
|
|
}
|
|
if m.PomPath != "" {
|
|
return []string{m.PomPath}
|
|
}
|
|
return []string{"pom.xml"}
|
|
}
|
|
|
|
type NodeConfig struct {
|
|
PackageJSON string `yaml:"package_json"` // single path
|
|
PackageJSONs []string `yaml:"package_jsons"` // multiple paths; overrides PackageJSON when set
|
|
}
|
|
|
|
// EffectivePaths returns the list of package.json paths to process.
|
|
// Returns nil when no node paths are configured (node processing is opt-in).
|
|
func (n NodeConfig) EffectivePaths() []string {
|
|
if len(n.PackageJSONs) > 0 {
|
|
return n.PackageJSONs
|
|
}
|
|
if n.PackageJSON != "" {
|
|
return []string{n.PackageJSON}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type GradleConfig struct {
|
|
BuildFile string `yaml:"build_file"` // single path (opt-in, no default)
|
|
BuildFiles []string `yaml:"build_files"` // multiple paths; overrides BuildFile
|
|
}
|
|
|
|
// EffectiveBuildFiles returns the list of Gradle build file paths to process.
|
|
// Returns nil when no gradle paths are configured (gradle processing is opt-in).
|
|
func (g GradleConfig) EffectiveBuildFiles() []string {
|
|
if len(g.BuildFiles) > 0 {
|
|
return g.BuildFiles
|
|
}
|
|
if g.BuildFile != "" {
|
|
return []string{g.BuildFile}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type GitLabConfig struct {
|
|
URL string `yaml:"url"`
|
|
Token string `yaml:"token"`
|
|
Project string `yaml:"project"`
|
|
}
|
|
|
|
type GitHubConfig struct {
|
|
Token string `yaml:"token"`
|
|
Repo string `yaml:"repo"` // "owner/repo"
|
|
}
|
|
|
|
func defaults() Config {
|
|
return Config{
|
|
Git: GitConfig{
|
|
TagPrefix: "",
|
|
BranchPattern: branch.DefaultBranchPattern,
|
|
CommitMessage: "chore(release): {version} [skip ci]",
|
|
},
|
|
Maven: MavenConfig{
|
|
PomPath: "pom.xml",
|
|
},
|
|
}
|
|
}
|
|
|
|
// Sources records where each config value came from.
|
|
// Keys are "section.field" (e.g. "git.tag_prefix").
|
|
// Values are one of: "default", "config file", "env: VARNAME", "flag: --flag-name".
|
|
type Sources map[string]string
|
|
|
|
func defaultSources() Sources {
|
|
return Sources{
|
|
"git.tag_prefix": "default",
|
|
"git.branch_pattern": "default",
|
|
"git.commit_message": "default",
|
|
"git.author_name": "default",
|
|
"git.author_email": "default",
|
|
"git.releasable_types": "default",
|
|
"git.bump_rules.breaking": "default",
|
|
"git.bump_rules.feat": "default",
|
|
"git.bump_rules.fix": "default",
|
|
"maven.pom_path": "default",
|
|
"maven.pom_paths": "default",
|
|
"node.package_json": "default",
|
|
"node.package_jsons": "default",
|
|
"gradle.build_file": "default",
|
|
"gradle.build_files": "default",
|
|
"gitlab.url": "default",
|
|
"gitlab.token": "default",
|
|
"gitlab.project": "default",
|
|
"github.token": "default",
|
|
"github.repo": "default",
|
|
}
|
|
}
|
|
|
|
// Load reads .releaser.yml from dir and merges it over the defaults.
|
|
// Missing file is not an error — defaults are returned as-is.
|
|
func Load(dir string) (Config, error) {
|
|
cfg, _, err := LoadWithSources(dir)
|
|
return cfg, err
|
|
}
|
|
|
|
// LoadWithSources is like Load but also returns a Sources map recording where each
|
|
// value came from ("default" or "config file").
|
|
func LoadWithSources(dir string) (Config, Sources, error) {
|
|
cfg := defaults()
|
|
src := defaultSources()
|
|
|
|
data, err := os.ReadFile(filepath.Join(dir, filename))
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return cfg, src, nil
|
|
}
|
|
if err != nil {
|
|
return cfg, src, fmt.Errorf("read %s: %w", filename, err)
|
|
}
|
|
|
|
// Unmarshal into cfg (merges over defaults).
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
return cfg, src, fmt.Errorf("parse %s: %w", filename, err)
|
|
}
|
|
|
|
// Detect which fields the file explicitly set by unmarshaling into a zero overlay.
|
|
var overlay Config
|
|
_ = yaml.Unmarshal(data, &overlay)
|
|
if overlay.Git.TagPrefix != "" {
|
|
src["git.tag_prefix"] = "config file"
|
|
}
|
|
if overlay.Git.BranchPattern != "" {
|
|
src["git.branch_pattern"] = "config file"
|
|
}
|
|
if overlay.Git.CommitMessage != "" {
|
|
src["git.commit_message"] = "config file"
|
|
}
|
|
if overlay.Git.AuthorName != "" {
|
|
src["git.author_name"] = "config file"
|
|
}
|
|
if overlay.Git.AuthorEmail != "" {
|
|
src["git.author_email"] = "config file"
|
|
}
|
|
if len(overlay.Git.ReleasableTypes) > 0 {
|
|
src["git.releasable_types"] = "config file"
|
|
}
|
|
if overlay.Git.BumpRules.Breaking != "" {
|
|
src["git.bump_rules.breaking"] = "config file"
|
|
}
|
|
if overlay.Git.BumpRules.Feat != "" {
|
|
src["git.bump_rules.feat"] = "config file"
|
|
}
|
|
if overlay.Git.BumpRules.Fix != "" {
|
|
src["git.bump_rules.fix"] = "config file"
|
|
}
|
|
if overlay.Maven.PomPath != "" {
|
|
src["maven.pom_path"] = "config file"
|
|
}
|
|
if len(overlay.Maven.PomPaths) > 0 {
|
|
src["maven.pom_paths"] = "config file"
|
|
}
|
|
if overlay.Node.PackageJSON != "" {
|
|
src["node.package_json"] = "config file"
|
|
}
|
|
if len(overlay.Node.PackageJSONs) > 0 {
|
|
src["node.package_jsons"] = "config file"
|
|
}
|
|
if overlay.Gradle.BuildFile != "" {
|
|
src["gradle.build_file"] = "config file"
|
|
}
|
|
if len(overlay.Gradle.BuildFiles) > 0 {
|
|
src["gradle.build_files"] = "config file"
|
|
}
|
|
if overlay.GitLab.URL != "" {
|
|
src["gitlab.url"] = "config file"
|
|
}
|
|
if overlay.GitLab.Token != "" {
|
|
src["gitlab.token"] = "config file"
|
|
}
|
|
if overlay.GitLab.Project != "" {
|
|
src["gitlab.project"] = "config file"
|
|
}
|
|
if overlay.GitHub.Token != "" {
|
|
src["github.token"] = "config file"
|
|
}
|
|
if overlay.GitHub.Repo != "" {
|
|
src["github.repo"] = "config file"
|
|
}
|
|
|
|
return cfg, src, nil
|
|
}
|
|
|
|
// ApplyEnv fills empty GitLab and GitHub fields from environment variables.
|
|
// Values already set in the config file are never overwritten.
|
|
func (c *Config) ApplyEnv() {
|
|
c.ApplyEnvWithSources(nil)
|
|
}
|
|
|
|
// ApplyEnvWithSources is like ApplyEnv but records the env var name in src for each
|
|
// field it fills. src may be nil.
|
|
func (c *Config) ApplyEnvWithSources(src Sources) {
|
|
if c.GitLab.Token == "" {
|
|
if v := os.Getenv("GITLAB_TOKEN"); v != "" {
|
|
c.GitLab.Token = v
|
|
if src != nil {
|
|
src["gitlab.token"] = "env: GITLAB_TOKEN"
|
|
}
|
|
}
|
|
}
|
|
if c.GitLab.URL == "" {
|
|
if v := os.Getenv("CI_SERVER_URL"); v != "" {
|
|
c.GitLab.URL = v
|
|
if src != nil {
|
|
src["gitlab.url"] = "env: CI_SERVER_URL"
|
|
}
|
|
}
|
|
}
|
|
if c.GitLab.Project == "" {
|
|
if id := os.Getenv("CI_PROJECT_ID"); id != "" {
|
|
c.GitLab.Project = id
|
|
if src != nil {
|
|
src["gitlab.project"] = "env: CI_PROJECT_ID"
|
|
}
|
|
} else if p := os.Getenv("CI_PROJECT_PATH"); p != "" {
|
|
c.GitLab.Project = p
|
|
if src != nil {
|
|
src["gitlab.project"] = "env: CI_PROJECT_PATH"
|
|
}
|
|
}
|
|
}
|
|
if c.GitHub.Token == "" {
|
|
if v := os.Getenv("GITHUB_TOKEN"); v != "" {
|
|
c.GitHub.Token = v
|
|
if src != nil {
|
|
src["github.token"] = "env: GITHUB_TOKEN"
|
|
}
|
|
}
|
|
}
|
|
}
|