Release/1.0 #1

Merged
k3nny merged 19 commits from release/1.0 into main 2026-07-11 22:11:21 +02:00
3 changed files with 136 additions and 52 deletions
Showing only changes of commit 153d65bc53 - Show all commits
+57 -43
View File
@@ -161,15 +161,8 @@ 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:")
logSection("configuration")
rows := []struct{ key, val string }{
{"git.tag_prefix", cfg.Git.TagPrefix},
{"git.branch_pattern", cfg.Git.BranchPattern},
@@ -191,7 +184,11 @@ func printVerboseConfig(cfg config.Config, src config.Sources) {
if source == "" {
source = "default"
}
fmt.Fprintf(os.Stderr, " %-25s = %-40s [%s]\n", r.key, r.val, source)
val := r.val
if val == "" {
val = paint(ansiDim, "(empty)")
}
fmt.Fprintf(os.Stderr, " %-25s = %-45s %s\n", r.key, val, fmtSource(source))
}
}
@@ -215,7 +212,9 @@ func run(o options) error {
}
if o.init {
vlog(o.verbose, "creating .releaser.yml in %s", absRepo)
if o.verbose {
logStep("creating .releaser.yml in %s", absRepo)
}
return initConfig(absRepo)
}
@@ -277,7 +276,12 @@ 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)
if o.verbose {
logSection("branch")
fmt.Fprintf(os.Stderr, " %s → major=%d, minor=%d %s\n",
paint(ansiBold, branchName), info.Major, info.Minor,
paint(ansiDim, "(pinned by branch)"))
}
// --- Dirty check (before any changes) ---
// Skipped in --no-commit mode: the user intentionally has changes in flight.
@@ -297,28 +301,24 @@ 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 == "" {
fmt.Fprintln(os.Stderr, "info: no previous tag found — scanning all commits")
messages, err = gitutil.AllCommits(repo)
} else {
fmt.Fprintf(os.Stderr, "info: last tag: %s\n", lastTag)
messages, err = gitutil.CommitsSince(repo, lastTag)
}
if err != nil {
return fmt.Errorf("read commits: %w", err)
}
fmt.Fprintf(os.Stderr, "info: %d commit(s) to analyze\n", len(messages))
if !o.verbose {
if lastTag == "" {
logStep("no previous tag — scanning all %d commit(s)", len(messages))
} else {
logStep("last tag: %s (%d commit(s) to analyze)", lastTag, len(messages))
}
}
// --- Version calculation ---
types := make([]commits.Type, len(messages))
@@ -327,28 +327,38 @@ func run(o options) error {
}
if o.verbose {
vlog(true, "commits analyzed (%d):", len(messages))
logSection(fmt.Sprintf("commits (%d)", len(messages)))
if lastTag != "" {
fmt.Fprintf(os.Stderr, " since: %s (patch=%d)\n", paint(ansiCyan, lastTag), currentPatch)
}
for i, msg := range messages {
first := strings.SplitN(strings.TrimSpace(msg), "\n", 2)[0]
if len(first) > 70 {
first = first[:67] + "..."
}
t := types[i]
var decision string
typeLabel := fmt.Sprintf("%-9s", t.String())
if t == commits.TypeNone {
fmt.Fprintf(os.Stderr, " %s\n", paint(ansiDim, typeLabel+first))
} else {
var col string
switch t {
case commits.TypeBreaking:
decision = "breaking → patch bump"
col = ansiRed + ansiBold
case commits.TypeFeat:
decision = "feat → patch bump"
case commits.TypeFix:
decision = "fix → patch bump"
default:
decision = "ignored"
col = ansiCyan
default: // fix
col = ansiGreen
}
fmt.Fprintf(os.Stderr, " %s %s %s\n",
paint(col, typeLabel), first, paint(ansiDim, "→ patch bump"))
}
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")
logWarn("no releasable commits found")
return errNothingToRelease
}
@@ -361,13 +371,17 @@ func run(o options) error {
highestType = t
}
}
vlog(true, "version decision: highest type=%s → next=%s (tag: %s)", highestType, nextVersion, nextTag)
logSection("version")
fmt.Fprintf(os.Stderr, " highest type: %s → next: %s (tag: %s)\n",
paint(ansiCyan, highestType.String()),
paint(ansiBold, nextVersion),
paint(ansiBold+ansiCyan, nextTag))
}
fmt.Printf("next version: %s (tag: %s)\n", nextVersion, nextTag)
if o.dryRun {
fmt.Fprintln(os.Stderr, "dry-run: no changes made")
logStep("dry-run: no changes made")
return nil
}
@@ -387,13 +401,13 @@ func run(o options) error {
if err != nil {
return fmt.Errorf("read pom version: %w", err)
}
fmt.Fprintf(os.Stderr, "info: pom.xml: %s → %s\n", currentPomVersion, nextVersion)
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 {
fmt.Fprintln(os.Stderr, "info: no pom.xml found — skipping version bump")
logWarn("no pom.xml — skipping version bump")
}
// CHANGELOG.md
@@ -401,7 +415,7 @@ func run(o options) error {
if err := changelog.Update(changelogAbsPath, nextTag, nextVersion, messages); err != nil {
return fmt.Errorf("update changelog: %w", err)
}
fmt.Fprintf(os.Stderr, "info: %s updated\n", o.changelogFile)
logDone("%s updated", o.changelogFile)
filesToCommit = append(filesToCommit, o.changelogFile)
if o.noCommit {
@@ -421,14 +435,14 @@ func run(o options) error {
if _, err := gitutil.CommitFiles(repo, filesToCommit, commitMsg, authorName, authorEmail); err != nil {
return fmt.Errorf("commit: %w", err)
}
fmt.Fprintf(os.Stderr, "info: committed: %s\n", commitMsg)
logDone("committed: %s", commitMsg)
}
// --- Git tag ---
if err := gitutil.CreateTag(repo, nextTag); err != nil {
return fmt.Errorf("create tag: %w", err)
}
fmt.Fprintf(os.Stderr, "info: tag created: %s\n", nextTag)
logDone("tag: %s", nextTag)
if o.noPush {
fmt.Printf("released %s locally — push manually with: git push && git push --tags\n", nextTag)
@@ -436,11 +450,11 @@ func run(o options) error {
}
// --- Push ---
fmt.Fprintln(os.Stderr, "info: pushing commit and tag...")
logStep("pushing commit and tag...")
if err := gitutil.Push(repo, branchName, nextTag, cfg.GitLab.Token); err != nil {
return fmt.Errorf("push: %w", err)
}
fmt.Fprintln(os.Stderr, "info: pushed")
logDone("pushed")
// --- GitLab release ---
if o.noRelease {
@@ -448,7 +462,7 @@ func run(o options) error {
return nil
}
if cfg.GitLab.URL == "" || cfg.GitLab.Project == "" {
fmt.Fprintln(os.Stderr, "warning: GitLab URL or project not configured — skipping release creation")
logWarn("GitLab URL or project not configured — skipping release creation")
fmt.Printf("released %s\n", nextTag)
return nil
}
@@ -463,7 +477,7 @@ func run(o options) error {
return fmt.Errorf("create GitLab release: %w", err)
}
fmt.Fprintf(os.Stderr, "info: GitLab release created: %s\n", nextTag)
logDone("GitLab release created: %s", nextTag)
fmt.Printf("released %s\n", nextTag)
return nil
}
+6 -5
View File
@@ -684,15 +684,16 @@ func TestRunVerbose(t *testing.T) {
}
checks := []string{
"configuration:",
"configuration",
"git.tag_prefix",
"[default]",
"branch: release/1.2",
"branch",
"release/1.2",
"major=1, minor=2",
"commits analyzed",
"commits",
"feat: add new thing",
"feat → patch bump",
"version decision:",
"patch bump",
"version",
}
for _, want := range checks {
if !strings.Contains(output, want) {
+69
View File
@@ -0,0 +1,69 @@
package main
import (
"fmt"
"os"
"strings"
)
const (
ansiReset = "\033[0m"
ansiBold = "\033[1m"
ansiDim = "\033[2m"
ansiRed = "\033[31m"
ansiGreen = "\033[32m"
ansiYellow = "\033[33m"
ansiCyan = "\033[36m"
)
var useColor bool
func init() {
fi, err := os.Stderr.Stat()
tty := err == nil && (fi.Mode()&os.ModeCharDevice) != 0
useColor = tty && os.Getenv("NO_COLOR") == "" && os.Getenv("TERM") != "dumb"
}
func paint(code, s string) string {
if !useColor {
return s
}
return code + s + ansiReset
}
// logStep writes a neutral progress line to stderr.
func logStep(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(os.Stderr, " %s %s\n", paint(ansiDim, "·"), msg)
}
// logDone writes a success completion line to stderr.
func logDone(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(os.Stderr, " %s %s\n", paint(ansiGreen+ansiBold, "✓"), msg)
}
// logWarn writes a warning line to stderr.
func logWarn(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(os.Stderr, " %s %s\n", paint(ansiYellow, "!"), msg)
}
// logSection writes a bold section header to stderr (used in verbose mode).
func logSection(title string) {
fmt.Fprintf(os.Stderr, "\n%s\n", paint(ansiBold, "▸ "+title))
}
// fmtSource returns a colored "[source]" tag for a config key source.
func fmtSource(src string) string {
switch {
case strings.HasPrefix(src, "env:"):
return paint(ansiCyan, "["+src+"]")
case strings.HasPrefix(src, "flag:"):
return paint(ansiGreen, "["+src+"]")
case src == "default":
return paint(ansiDim, "[default]")
default: // "config file"
return paint(ansiBold, "["+src+"]")
}
}