feat(gradle): release v1.6.0 — Gradle build file version bump
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>
This commit is contained in:
+58
-16
@@ -18,6 +18,7 @@ import (
|
||||
"git.k3nny.fr/releaser/internal/ghclient"
|
||||
"git.k3nny.fr/releaser/internal/gitutil"
|
||||
"git.k3nny.fr/releaser/internal/glclient"
|
||||
"git.k3nny.fr/releaser/internal/gradle"
|
||||
"git.k3nny.fr/releaser/internal/maven"
|
||||
"git.k3nny.fr/releaser/internal/node"
|
||||
"git.k3nny.fr/releaser/internal/notes"
|
||||
@@ -78,6 +79,17 @@ node:
|
||||
# - "packages/frontend/package.json"
|
||||
# - "packages/backend/package.json"
|
||||
|
||||
gradle:
|
||||
# Single build.gradle or build.gradle.kts path (opt-in — no default).
|
||||
# Both Groovy DSL (single-quoted) and Kotlin DSL (double-quoted) are supported.
|
||||
# build_file: "build.gradle"
|
||||
|
||||
# Multiple build files for multi-module projects (overrides build_file).
|
||||
# build_files:
|
||||
# - "build.gradle"
|
||||
# - "module-a/build.gradle"
|
||||
# - "module-b/build.gradle"
|
||||
|
||||
gitlab:
|
||||
# GitLab instance URL. Falls back to the CI_SERVER_URL environment variable.
|
||||
# url: "https://gitlab.example.com"
|
||||
@@ -149,6 +161,7 @@ func newRootCmd() *cobra.Command {
|
||||
branchOverride string
|
||||
repoPath string
|
||||
pomOverride string
|
||||
gradleOverride string
|
||||
changelogFile string
|
||||
tagPrefixFlag string
|
||||
tagPrefixSet bool
|
||||
@@ -171,6 +184,7 @@ func newRootCmd() *cobra.Command {
|
||||
repoPath: repoPath,
|
||||
branchOverride: branchOverride,
|
||||
pomOverride: pomOverride,
|
||||
gradleOverride: gradleOverride,
|
||||
changelogFile: changelogFile,
|
||||
tagPrefixFlag: tagPrefixFlag,
|
||||
tagPrefixSet: tagPrefixSet,
|
||||
@@ -196,6 +210,7 @@ func newRootCmd() *cobra.Command {
|
||||
root.Flags().StringVar(&branchOverride, "branch", "", "override branch name detection (required in detached HEAD)")
|
||||
root.Flags().StringVar(&repoPath, "repo", ".", "path to git repository")
|
||||
root.Flags().StringVar(&pomOverride, "pom", "", "override maven.pom_path from config")
|
||||
root.Flags().StringVar(&gradleOverride, "gradle", "", "override gradle.build_file from config")
|
||||
root.Flags().StringVar(&changelogFile, "changelog-file", "CHANGELOG.md", "path to changelog file relative to repo root")
|
||||
root.Flags().StringVar(&tagPrefixFlag, "tag-prefix", "", "override git.tag_prefix from config")
|
||||
root.Flags().StringVar(&patternFlag, "branch-pattern", "", "override git.branch_pattern from config")
|
||||
@@ -215,22 +230,23 @@ func main() {
|
||||
}
|
||||
|
||||
type options struct {
|
||||
init bool
|
||||
verbose bool
|
||||
repoPath string
|
||||
branchOverride string
|
||||
pomOverride string
|
||||
changelogFile string
|
||||
tagPrefixFlag string
|
||||
tagPrefixSet bool
|
||||
patternFlag string
|
||||
patternSet bool
|
||||
dryRun bool
|
||||
noPush bool
|
||||
noRelease bool
|
||||
noCommit bool
|
||||
tagOnly bool
|
||||
releaseEnvFile string
|
||||
init bool
|
||||
verbose bool
|
||||
repoPath string
|
||||
branchOverride string
|
||||
pomOverride string
|
||||
gradleOverride string
|
||||
changelogFile string
|
||||
tagPrefixFlag string
|
||||
tagPrefixSet bool
|
||||
patternFlag string
|
||||
patternSet bool
|
||||
dryRun bool
|
||||
noPush bool
|
||||
noRelease bool
|
||||
noCommit bool
|
||||
tagOnly bool
|
||||
releaseEnvFile string
|
||||
}
|
||||
|
||||
func printVerboseConfig(cfg config.Config, src config.Sources) {
|
||||
@@ -273,6 +289,13 @@ func printVerboseConfig(cfg config.Config, src config.Sources) {
|
||||
}
|
||||
return strings.Join(paths, ", ")
|
||||
}()},
|
||||
{"gradle.paths", func() string {
|
||||
paths := cfg.Gradle.EffectiveBuildFiles()
|
||||
if len(paths) == 0 {
|
||||
return "(not configured)"
|
||||
}
|
||||
return strings.Join(paths, ", ")
|
||||
}()},
|
||||
{"gitlab.url", cfg.GitLab.URL},
|
||||
{"gitlab.token", func() string {
|
||||
if cfg.GitLab.Token != "" {
|
||||
@@ -360,6 +383,11 @@ func run(o options) error {
|
||||
cfg.Maven.PomPaths = nil
|
||||
src["maven.pom_paths"] = "flag: --pom"
|
||||
}
|
||||
if o.gradleOverride != "" {
|
||||
cfg.Gradle.BuildFile = o.gradleOverride
|
||||
cfg.Gradle.BuildFiles = nil
|
||||
src["gradle.build_files"] = "flag: --gradle"
|
||||
}
|
||||
if o.patternSet {
|
||||
cfg.Git.BranchPattern = o.patternFlag
|
||||
src["git.branch_pattern"] = "flag: --branch-pattern"
|
||||
@@ -552,6 +580,20 @@ func run(o options) error {
|
||||
filesToCommit = append(filesToCommit, relPkgPath)
|
||||
}
|
||||
|
||||
// build.gradle / build.gradle.kts (opt-in via gradle.build_file / gradle.build_files)
|
||||
for _, relGradlePath := range cfg.Gradle.EffectiveBuildFiles() {
|
||||
gradlePath := filepath.Join(absRepo, relGradlePath)
|
||||
currentGradleVersion, err := gradle.ReadVersion(gradlePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read gradle version: %w", err)
|
||||
}
|
||||
if err := gradle.WriteVersion(gradlePath, currentGradleVersion, nextVersion); err != nil {
|
||||
return fmt.Errorf("update gradle version: %w", err)
|
||||
}
|
||||
logDone("%s: %s → %s", relGradlePath, currentGradleVersion, nextVersion)
|
||||
filesToCommit = append(filesToCommit, relGradlePath)
|
||||
}
|
||||
|
||||
// CHANGELOG.md
|
||||
changelogAbsPath := filepath.Join(absRepo, o.changelogFile)
|
||||
if err := changelog.Update(changelogAbsPath, nextTag, nextVersion, messages); err != nil {
|
||||
|
||||
+116
-1
@@ -1093,7 +1093,8 @@ func TestPrintVerboseConfigBumpRulesAndNode(t *testing.T) {
|
||||
Git: config.GitConfig{
|
||||
BumpRules: config.BumpRulesConfig{Breaking: "minor", Feat: "minor", Fix: "minor"},
|
||||
},
|
||||
Node: config.NodeConfig{PackageJSON: "package.json"},
|
||||
Node: config.NodeConfig{PackageJSON: "package.json"},
|
||||
Gradle: config.GradleConfig{BuildFile: "build.gradle"},
|
||||
}
|
||||
src := config.Sources{}
|
||||
|
||||
@@ -1112,6 +1113,9 @@ func TestPrintVerboseConfigBumpRulesAndNode(t *testing.T) {
|
||||
if !strings.Contains(output, "package.json") {
|
||||
t.Error("expected 'package.json' in output for node.paths")
|
||||
}
|
||||
if !strings.Contains(output, "build.gradle") {
|
||||
t.Error("expected 'build.gradle' in output for gradle.paths")
|
||||
}
|
||||
}
|
||||
|
||||
// ── node package.json handling ────────────────────────────────────────────────
|
||||
@@ -1200,3 +1204,114 @@ func TestRunNodeWriteVersionFails(t *testing.T) {
|
||||
t.Fatal("expected error when package.json is read-only")
|
||||
}
|
||||
}
|
||||
|
||||
// ── gradle build file handling ────────────────────────────────────────────────
|
||||
|
||||
func writeGradleFile(t *testing.T, dir, ver string) {
|
||||
t.Helper()
|
||||
content := fmt.Sprintf("group = \"com.example\"\nversion = \"%s\"\n", ver)
|
||||
if err := os.WriteFile(filepath.Join(dir, "build.gradle"), []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunGradleVersionBump(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
repo, err := gogit.PlainInit(dir, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeGradleFile(t, dir, "0.0.0")
|
||||
commitAll(t, repo, dir, "chore: init")
|
||||
|
||||
os.WriteFile(filepath.Join(dir, ".releaser.yml"), []byte("gradle:\n build_file: \"build.gradle\"\n"), 0644)
|
||||
|
||||
addFile(t, dir, "x.go", "// fix")
|
||||
w, _ := repo.Worktree()
|
||||
w.Add("x.go")
|
||||
w.Commit("fix: something", &gogit.CommitOptions{Author: testSig()})
|
||||
|
||||
if err := execCmd(t, "--no-push", "--branch", "release/1.2", "--repo", dir); err != nil {
|
||||
t.Fatalf("gradle version bump: unexpected error: %v", err)
|
||||
}
|
||||
|
||||
data, _ := os.ReadFile(filepath.Join(dir, "build.gradle"))
|
||||
if !strings.Contains(string(data), `version = "1.2.0"`) {
|
||||
t.Errorf("expected version 1.2.0 in build.gradle, got: %s", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunGradleOverrideFlag(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
repo, err := gogit.PlainInit(dir, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Write gradle file at custom path
|
||||
if err := os.MkdirAll(filepath.Join(dir, "sub"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := "version = \"0.0.0\"\n"
|
||||
os.WriteFile(filepath.Join(dir, "sub", "build.gradle"), []byte(content), 0644)
|
||||
commitAll(t, repo, dir, "chore: init")
|
||||
|
||||
addFile(t, dir, "x.go", "// fix")
|
||||
w, _ := repo.Worktree()
|
||||
w.Add("x.go")
|
||||
w.Commit("fix: something", &gogit.CommitOptions{Author: testSig()})
|
||||
|
||||
if err := execCmd(t, "--no-push", "--branch", "release/1.2", "--repo", dir, "--gradle", "sub/build.gradle"); err != nil {
|
||||
t.Fatalf("--gradle flag: unexpected error: %v", err)
|
||||
}
|
||||
|
||||
data, _ := os.ReadFile(filepath.Join(dir, "sub", "build.gradle"))
|
||||
if !strings.Contains(string(data), `version = "1.2.0"`) {
|
||||
t.Errorf("expected version 1.2.0, got: %s", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunGradleReadVersionFails(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
repo, err := gogit.PlainInit(dir, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// build.gradle with no version assignment
|
||||
os.WriteFile(filepath.Join(dir, "build.gradle"), []byte(`group = "com.example"`), 0644)
|
||||
commitAll(t, repo, dir, "chore: init")
|
||||
|
||||
os.WriteFile(filepath.Join(dir, ".releaser.yml"), []byte("gradle:\n build_file: \"build.gradle\"\n"), 0644)
|
||||
|
||||
addFile(t, dir, "x.go", "// fix")
|
||||
w, _ := repo.Worktree()
|
||||
w.Add("x.go")
|
||||
w.Commit("fix: something", &gogit.CommitOptions{Author: testSig()})
|
||||
|
||||
if err := execCmd(t, "--branch", "release/1.2", "--repo", dir); err == nil {
|
||||
t.Fatal("expected error when build.gradle has no version assignment")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunGradleWriteVersionFails(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
repo, err := gogit.PlainInit(dir, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeGradleFile(t, dir, "0.0.0")
|
||||
commitAll(t, repo, dir, "chore: init")
|
||||
|
||||
os.WriteFile(filepath.Join(dir, ".releaser.yml"), []byte("gradle:\n build_file: \"build.gradle\"\n"), 0644)
|
||||
|
||||
addFile(t, dir, "x.go", "// fix")
|
||||
w, _ := repo.Worktree()
|
||||
w.Add("x.go")
|
||||
w.Commit("fix: something", &gogit.CommitOptions{Author: testSig()})
|
||||
|
||||
os.Chmod(filepath.Join(dir, "build.gradle"), 0444)
|
||||
defer os.Chmod(filepath.Join(dir, "build.gradle"), 0644)
|
||||
|
||||
if err := execCmd(t, "--branch", "release/1.2", "--repo", dir); err == nil {
|
||||
t.Fatal("expected error when build.gradle is read-only")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user