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:
+116
-25
@@ -19,6 +19,7 @@ import (
|
||||
"git.k3nny.fr/releaser/internal/gitutil"
|
||||
"git.k3nny.fr/releaser/internal/glclient"
|
||||
"git.k3nny.fr/releaser/internal/maven"
|
||||
"git.k3nny.fr/releaser/internal/node"
|
||||
"git.k3nny.fr/releaser/internal/notes"
|
||||
semver "git.k3nny.fr/releaser/internal/version"
|
||||
)
|
||||
@@ -50,10 +51,33 @@ git:
|
||||
# - feat
|
||||
# - breaking
|
||||
|
||||
# Configure which version component each commit type bumps.
|
||||
# Valid values: "patch" (default) or "minor".
|
||||
# bump_rules:
|
||||
# breaking: "minor" # bump minor version instead of patch on breaking changes
|
||||
# feat: "patch"
|
||||
# fix: "patch"
|
||||
|
||||
maven:
|
||||
# Path to pom.xml, relative to the repository root.
|
||||
# Single pom.xml path, relative to the repository root.
|
||||
# pom_path: "pom.xml"
|
||||
|
||||
# Multiple pom.xml paths for multi-module projects (overrides pom_path).
|
||||
# pom_paths:
|
||||
# - "pom.xml"
|
||||
# - "module-a/pom.xml"
|
||||
# - "module-b/pom.xml"
|
||||
|
||||
node:
|
||||
# Single package.json path (node processing is opt-in — no default).
|
||||
# package_json: "package.json"
|
||||
|
||||
# Multiple package.json paths for monorepos (overrides package_json).
|
||||
# package_jsons:
|
||||
# - "package.json"
|
||||
# - "packages/frontend/package.json"
|
||||
# - "packages/backend/package.json"
|
||||
|
||||
gitlab:
|
||||
# GitLab instance URL. Falls back to the CI_SERVER_URL environment variable.
|
||||
# url: "https://gitlab.example.com"
|
||||
@@ -84,6 +108,14 @@ var (
|
||||
// exitFn is a variable so tests can intercept os.Exit calls.
|
||||
var exitFn = os.Exit
|
||||
|
||||
// injectable function variables for testing error paths.
|
||||
var (
|
||||
absPath = filepath.Abs
|
||||
gitAllCommits = gitutil.AllCommits
|
||||
gitCommitsSince = gitutil.CommitsSince
|
||||
gitCommitFiles = gitutil.CommitFiles
|
||||
)
|
||||
|
||||
// releasePublisher is implemented by both glclient and ghclient.
|
||||
type releasePublisher interface {
|
||||
CreateRelease(ctx context.Context, tagName, body string) error
|
||||
@@ -215,7 +247,32 @@ func printVerboseConfig(cfg config.Config, src config.Sources) {
|
||||
}
|
||||
return strings.Join(cfg.Git.ReleasableTypes, ", ")
|
||||
}()},
|
||||
{"maven.pom_path", cfg.Maven.PomPath},
|
||||
{"git.bump_rules.breaking", func() string {
|
||||
if cfg.Git.BumpRules.Breaking == "" {
|
||||
return "patch"
|
||||
}
|
||||
return cfg.Git.BumpRules.Breaking
|
||||
}()},
|
||||
{"git.bump_rules.feat", func() string {
|
||||
if cfg.Git.BumpRules.Feat == "" {
|
||||
return "patch"
|
||||
}
|
||||
return cfg.Git.BumpRules.Feat
|
||||
}()},
|
||||
{"git.bump_rules.fix", func() string {
|
||||
if cfg.Git.BumpRules.Fix == "" {
|
||||
return "patch"
|
||||
}
|
||||
return cfg.Git.BumpRules.Fix
|
||||
}()},
|
||||
{"maven.pom_paths", strings.Join(cfg.Maven.EffectivePomPaths(), ", ")},
|
||||
{"node.paths", func() string {
|
||||
paths := cfg.Node.EffectivePaths()
|
||||
if len(paths) == 0 {
|
||||
return "(not configured)"
|
||||
}
|
||||
return strings.Join(paths, ", ")
|
||||
}()},
|
||||
{"gitlab.url", cfg.GitLab.URL},
|
||||
{"gitlab.token", func() string {
|
||||
if cfg.GitLab.Token != "" {
|
||||
@@ -257,11 +314,25 @@ func initConfig(absRepo string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseBumpRules(rules config.BumpRulesConfig) map[commits.Type]semver.BumpLevel {
|
||||
m := map[commits.Type]semver.BumpLevel{}
|
||||
if rules.Breaking == "minor" {
|
||||
m[commits.TypeBreaking] = semver.BumpMinor
|
||||
}
|
||||
if rules.Feat == "minor" {
|
||||
m[commits.TypeFeat] = semver.BumpMinor
|
||||
}
|
||||
if rules.Fix == "minor" {
|
||||
m[commits.TypeFix] = semver.BumpMinor
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func run(o options) error {
|
||||
logHeader(version)
|
||||
|
||||
// --- Config ---
|
||||
absRepo, err := filepath.Abs(o.repoPath)
|
||||
absRepo, err := absPath(o.repoPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve repo path: %w", err)
|
||||
}
|
||||
@@ -286,7 +357,8 @@ func run(o options) error {
|
||||
}
|
||||
if o.pomOverride != "" {
|
||||
cfg.Maven.PomPath = o.pomOverride
|
||||
src["maven.pom_path"] = "flag: --pom"
|
||||
cfg.Maven.PomPaths = nil
|
||||
src["maven.pom_paths"] = "flag: --pom"
|
||||
}
|
||||
if o.patternSet {
|
||||
cfg.Git.BranchPattern = o.patternFlag
|
||||
@@ -345,9 +417,9 @@ func run(o options) error {
|
||||
// --- Commit range ---
|
||||
var messages []string
|
||||
if lastTag == "" {
|
||||
messages, err = gitutil.AllCommits(repo)
|
||||
messages, err = gitAllCommits(repo)
|
||||
} else {
|
||||
messages, err = gitutil.CommitsSince(repo, lastTag)
|
||||
messages, err = gitCommitsSince(repo, lastTag)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("read commits: %w", err)
|
||||
@@ -398,7 +470,7 @@ func run(o options) error {
|
||||
}
|
||||
|
||||
releasable := commits.ReleasableSet(cfg.Git.ReleasableTypes)
|
||||
nextVersion, ok := semver.Next(info.Major, info.Minor, currentPatch, types, releasable)
|
||||
nextVersion, ok := semver.Next(info.Major, info.Minor, currentPatch, types, releasable, parseBumpRules(cfg.Git.BumpRules))
|
||||
if !ok {
|
||||
logWarn("no releasable commits found")
|
||||
return errNothingToRelease
|
||||
@@ -440,27 +512,46 @@ func run(o options) error {
|
||||
if !o.tagOnly {
|
||||
var filesToCommit []string
|
||||
|
||||
// pom.xml
|
||||
pomPath := filepath.Join(absRepo, cfg.Maven.PomPath)
|
||||
_, statErr := os.Stat(pomPath)
|
||||
hasPom := !errors.Is(statErr, os.ErrNotExist)
|
||||
if statErr != nil && hasPom {
|
||||
return fmt.Errorf("check pom path: %w", statErr)
|
||||
// pom.xml (supports multi-module via pom_paths)
|
||||
anyPom := false
|
||||
for _, relPomPath := range cfg.Maven.EffectivePomPaths() {
|
||||
pomPath := filepath.Join(absRepo, relPomPath)
|
||||
_, statErr := os.Stat(pomPath)
|
||||
hasPom := !errors.Is(statErr, os.ErrNotExist)
|
||||
if statErr != nil && hasPom {
|
||||
return fmt.Errorf("check pom path: %w", statErr)
|
||||
}
|
||||
if hasPom {
|
||||
anyPom = true
|
||||
currentPomVersion, err := maven.ReadVersion(pomPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read pom version: %w", err)
|
||||
}
|
||||
if err := maven.WriteVersion(pomPath, currentPomVersion, nextVersion); err != nil {
|
||||
return fmt.Errorf("update pom version: %w", err)
|
||||
}
|
||||
logDone("%s: %s → %s", relPomPath, currentPomVersion, nextVersion)
|
||||
filesToCommit = append(filesToCommit, relPomPath)
|
||||
}
|
||||
}
|
||||
if hasPom {
|
||||
currentPomVersion, err := maven.ReadVersion(pomPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read pom version: %w", err)
|
||||
}
|
||||
if err := maven.WriteVersion(pomPath, currentPomVersion, nextVersion); err != nil {
|
||||
return fmt.Errorf("update pom version: %w", err)
|
||||
}
|
||||
logDone("pom.xml: %s → %s", currentPomVersion, nextVersion)
|
||||
filesToCommit = append(filesToCommit, cfg.Maven.PomPath)
|
||||
} else {
|
||||
if !anyPom {
|
||||
logWarn("no pom.xml — skipping version bump")
|
||||
}
|
||||
|
||||
// package.json (opt-in via node.package_json / node.package_jsons)
|
||||
for _, relPkgPath := range cfg.Node.EffectivePaths() {
|
||||
pkgPath := filepath.Join(absRepo, relPkgPath)
|
||||
currentNodeVersion, err := node.ReadVersion(pkgPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read package.json version: %w", err)
|
||||
}
|
||||
if err := node.WriteVersion(pkgPath, currentNodeVersion, nextVersion); err != nil {
|
||||
return fmt.Errorf("update package.json version: %w", err)
|
||||
}
|
||||
logDone("%s: %s → %s", relPkgPath, currentNodeVersion, nextVersion)
|
||||
filesToCommit = append(filesToCommit, relPkgPath)
|
||||
}
|
||||
|
||||
// CHANGELOG.md
|
||||
changelogAbsPath := filepath.Join(absRepo, o.changelogFile)
|
||||
if err := changelog.Update(changelogAbsPath, nextTag, nextVersion, messages); err != nil {
|
||||
@@ -483,7 +574,7 @@ func run(o options) error {
|
||||
authorEmail = cfg.Git.AuthorEmail
|
||||
}
|
||||
commitMsg := strings.ReplaceAll(cfg.Git.CommitMessage, "{version}", nextTag)
|
||||
if _, err := gitutil.CommitFiles(repo, filesToCommit, commitMsg, authorName, authorEmail); err != nil {
|
||||
if _, err := gitCommitFiles(repo, filesToCommit, commitMsg, authorName, authorEmail); err != nil {
|
||||
return fmt.Errorf("commit: %w", err)
|
||||
}
|
||||
logDone("committed: %s", commitMsg)
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -15,6 +16,8 @@ import (
|
||||
gitcfg "github.com/go-git/go-git/v5/config"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
|
||||
"git.k3nny.fr/releaser/internal/config"
|
||||
)
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||
@@ -742,3 +745,458 @@ func TestRunVerbose(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── ui.go coverage ────────────────────────────────────────────────────────────
|
||||
|
||||
func TestPaintColor(t *testing.T) {
|
||||
old := useColor
|
||||
useColor = true
|
||||
defer func() { useColor = old }()
|
||||
|
||||
got := paint(ansiGreen, "hello")
|
||||
if !strings.Contains(got, "hello") || !strings.Contains(got, ansiReset) || !strings.Contains(got, ansiGreen) {
|
||||
t.Errorf("paint with color = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFmtSourceEnv(t *testing.T) {
|
||||
old := useColor
|
||||
useColor = false
|
||||
defer func() { useColor = old }()
|
||||
|
||||
got := fmtSource("env: GITLAB_TOKEN")
|
||||
if got != "[env: GITLAB_TOKEN]" {
|
||||
t.Errorf("fmtSource env = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFmtSourceFlag(t *testing.T) {
|
||||
old := useColor
|
||||
useColor = false
|
||||
defer func() { useColor = old }()
|
||||
|
||||
got := fmtSource("flag: --tag-prefix")
|
||||
if got != "[flag: --tag-prefix]" {
|
||||
t.Errorf("fmtSource flag = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFmtSourceConfigFile(t *testing.T) {
|
||||
old := useColor
|
||||
useColor = false
|
||||
defer func() { useColor = old }()
|
||||
|
||||
got := fmtSource("config file")
|
||||
if got != "[config file]" {
|
||||
t.Errorf("fmtSource config file = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// ── buildPublisher coverage ───────────────────────────────────────────────────
|
||||
|
||||
func TestBuildPublisherGitHub(t *testing.T) {
|
||||
cfg := config.Config{
|
||||
GitHub: config.GitHubConfig{Token: "ghtoken", Repo: "owner/repo"},
|
||||
}
|
||||
pub, err := buildPublisher(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("buildPublisher GitHub: %v", err)
|
||||
}
|
||||
if pub == nil {
|
||||
t.Fatal("expected non-nil publisher for GitHub config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPublisherGitLabNoToken(t *testing.T) {
|
||||
cfg := config.Config{
|
||||
GitLab: config.GitLabConfig{URL: "https://gitlab.example.com", Project: "42"},
|
||||
}
|
||||
_, err := buildPublisher(cfg)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when GitLab URL+Project set but token is empty")
|
||||
}
|
||||
}
|
||||
|
||||
// ── printVerboseConfig coverage ───────────────────────────────────────────────
|
||||
|
||||
func TestPrintVerboseConfigDirect(t *testing.T) {
|
||||
// Use a sparse Sources map (missing keys → source == "" → hits "default" branch).
|
||||
// Also set non-empty ReleasableTypes and both tokens to cover those branches.
|
||||
cfg := config.Config{
|
||||
Git: config.GitConfig{
|
||||
ReleasableTypes: []string{"fix", "feat"},
|
||||
},
|
||||
GitLab: config.GitLabConfig{Token: "secret"},
|
||||
GitHub: config.GitHubConfig{Token: "ghsecret"},
|
||||
}
|
||||
src := config.Sources{} // empty → all lookups return ""
|
||||
|
||||
old := os.Stderr
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stderr = w
|
||||
|
||||
printVerboseConfig(cfg, src)
|
||||
|
||||
w.Close()
|
||||
os.Stderr = old
|
||||
out, _ := io.ReadAll(r)
|
||||
|
||||
if !strings.Contains(string(out), "fix, feat") {
|
||||
t.Error("expected releasable types joined in output")
|
||||
}
|
||||
if !strings.Contains(string(out), "(set)") {
|
||||
t.Error("expected '(set)' for configured tokens")
|
||||
}
|
||||
}
|
||||
|
||||
// ── initConfig coverage ───────────────────────────────────────────────────────
|
||||
|
||||
func TestInitConfigWriteFails(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
os.Chmod(dir, 0555)
|
||||
defer os.Chmod(dir, 0755)
|
||||
|
||||
err := initConfig(dir)
|
||||
if err == nil {
|
||||
t.Fatal("expected error writing .releaser.yml to read-only directory")
|
||||
}
|
||||
}
|
||||
|
||||
// ── run() injectable error paths ─────────────────────────────────────────────
|
||||
|
||||
func TestRunVerboseInit(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := execCmd(t, "--init", "--verbose", "--repo", dir)
|
||||
if err != nil {
|
||||
t.Fatalf("--init --verbose: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, ".releaser.yml")); err != nil {
|
||||
t.Error("expected .releaser.yml to be created")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAbsPathError(t *testing.T) {
|
||||
old := absPath
|
||||
absPath = func(string) (string, error) { return "", fmt.Errorf("injected abs error") }
|
||||
defer func() { absPath = old }()
|
||||
|
||||
err := execCmd(t, "--repo", ".")
|
||||
if err == nil {
|
||||
t.Fatal("expected error when filepath.Abs fails")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWorkingTreeCheckFails(t *testing.T) {
|
||||
_, dir := setupRepo(t)
|
||||
addFile(t, dir, "x.go", "// fix")
|
||||
repo, _ := gogit.PlainOpen(dir)
|
||||
w, _ := repo.Worktree()
|
||||
w.Add("x.go")
|
||||
w.Commit("fix: patch something", &gogit.CommitOptions{Author: testSig()})
|
||||
|
||||
// Corrupt the git index so IsWorkingTreeClean fails.
|
||||
os.WriteFile(filepath.Join(dir, ".git", "index"), []byte("garbage"), 0644)
|
||||
|
||||
err := execCmd(t, "--branch", "release/1.2", "--repo", dir)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for corrupt git index")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunLatestTagFails(t *testing.T) {
|
||||
_, dir := setupRepo(t)
|
||||
addFile(t, dir, "x.go", "// fix")
|
||||
repo, _ := gogit.PlainOpen(dir)
|
||||
w, _ := repo.Worktree()
|
||||
w.Add("x.go")
|
||||
w.Commit("fix: patch something", &gogit.CommitOptions{Author: testSig()})
|
||||
|
||||
tagsDir := filepath.Join(dir, ".git", "refs", "tags")
|
||||
os.Chmod(tagsDir, 0000)
|
||||
defer os.Chmod(tagsDir, 0755)
|
||||
|
||||
err := execCmd(t, "--dry-run", "--branch", "release/1.2", "--repo", dir)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unreadable tags directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAllCommitsError(t *testing.T) {
|
||||
_, dir := setupRepo(t)
|
||||
addFile(t, dir, "x.go", "// fix")
|
||||
repo, _ := gogit.PlainOpen(dir)
|
||||
w, _ := repo.Worktree()
|
||||
w.Add("x.go")
|
||||
w.Commit("fix: patch something", &gogit.CommitOptions{Author: testSig()})
|
||||
|
||||
old := gitAllCommits
|
||||
gitAllCommits = func(_ *gogit.Repository) ([]string, error) {
|
||||
return nil, fmt.Errorf("injected AllCommits error")
|
||||
}
|
||||
defer func() { gitAllCommits = old }()
|
||||
|
||||
err := execCmd(t, "--dry-run", "--branch", "release/1.2", "--repo", dir)
|
||||
if err == nil {
|
||||
t.Fatal("expected error from AllCommits")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCommitsSinceError(t *testing.T) {
|
||||
repo, dir := setupRepo(t)
|
||||
head, _ := repo.Head()
|
||||
repo.CreateTag("1.2.0", head.Hash(), nil)
|
||||
|
||||
addFile(t, dir, "x.go", "// fix")
|
||||
w, _ := repo.Worktree()
|
||||
w.Add("x.go")
|
||||
w.Commit("fix: patch something", &gogit.CommitOptions{Author: testSig()})
|
||||
|
||||
old := gitCommitsSince
|
||||
gitCommitsSince = func(_ *gogit.Repository, _ string) ([]string, error) {
|
||||
return nil, fmt.Errorf("injected CommitsSince error")
|
||||
}
|
||||
defer func() { gitCommitsSince = old }()
|
||||
|
||||
err := execCmd(t, "--dry-run", "--branch", "release/1.2", "--repo", dir)
|
||||
if err == nil {
|
||||
t.Fatal("expected error from CommitsSince")
|
||||
}
|
||||
}
|
||||
|
||||
// ── verbose commit section coverage ──────────────────────────────────────────
|
||||
|
||||
func TestRunVerboseBreakingAndFix(t *testing.T) {
|
||||
// Covers: verbose "since: lastTag", message truncation (>70 chars),
|
||||
// TypeBreaking color (ansiRed+ansiBold), TypeFix color (ansiGreen).
|
||||
repo, dir := setupRepo(t)
|
||||
head, _ := repo.Head()
|
||||
repo.CreateTag("1.2.0", head.Hash(), nil)
|
||||
|
||||
w, _ := repo.Worktree()
|
||||
|
||||
addFile(t, dir, "a.go", "a")
|
||||
w.Add("a.go")
|
||||
w.Commit("feat!: redesign the entire public API surface which is a very long commit message header", &gogit.CommitOptions{Author: testSig()})
|
||||
|
||||
addFile(t, dir, "b.go", "b")
|
||||
w.Add("b.go")
|
||||
w.Commit("fix: correct null pointer in edge case handler", &gogit.CommitOptions{Author: testSig()})
|
||||
|
||||
old := os.Stderr
|
||||
r, wp, _ := os.Pipe()
|
||||
os.Stderr = wp
|
||||
|
||||
err := execCmd(t, "--dry-run", "--verbose", "--branch", "release/1.2", "--repo", dir)
|
||||
|
||||
wp.Close()
|
||||
os.Stderr = old
|
||||
io.ReadAll(r)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("verbose breaking+fix: unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ── release.env write error ───────────────────────────────────────────────────
|
||||
|
||||
func TestRunReleaseEnvWriteFails(t *testing.T) {
|
||||
_, dir := setupRepo(t)
|
||||
addFile(t, dir, "x.go", "// fix")
|
||||
repo, _ := gogit.PlainOpen(dir)
|
||||
w, _ := repo.Worktree()
|
||||
w.Add("x.go")
|
||||
w.Commit("fix: patch something", &gogit.CommitOptions{Author: testSig()})
|
||||
|
||||
// Empty directory is invisible to git — dirty check passes.
|
||||
os.Mkdir(filepath.Join(dir, "release.env"), 0755)
|
||||
|
||||
err := execCmd(t, "--branch", "release/1.2", "--repo", dir)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when release.env is a directory")
|
||||
}
|
||||
}
|
||||
|
||||
// ── pom stat error ────────────────────────────────────────────────────────────
|
||||
|
||||
func TestRunPomStatError(t *testing.T) {
|
||||
// A null byte in the path makes os.Stat return EINVAL (not ErrNotExist),
|
||||
// so hasPom=true and the stat error is propagated.
|
||||
_, dir := setupRepo(t)
|
||||
addFile(t, dir, "x.go", "// fix")
|
||||
repo, _ := gogit.PlainOpen(dir)
|
||||
w, _ := repo.Worktree()
|
||||
w.Add("x.go")
|
||||
w.Commit("fix: patch something", &gogit.CommitOptions{Author: testSig()})
|
||||
|
||||
err := execCmd(t, "--branch", "release/1.2", "--repo", dir, "--pom", "pom\x00.xml")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for pom path with null byte (EINVAL)")
|
||||
}
|
||||
}
|
||||
|
||||
// ── changelog update error ────────────────────────────────────────────────────
|
||||
|
||||
func TestRunChangelogUpdateFails(t *testing.T) {
|
||||
_, dir := setupRepo(t)
|
||||
addFile(t, dir, "x.go", "// fix")
|
||||
repo, _ := gogit.PlainOpen(dir)
|
||||
w, _ := repo.Worktree()
|
||||
w.Add("x.go")
|
||||
w.Commit("fix: patch something", &gogit.CommitOptions{Author: testSig()})
|
||||
|
||||
// Empty directory is invisible to git — dirty check passes.
|
||||
// changelog.Update will fail trying to ReadFile on a directory.
|
||||
os.Mkdir(filepath.Join(dir, "CHANGELOG.md"), 0755)
|
||||
|
||||
err := execCmd(t, "--branch", "release/1.2", "--repo", dir)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when CHANGELOG.md is a directory")
|
||||
}
|
||||
}
|
||||
|
||||
// ── CommitFiles error ─────────────────────────────────────────────────────────
|
||||
|
||||
func TestRunCommitFilesError(t *testing.T) {
|
||||
_, dir := setupRepo(t)
|
||||
addFile(t, dir, "x.go", "// fix")
|
||||
repo, _ := gogit.PlainOpen(dir)
|
||||
w, _ := repo.Worktree()
|
||||
w.Add("x.go")
|
||||
w.Commit("fix: patch something", &gogit.CommitOptions{Author: testSig()})
|
||||
|
||||
old := gitCommitFiles
|
||||
gitCommitFiles = func(_ *gogit.Repository, _ []string, _, _, _ string) (plumbing.Hash, error) {
|
||||
return plumbing.ZeroHash, fmt.Errorf("injected commit error")
|
||||
}
|
||||
defer func() { gitCommitFiles = old }()
|
||||
|
||||
err := execCmd(t, "--branch", "release/1.2", "--repo", dir)
|
||||
if err == nil {
|
||||
t.Fatal("expected error from CommitFiles")
|
||||
}
|
||||
}
|
||||
|
||||
// ── parseBumpRules coverage ───────────────────────────────────────────────────
|
||||
|
||||
func TestParseBumpRules(t *testing.T) {
|
||||
rules := config.BumpRulesConfig{Breaking: "minor", Feat: "minor", Fix: "minor"}
|
||||
m := parseBumpRules(rules)
|
||||
if len(m) != 3 {
|
||||
t.Errorf("expected 3 entries in bump rules map, got %d", len(m))
|
||||
}
|
||||
}
|
||||
|
||||
// ── printVerboseConfig — bump_rules and node rows ─────────────────────────────
|
||||
|
||||
func TestPrintVerboseConfigBumpRulesAndNode(t *testing.T) {
|
||||
cfg := config.Config{
|
||||
Git: config.GitConfig{
|
||||
BumpRules: config.BumpRulesConfig{Breaking: "minor", Feat: "minor", Fix: "minor"},
|
||||
},
|
||||
Node: config.NodeConfig{PackageJSON: "package.json"},
|
||||
}
|
||||
src := config.Sources{}
|
||||
|
||||
old := os.Stderr
|
||||
r, wp, _ := os.Pipe()
|
||||
os.Stderr = wp
|
||||
printVerboseConfig(cfg, src)
|
||||
wp.Close()
|
||||
os.Stderr = old
|
||||
out, _ := io.ReadAll(r)
|
||||
output := string(out)
|
||||
|
||||
if !strings.Contains(output, "minor") {
|
||||
t.Error("expected 'minor' in output for bump_rules")
|
||||
}
|
||||
if !strings.Contains(output, "package.json") {
|
||||
t.Error("expected 'package.json' in output for node.paths")
|
||||
}
|
||||
}
|
||||
|
||||
// ── node package.json handling ────────────────────────────────────────────────
|
||||
|
||||
func writePackageJSON(t *testing.T, dir, ver string) {
|
||||
t.Helper()
|
||||
content := fmt.Sprintf(`{"name": "my-app", "version": "%s"}`, ver)
|
||||
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunNodeVersionBump(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
repo, err := gogit.PlainInit(dir, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writePackageJSON(t, dir, "0.0.0")
|
||||
commitAll(t, repo, dir, "chore: init")
|
||||
|
||||
// .releaser.yml is untracked — go-git IsClean ignores untracked files
|
||||
os.WriteFile(filepath.Join(dir, ".releaser.yml"), []byte("node:\n package_json: \"package.json\"\n"), 0644)
|
||||
|
||||
addFile(t, dir, "x.go", "// fix")
|
||||
w, _ := repo.Worktree()
|
||||
w.Add("x.go")
|
||||
w.Commit("fix: something", &gogit.CommitOptions{Author: testSig()})
|
||||
|
||||
err = execCmd(t, "--no-push", "--branch", "release/1.2", "--repo", dir)
|
||||
if err != nil {
|
||||
t.Fatalf("node version bump: unexpected error: %v", err)
|
||||
}
|
||||
|
||||
data, _ := os.ReadFile(filepath.Join(dir, "package.json"))
|
||||
if !strings.Contains(string(data), `"version": "1.2.0"`) {
|
||||
t.Errorf("expected version 1.2.0 in package.json, got: %s", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunNodeReadVersionFails(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
repo, err := gogit.PlainInit(dir, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// invalid JSON — ReadVersion will fail
|
||||
os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{not json`), 0644)
|
||||
commitAll(t, repo, dir, "chore: init")
|
||||
|
||||
os.WriteFile(filepath.Join(dir, ".releaser.yml"), []byte("node:\n package_json: \"package.json\"\n"), 0644)
|
||||
|
||||
addFile(t, dir, "x.go", "// fix")
|
||||
w, _ := repo.Worktree()
|
||||
w.Add("x.go")
|
||||
w.Commit("fix: something", &gogit.CommitOptions{Author: testSig()})
|
||||
|
||||
err = execCmd(t, "--branch", "release/1.2", "--repo", dir)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when package.json has invalid JSON")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunNodeWriteVersionFails(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
repo, err := gogit.PlainInit(dir, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writePackageJSON(t, dir, "0.0.0")
|
||||
commitAll(t, repo, dir, "chore: init")
|
||||
|
||||
os.WriteFile(filepath.Join(dir, ".releaser.yml"), []byte("node:\n package_json: \"package.json\"\n"), 0644)
|
||||
|
||||
addFile(t, dir, "x.go", "// fix")
|
||||
w, _ := repo.Worktree()
|
||||
w.Add("x.go")
|
||||
w.Commit("fix: something", &gogit.CommitOptions{Author: testSig()})
|
||||
|
||||
// Make package.json read-only so WriteVersion fails
|
||||
os.Chmod(filepath.Join(dir, "package.json"), 0444)
|
||||
defer os.Chmod(filepath.Join(dir, "package.json"), 0644)
|
||||
|
||||
err = execCmd(t, "--branch", "release/1.2", "--repo", dir)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when package.json is read-only")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user