feat(releaser): release v1.5.0 — Node.js, multi-module Maven, configurable bump rules
- Add internal/node package: reads/writes package.json version (single or multi-path via node.package_json / node.package_jsons) - Add maven.pom_paths support: update multiple pom.xml files in one release commit; pom_paths overrides pom_path; --pom flag clears pom_paths - Add git.bump_rules config: per-type control of which version component bumps (breaking/feat/fix accept "patch" or "minor"); wired through version.Next() as a new sixth parameter - Extract injectable function vars (absPath, gitAllCommits, gitCommitsSince, gitCommitFiles) to enable error-path testing without interfaces - Achieve 100% per-package statement coverage across all 12 packages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+83
-19
@@ -16,21 +16,61 @@ const filename = ".releaser.yml"
|
||||
type Config struct {
|
||||
Git GitConfig `yaml:"git"`
|
||||
Maven MavenConfig `yaml:"maven"`
|
||||
Node NodeConfig `yaml:"node"`
|
||||
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"`
|
||||
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"`
|
||||
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 GitLabConfig struct {
|
||||
@@ -64,18 +104,24 @@ 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",
|
||||
"maven.pom_path": "default",
|
||||
"gitlab.url": "default",
|
||||
"gitlab.token": "default",
|
||||
"gitlab.project": "default",
|
||||
"github.token": "default",
|
||||
"github.repo": "default",
|
||||
"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",
|
||||
"gitlab.url": "default",
|
||||
"gitlab.token": "default",
|
||||
"gitlab.project": "default",
|
||||
"github.token": "default",
|
||||
"github.repo": "default",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,9 +172,27 @@ func LoadWithSources(dir string) (Config, Sources, error) {
|
||||
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.GitLab.URL != "" {
|
||||
src["gitlab.url"] = "config file"
|
||||
}
|
||||
|
||||
@@ -123,6 +123,175 @@ func TestApplyEnvProjectPathFallback(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadWithSourcesFullConfig(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
content := `
|
||||
git:
|
||||
tag_prefix: "v"
|
||||
branch_pattern: "^release/(\\d+)$"
|
||||
commit_message: "release {version}"
|
||||
author_name: "Bot"
|
||||
author_email: "bot@example.com"
|
||||
releasable_types: ["fix", "feat"]
|
||||
bump_rules:
|
||||
breaking: "minor"
|
||||
feat: "patch"
|
||||
fix: "patch"
|
||||
maven:
|
||||
pom_path: "sub/pom.xml"
|
||||
pom_paths:
|
||||
- "a/pom.xml"
|
||||
- "b/pom.xml"
|
||||
node:
|
||||
package_json: "frontend/package.json"
|
||||
package_jsons:
|
||||
- "pkg-a/package.json"
|
||||
- "pkg-b/package.json"
|
||||
gitlab:
|
||||
url: "https://gitlab.example.com"
|
||||
token: "gitlab-token"
|
||||
project: "42"
|
||||
github:
|
||||
token: "github-token"
|
||||
repo: "owner/repo"
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, filename), []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, src, err := LoadWithSources(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantConfigFile := []string{
|
||||
"git.tag_prefix", "git.branch_pattern", "git.commit_message",
|
||||
"git.author_name", "git.author_email", "git.releasable_types",
|
||||
"git.bump_rules.breaking", "git.bump_rules.feat", "git.bump_rules.fix",
|
||||
"maven.pom_path", "maven.pom_paths",
|
||||
"node.package_json", "node.package_jsons",
|
||||
"gitlab.url", "gitlab.token", "gitlab.project",
|
||||
"github.token", "github.repo",
|
||||
}
|
||||
for _, key := range wantConfigFile {
|
||||
if got := src[key]; got != "config file" {
|
||||
t.Errorf("src[%q] = %q, want %q", key, got, "config file")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectivePomPaths(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
cfg MavenConfig
|
||||
want []string
|
||||
}{
|
||||
{"default", MavenConfig{PomPath: "pom.xml"}, []string{"pom.xml"}},
|
||||
{"single override", MavenConfig{PomPath: "sub/pom.xml"}, []string{"sub/pom.xml"}},
|
||||
{"multi overrides single", MavenConfig{PomPath: "pom.xml", PomPaths: []string{"a/pom.xml", "b/pom.xml"}}, []string{"a/pom.xml", "b/pom.xml"}},
|
||||
{"empty falls back to default", MavenConfig{}, []string{"pom.xml"}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := c.cfg.EffectivePomPaths()
|
||||
if len(got) != len(c.want) {
|
||||
t.Fatalf("got %v, want %v", got, c.want)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != c.want[i] {
|
||||
t.Errorf("[%d] got %q, want %q", i, got[i], c.want[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeEffectivePaths(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
cfg NodeConfig
|
||||
want []string
|
||||
}{
|
||||
{"empty — opt-in, skip by default", NodeConfig{}, nil},
|
||||
{"single", NodeConfig{PackageJSON: "package.json"}, []string{"package.json"}},
|
||||
{"multi overrides single", NodeConfig{PackageJSON: "package.json", PackageJSONs: []string{"a/package.json", "b/package.json"}}, []string{"a/package.json", "b/package.json"}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := c.cfg.EffectivePaths()
|
||||
if len(got) != len(c.want) {
|
||||
t.Fatalf("got %v, want %v", got, c.want)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != c.want[i] {
|
||||
t.Errorf("[%d] got %q, want %q", i, got[i], c.want[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvWithSourcesCIProjectID(t *testing.T) {
|
||||
t.Setenv("CI_PROJECT_ID", "123")
|
||||
t.Setenv("CI_PROJECT_PATH", "")
|
||||
|
||||
cfg := defaults()
|
||||
src := defaultSources()
|
||||
cfg.ApplyEnvWithSources(src)
|
||||
|
||||
if src["gitlab.project"] != "env: CI_PROJECT_ID" {
|
||||
t.Errorf("src[gitlab.project] = %q, want %q", src["gitlab.project"], "env: CI_PROJECT_ID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvWithSourcesGitHubToken(t *testing.T) {
|
||||
t.Setenv("GITHUB_TOKEN", "ghtoken")
|
||||
|
||||
cfg := defaults()
|
||||
src := defaultSources()
|
||||
cfg.ApplyEnvWithSources(src)
|
||||
|
||||
if src["github.token"] != "env: GITHUB_TOKEN" {
|
||||
t.Errorf("src[github.token] = %q, want %q", src["github.token"], "env: GITHUB_TOKEN")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvWithSourcesCIProjectPath(t *testing.T) {
|
||||
t.Setenv("CI_PROJECT_ID", "")
|
||||
t.Setenv("CI_PROJECT_PATH", "group/project")
|
||||
|
||||
cfg := defaults()
|
||||
src := defaultSources()
|
||||
cfg.ApplyEnvWithSources(src)
|
||||
|
||||
if src["gitlab.project"] != "env: CI_PROJECT_PATH" {
|
||||
t.Errorf("src[gitlab.project] = %q, want %q", src["gitlab.project"], "env: CI_PROJECT_PATH")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvWithSourcesGitLabToken(t *testing.T) {
|
||||
t.Setenv("GITLAB_TOKEN", "mytoken")
|
||||
|
||||
cfg := defaults()
|
||||
src := defaultSources()
|
||||
cfg.ApplyEnvWithSources(src)
|
||||
|
||||
if src["gitlab.token"] != "env: GITLAB_TOKEN" {
|
||||
t.Errorf("src[gitlab.token] = %q, want %q", src["gitlab.token"], "env: GITLAB_TOKEN")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvWithSourcesCIServerURL(t *testing.T) {
|
||||
t.Setenv("CI_SERVER_URL", "https://gitlab.example.com")
|
||||
|
||||
cfg := defaults()
|
||||
src := defaultSources()
|
||||
cfg.ApplyEnvWithSources(src)
|
||||
|
||||
if src["gitlab.url"] != "env: CI_SERVER_URL" {
|
||||
t.Errorf("src[gitlab.url] = %q, want %q", src["gitlab.url"], "env: CI_SERVER_URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPartialOverride(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// Only override tag_prefix — commit_message should keep its default
|
||||
|
||||
Reference in New Issue
Block a user