feat(releaser): add --verbose flag for configuration and decision tracing
ci / vet, staticcheck, test, build (push) Successful in 3m10s
ci / vet, staticcheck, test, build (push) Successful in 3m10s
- Prints a configuration table on startup showing each key, its value, and the source (default / config file / env: VARNAME / flag: --name) - Lists every commit since the last tag with its parsed type and the version-bump decision (feat/fix/breaking → patch bump, or ignored) - Explains the final version choice: highest commit type → next tag - All verbose output goes to stderr so it never pollutes stdout captures - Sources tracking wired through config.LoadWithSources and ApplyEnvWithSources; LoadWithSources uses a two-pass approach to detect which YAML fields were explicitly set vs defaulted Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+102
-2
@@ -72,6 +72,7 @@ var exitFn = os.Exit
|
||||
func newRootCmd() *cobra.Command {
|
||||
var (
|
||||
init_ bool
|
||||
verbose bool
|
||||
dryRun bool
|
||||
noPush bool
|
||||
noRelease bool
|
||||
@@ -97,6 +98,7 @@ func newRootCmd() *cobra.Command {
|
||||
patternSet = cmd.Flags().Changed("branch-pattern")
|
||||
return run(options{
|
||||
init: init_,
|
||||
verbose: verbose,
|
||||
repoPath: repoPath,
|
||||
branchOverride: branchOverride,
|
||||
pomOverride: pomOverride,
|
||||
@@ -115,6 +117,7 @@ func newRootCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
root.Flags().BoolVar(&init_, "init", false, "create a default .releaser.yml in the repository and exit")
|
||||
root.Flags().BoolVar(&verbose, "verbose", false, "print configuration sources, commit list, and version decision")
|
||||
root.Flags().BoolVar(&dryRun, "dry-run", false, "print next version without making changes")
|
||||
root.Flags().BoolVar(&noPush, "no-push", false, "create commit and tag locally without pushing or creating a GitLab release")
|
||||
root.Flags().BoolVar(&noRelease, "no-release", false, "push commit and tag but skip creating the GitLab release")
|
||||
@@ -142,6 +145,7 @@ func main() {
|
||||
|
||||
type options struct {
|
||||
init bool
|
||||
verbose bool
|
||||
repoPath string
|
||||
branchOverride string
|
||||
pomOverride string
|
||||
@@ -157,6 +161,40 @@ type options struct {
|
||||
tagOnly bool
|
||||
}
|
||||
|
||||
// vlog prints a verbose line to stderr, prefixed with "[verbose] ", when verbose is true.
|
||||
func vlog(verbose bool, format string, args ...any) {
|
||||
if verbose {
|
||||
fmt.Fprintf(os.Stderr, "[verbose] "+format+"\n", args...)
|
||||
}
|
||||
}
|
||||
|
||||
func printVerboseConfig(cfg config.Config, src config.Sources) {
|
||||
fmt.Fprintln(os.Stderr, "[verbose] configuration:")
|
||||
rows := []struct{ key, val string }{
|
||||
{"git.tag_prefix", cfg.Git.TagPrefix},
|
||||
{"git.branch_pattern", cfg.Git.BranchPattern},
|
||||
{"git.commit_message", cfg.Git.CommitMessage},
|
||||
{"git.author_name", cfg.Git.AuthorName},
|
||||
{"git.author_email", cfg.Git.AuthorEmail},
|
||||
{"maven.pom_path", cfg.Maven.PomPath},
|
||||
{"gitlab.url", cfg.GitLab.URL},
|
||||
{"gitlab.token", func() string {
|
||||
if cfg.GitLab.Token != "" {
|
||||
return "(set)"
|
||||
}
|
||||
return "(not set)"
|
||||
}()},
|
||||
{"gitlab.project", cfg.GitLab.Project},
|
||||
}
|
||||
for _, r := range rows {
|
||||
source := src[r.key]
|
||||
if source == "" {
|
||||
source = "default"
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, " %-25s = %-40s [%s]\n", r.key, r.val, source)
|
||||
}
|
||||
}
|
||||
|
||||
func initConfig(absRepo string) error {
|
||||
path := filepath.Join(absRepo, ".releaser.yml")
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
@@ -177,24 +215,46 @@ func run(o options) error {
|
||||
}
|
||||
|
||||
if o.init {
|
||||
vlog(o.verbose, "creating .releaser.yml in %s", absRepo)
|
||||
return initConfig(absRepo)
|
||||
}
|
||||
|
||||
cfg, err := config.Load(absRepo)
|
||||
var (
|
||||
cfg config.Config
|
||||
src config.Sources
|
||||
)
|
||||
if o.verbose {
|
||||
cfg, src, err = config.LoadWithSources(absRepo)
|
||||
} else {
|
||||
cfg, err = config.Load(absRepo)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.ApplyEnv()
|
||||
cfg.ApplyEnvWithSources(src)
|
||||
|
||||
// CLI flags take precedence over config file and env vars
|
||||
if o.tagPrefixSet {
|
||||
cfg.Git.TagPrefix = o.tagPrefixFlag
|
||||
if src != nil {
|
||||
src["git.tag_prefix"] = "flag: --tag-prefix"
|
||||
}
|
||||
}
|
||||
if o.pomOverride != "" {
|
||||
cfg.Maven.PomPath = o.pomOverride
|
||||
if src != nil {
|
||||
src["maven.pom_path"] = "flag: --pom"
|
||||
}
|
||||
}
|
||||
if o.patternSet {
|
||||
cfg.Git.BranchPattern = o.patternFlag
|
||||
if src != nil {
|
||||
src["git.branch_pattern"] = "flag: --branch-pattern"
|
||||
}
|
||||
}
|
||||
|
||||
if o.verbose {
|
||||
printVerboseConfig(cfg, src)
|
||||
}
|
||||
|
||||
// --- Git ---
|
||||
@@ -217,6 +277,8 @@ func run(o options) error {
|
||||
}
|
||||
info.TagPrefix = cfg.Git.TagPrefix
|
||||
|
||||
vlog(o.verbose, "branch: %s → major=%d, minor=%d (pinned by branch)", branchName, info.Major, info.Minor)
|
||||
|
||||
// --- Dirty check (before any changes) ---
|
||||
// Skipped in --no-commit mode: the user intentionally has changes in flight.
|
||||
if !o.dryRun && !o.noCommit {
|
||||
@@ -235,6 +297,14 @@ func run(o options) error {
|
||||
return fmt.Errorf("find latest tag: %w", err)
|
||||
}
|
||||
|
||||
if o.verbose {
|
||||
if lastTag == "" {
|
||||
vlog(true, "last tag: none — scanning all commits")
|
||||
} else {
|
||||
vlog(true, "last tag: %s → current patch=%d", lastTag, currentPatch)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Commit range ---
|
||||
var messages []string
|
||||
if lastTag == "" {
|
||||
@@ -256,6 +326,26 @@ func run(o options) error {
|
||||
types[i] = commits.Parse(msg)
|
||||
}
|
||||
|
||||
if o.verbose {
|
||||
vlog(true, "commits analyzed (%d):", len(messages))
|
||||
for i, msg := range messages {
|
||||
first := strings.SplitN(strings.TrimSpace(msg), "\n", 2)[0]
|
||||
t := types[i]
|
||||
var decision string
|
||||
switch t {
|
||||
case commits.TypeBreaking:
|
||||
decision = "breaking → patch bump"
|
||||
case commits.TypeFeat:
|
||||
decision = "feat → patch bump"
|
||||
case commits.TypeFix:
|
||||
decision = "fix → patch bump"
|
||||
default:
|
||||
decision = "ignored"
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, " %-60s %s\n", first, decision)
|
||||
}
|
||||
}
|
||||
|
||||
nextVersion, ok := semver.Next(info.Major, info.Minor, currentPatch, types)
|
||||
if !ok {
|
||||
fmt.Fprintln(os.Stderr, "info: no releasable commits found")
|
||||
@@ -264,6 +354,16 @@ func run(o options) error {
|
||||
|
||||
nextTag := info.TagName(nextVersion)
|
||||
|
||||
if o.verbose {
|
||||
highestType := commits.TypeNone
|
||||
for _, t := range types {
|
||||
if t > highestType {
|
||||
highestType = t
|
||||
}
|
||||
}
|
||||
vlog(true, "version decision: highest type=%s → next=%s (tag: %s)", highestType, nextVersion, nextTag)
|
||||
}
|
||||
|
||||
fmt.Printf("next version: %s (tag: %s)\n", nextVersion, nextTag)
|
||||
|
||||
if o.dryRun {
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -656,3 +657,46 @@ func TestRunChangelogFile(t *testing.T) {
|
||||
t.Error("expected CHANGES.md to be created")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunVerbose(t *testing.T) {
|
||||
_, dir := setupRepo(t)
|
||||
addFile(t, dir, "x.go", "// feat")
|
||||
repo, _ := gogit.PlainOpen(dir)
|
||||
w, _ := repo.Worktree()
|
||||
w.Add("x.go")
|
||||
w.Commit("feat: add new thing", &gogit.CommitOptions{Author: testSig()})
|
||||
|
||||
// Capture stderr output by redirecting it temporarily.
|
||||
old := os.Stderr
|
||||
r, wPipe, _ := os.Pipe()
|
||||
os.Stderr = wPipe
|
||||
|
||||
err := execCmd(t, "--dry-run", "--verbose", "--branch", "release/1.2", "--repo", dir)
|
||||
|
||||
wPipe.Close()
|
||||
os.Stderr = old
|
||||
|
||||
rawBytes, _ := io.ReadAll(r)
|
||||
output := string(rawBytes)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("--verbose: unexpected error: %v", err)
|
||||
}
|
||||
|
||||
checks := []string{
|
||||
"configuration:",
|
||||
"git.tag_prefix",
|
||||
"[default]",
|
||||
"branch: release/1.2",
|
||||
"major=1, minor=2",
|
||||
"commits analyzed",
|
||||
"feat: add new thing",
|
||||
"feat → patch bump",
|
||||
"version decision:",
|
||||
}
|
||||
for _, want := range checks {
|
||||
if !strings.Contains(output, want) {
|
||||
t.Errorf("--verbose output missing %q\nfull output:\n%s", want, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user