< Summary - go-semantic-release Coverage

Line coverage
71%
Covered lines: 422
Uncovered lines: 169
Coverable lines: 591
Total lines: 913
Line coverage: 71.4%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/go-semantic-release/go-semantic-release/internal/adapters/cli/changelog.go

#LineLine coverage
 1package cli
 2
 3import (
 4  "fmt"
 5
 6  "github.com/spf13/cobra"
 7
 8  "github.com/jedi-knights/go-semantic-release/internal/domain"
 9)
 10
 1111func newChangelogCmd(opts *rootOptions) *cobra.Command {
 1112  return &cobra.Command{
 1113    Use:   "changelog",
 1114    Short: "Generate changelog for the next release",
 015    RunE: func(cmd *cobra.Command, args []string) error {
 016      return runChangelog(cmd, args, opts)
 017    },
 18  }
 19}
 20
 221func runChangelog(cmd *cobra.Command, _ []string, opts *rootOptions) error {
 222  ctx := cmd.Context()
 223
 224  container, workDir, err := buildContainerWithWorkDir(opts)
 025  if err != nil {
 026    return err
 027  }
 28
 229  cfg := container.Config()
 230
 231  projects, err := container.ProjectDetector().Detect(ctx, workDir)
 032  if err != nil {
 033    return fmt.Errorf("detecting projects: %w", err)
 034  }
 35
 036  if opts.project != "" {
 037    projects = filterProject(projects, opts.project)
 038    if len(projects) == 0 {
 039      return fmt.Errorf("project %q not found", opts.project)
 040    }
 41  }
 42
 243  commits, err := container.CommitAnalyzer().Analyze(ctx, "")
 044  if err != nil {
 045    return fmt.Errorf("analyzing commits: %w", err)
 046  }
 47
 248  branch, err := container.GitRepository().CurrentBranch(ctx)
 049  if err != nil {
 050    return fmt.Errorf("resolving current branch: %w", err)
 051  }
 252  policy := domain.FindBranchPolicy(cfg.Branches, branch)
 253
 254  plan, err := container.ReleasePlanner().Plan(ctx, projects, commits, cfg.ReleaseMode, policy, true)
 055  if err != nil {
 056    return fmt.Errorf("planning release: %w", err)
 057  }
 58
 259  out := cmd.OutOrStdout()
 260  gen := container.ChangelogGenerator()
 261  releasable := plan.ReleasableProjects()
 162  for i := range releasable {
 163    notes, err := gen.Generate(releasable[i].NextVersion, releasable[i].Project.Name, releasable[i].Commits, cfg.Changel
 064    if err != nil {
 065      return fmt.Errorf("generating changelog for %s: %w", releasable[i].Project.Name, err)
 066    }
 167    _, _ = fmt.Fprintln(out, notes)
 168    _, _ = fmt.Fprintln(out)
 69  }
 70
 171  if !plan.HasReleasableProjects() {
 172    _, _ = fmt.Fprintln(out, "No releasable changes found.")
 173  }
 74
 275  return nil
 76}

/home/runner/work/go-semantic-release/go-semantic-release/internal/adapters/cli/config.go

#LineLine coverage
 1package cli
 2
 3import (
 4  "fmt"
 5
 6  "github.com/spf13/cobra"
 7
 8  adapterconfig "github.com/jedi-knights/go-semantic-release/internal/adapters/config"
 9)
 10
 911func newConfigCmd() *cobra.Command {
 912  cmd := &cobra.Command{
 913    Use:   "config",
 914    Short: "Configuration management",
 915  }
 916
 917  cmd.AddCommand(newConfigInitCmd())
 918  return cmd
 919}
 20
 1421func newConfigInitCmd() *cobra.Command {
 1422  return &cobra.Command{
 1423    Use:   "init",
 1424    Short: "Initialize a default configuration file",
 1425    RunE:  runConfigInit,
 1426  }
 1427}
 28
 129func runConfigInit(cmd *cobra.Command, _ []string) error {
 130  path := ".semantic-release.yaml"
 031  if err := adapterconfig.WriteDefaultConfig(path); err != nil {
 032    return fmt.Errorf("writing config: %w", err)
 033  }
 134  _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Created %s with default configuration.\n", path)
 135  return nil
 36}

/home/runner/work/go-semantic-release/go-semantic-release/internal/adapters/cli/detect_projects.go

#LineLine coverage
 1package cli
 2
 3import (
 4  "encoding/json"
 5  "fmt"
 6
 7  "github.com/spf13/cobra"
 8)
 9
 1110func newDetectProjectsCmd(opts *rootOptions) *cobra.Command {
 1111  return &cobra.Command{
 1112    Use:   "detect-projects",
 1113    Short: "Discover projects in the repository",
 1114    Long:  `Detect all projects/modules in the repository using configured discovery strategies.`,
 015    RunE: func(cmd *cobra.Command, args []string) error {
 016      return runDetectProjects(cmd, args, opts)
 017    },
 18  }
 19}
 20
 221func runDetectProjects(cmd *cobra.Command, _ []string, opts *rootOptions) error {
 222  ctx := cmd.Context()
 223
 224  container, workDir, err := buildContainerWithWorkDir(opts)
 025  if err != nil {
 026    return err
 027  }
 28
 229  projects, err := container.ProjectDetector().Detect(ctx, workDir)
 030  if err != nil {
 031    return fmt.Errorf("detecting projects: %w", err)
 032  }
 33
 134  if opts.jsonOut {
 035    if err := json.NewEncoder(cmd.OutOrStdout()).Encode(projects); err != nil {
 036      return fmt.Errorf("encoding projects as JSON: %w", err)
 037    }
 138    return nil
 39  }
 40
 141  _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Discovered %d project(s):\n\n", len(projects))
 142  for _, p := range projects {
 143    _, _ = fmt.Fprintf(cmd.OutOrStdout(), "  %s\n", p.Name)
 144    _, _ = fmt.Fprintf(cmd.OutOrStdout(), "    Path:   %s\n", p.Path)
 145    _, _ = fmt.Fprintf(cmd.OutOrStdout(), "    Type:   %s\n", p.Type)
 046    if p.ModulePath != "" {
 047      _, _ = fmt.Fprintf(cmd.OutOrStdout(), "    Module: %s\n", p.ModulePath)
 048    }
 049    if p.TagPrefix != "" {
 050      _, _ = fmt.Fprintf(cmd.OutOrStdout(), "    Tags:   %s*\n", p.TagPrefix)
 051    }
 152    _, _ = fmt.Fprintln(cmd.OutOrStdout())
 53  }
 154  return nil
 55}

/home/runner/work/go-semantic-release/go-semantic-release/internal/adapters/cli/lint.go

#LineLine coverage
 1package cli
 2
 3import (
 4  "fmt"
 5
 6  "github.com/spf13/cobra"
 7
 8  "github.com/jedi-knights/go-semantic-release/internal/adapters/lint"
 9  "github.com/jedi-knights/go-semantic-release/internal/domain"
 10)
 11
 1112func newLintCmd(opts *rootOptions) *cobra.Command {
 1113  return &cobra.Command{
 1114    Use:   "lint",
 1115    Short: "Lint commit messages against conventional commit rules",
 1116    Long:  `Validate that recent commit messages follow the conventional commits specification and configured rules.`,
 017    RunE: func(cmd *cobra.Command, args []string) error {
 018      return runLint(cmd, args, opts)
 019    },
 20  }
 21}
 22
 223func runLint(cmd *cobra.Command, _ []string, opts *rootOptions) error {
 224  ctx := cmd.Context()
 225
 226  container, _, err := buildContainerWithWorkDir(opts)
 027  if err != nil {
 028    return err
 029  }
 30
 231  cfg := container.Config()
 232  lintCfg := cfg.Lint
 033  if len(lintCfg.AllowedTypes) == 0 {
 034    lintCfg = domain.DefaultLintConfig()
 035  }
 36
 237  commits, err := container.CommitAnalyzer().Analyze(ctx, "")
 038  if err != nil {
 039    return fmt.Errorf("analyzing commits: %w", err)
 040  }
 41
 042  if len(commits) == 0 {
 043    _, _ = fmt.Fprintln(cmd.OutOrStdout(), "No commits to lint.")
 044    return nil
 045  }
 46
 247  linter := lint.NewConventionalLinter(lintCfg)
 248  totalViolations := 0
 249  hasErrors := false
 250
 251  for i := range commits {
 352    violations := linter.Lint(commits[i])
 253    if len(violations) == 0 {
 254      continue
 55    }
 56
 157    hash := commits[i].Hash
 158    if len(hash) > 7 {
 159      hash = hash[:7]
 160    }
 61    // Per-commit violation details go to stderr so they do not pollute
 62    // piped output. The clean-pass summary below goes to stdout.
 163    _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s %s\n", hash, commits[i].Message)
 164    for _, v := range violations {
 165      totalViolations++
 166      icon := "[WARN]"
 167      if v.Severity == domain.LintError {
 168        icon = "[ERROR]"
 169        hasErrors = true
 170      }
 171      _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "  %s %s: %s\n", icon, v.Rule, v.Message)
 72    }
 173    _, _ = fmt.Fprintln(cmd.ErrOrStderr())
 74  }
 75
 176  if totalViolations == 0 {
 177    _, _ = fmt.Fprintf(cmd.OutOrStdout(), "All %d commit(s) pass lint checks.\n", len(commits))
 178    return nil
 179  }
 80
 181  _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Found %d violation(s) in %d commit(s).\n", totalViolations, len(commits))
 182  if hasErrors {
 183    // Violations already printed above; return ErrQuietExit so main exits
 184    // with code 1 without printing a redundant error message.
 185    return ErrQuietExit
 186  }
 087  return nil
 88}

/home/runner/work/go-semantic-release/go-semantic-release/internal/adapters/cli/plan.go

#LineLine coverage
 1package cli
 2
 3import (
 4  "encoding/json"
 5  "fmt"
 6  "io"
 7
 8  "github.com/spf13/cobra"
 9
 10  "github.com/jedi-knights/go-semantic-release/internal/domain"
 11)
 12
 1213func newPlanCmd(opts *rootOptions) *cobra.Command {
 1214  return &cobra.Command{
 1215    Use:   "plan",
 1216    Short: "Show the release plan without executing",
 1217    Long:  `Analyze commits and show what would happen during a release, including version bumps and affected projects.`
 018    RunE: func(cmd *cobra.Command, args []string) error {
 019      return runPlan(cmd, args, opts)
 020    },
 21  }
 22}
 23
 324func runPlan(cmd *cobra.Command, _ []string, opts *rootOptions) error {
 325  ctx := cmd.Context()
 326
 327  container, workDir, err := buildContainerWithWorkDir(opts)
 028  if err != nil {
 029    return err
 030  }
 31
 332  cfg := container.Config()
 333
 334  projects, err := container.ProjectDetector().Detect(ctx, workDir)
 035  if err != nil {
 036    return fmt.Errorf("detecting projects: %w", err)
 037  }
 38
 139  if opts.project != "" {
 140    projects = filterProject(projects, opts.project)
 141    if len(projects) == 0 {
 142      return fmt.Errorf("project %q not found", opts.project)
 143    }
 44  }
 45
 246  commits, err := container.CommitAnalyzer().Analyze(ctx, "")
 047  if err != nil {
 048    return fmt.Errorf("analyzing commits: %w", err)
 049  }
 50
 251  branch, err := container.GitRepository().CurrentBranch(ctx)
 052  if err != nil {
 053    return fmt.Errorf("resolving current branch: %w", err)
 054  }
 255  policy := domain.FindBranchPolicy(cfg.Branches, branch)
 256
 257  // true = dry-run: plan only previews what would be released, never executes.
 258  plan, err := container.ReleasePlanner().Plan(ctx, projects, commits, cfg.ReleaseMode, policy, true)
 059  if err != nil {
 060    return fmt.Errorf("planning release: %w", err)
 061  }
 62
 263  return printPlan(cmd.OutOrStdout(), plan, opts.jsonOut)
 64}
 65
 66// printPlan renders the release plan to w. asJSON controls whether output is
 67// JSON-encoded; it is passed explicitly rather than read from the package-level
 68// jsonOut flag so callers can exercise both rendering paths in tests without
 69// mutating shared state.
 670func printPlan(w io.Writer, plan *domain.ReleasePlan, asJSON bool) error {
 271  if asJSON {
 272    return json.NewEncoder(w).Encode(plan)
 273  }
 74
 475  _, _ = fmt.Fprintf(w, "Branch: %s\n", plan.Branch)
 476  _, _ = fmt.Fprintf(w, "Release mode: %s\n\n", modeString(plan))
 477
 178  if !plan.HasReleasableProjects() {
 179    _, _ = fmt.Fprintln(w, "No releasable changes found.")
 180    return nil
 181  }
 82
 383  for i := range plan.Projects {
 384    status := "skip"
 385    if plan.Projects[i].ShouldRelease {
 386      status = "release"
 387    }
 388    _, _ = fmt.Fprintf(w, "  %s [%s]\n", displayProjectName(plan.Projects[i].Project), status)
 389    _, _ = fmt.Fprintf(w, "    Current: %s\n", plan.Projects[i].CurrentVersion)
 390    if plan.Projects[i].ShouldRelease {
 391      _, _ = fmt.Fprintf(w, "    Next:    %s (%s)\n", plan.Projects[i].NextVersion, plan.Projects[i].ReleaseType)
 392    }
 393    _, _ = fmt.Fprintf(w, "    Commits: %d\n", len(plan.Projects[i].Commits))
 394    _, _ = fmt.Fprintf(w, "    Reason:  %s\n\n", plan.Projects[i].Reason)
 95  }
 396  return nil
 97}
 98
 799func modeString(plan *domain.ReleasePlan) string {
 1100  if plan.Policy != nil && plan.Policy.Prerelease {
 1101    return fmt.Sprintf("prerelease (%s)", plan.Policy.Channel)
 1102  }
 6103  return "stable"
 104}
 105
 8106func displayProjectName(p domain.Project) string {
 6107  if p.Name == "" || p.Name == "root" {
 6108    return "(repository)"
 6109  }
 2110  return p.Name
 111}

/home/runner/work/go-semantic-release/go-semantic-release/internal/adapters/cli/release.go

#LineLine coverage
 1package cli
 2
 3import (
 4  "encoding/json"
 5  "fmt"
 6  "io"
 7
 8  "github.com/spf13/cobra"
 9
 10  "github.com/jedi-knights/go-semantic-release/internal/adapters/prompt"
 11  "github.com/jedi-knights/go-semantic-release/internal/domain"
 12  "github.com/jedi-knights/go-semantic-release/internal/ports"
 13)
 14
 15// runRelease is the default action when semantic-release is invoked without a subcommand.
 16// This matches the original semantic-release behavior.
 317func runRelease(cmd *cobra.Command, _ []string, opts *rootOptions) error {
 318  ctx := cmd.Context()
 319
 320  container, workDir, err := buildContainerWithWorkDir(opts)
 021  if err != nil {
 022    return err
 023  }
 24
 325  cfg := container.Config()
 326
 327  // Discover projects.
 328  projects, err := container.ProjectDetector().Detect(ctx, workDir)
 029  if err != nil {
 030    return fmt.Errorf("detecting projects: %w", err)
 031  }
 32
 33  // Filter to specific project if requested.
 034  if opts.project != "" {
 035    projects = filterProject(projects, opts.project)
 036    if len(projects) == 0 {
 037      return fmt.Errorf("project %q not found", opts.project)
 038    }
 39  }
 40
 41  // Analyze commits.
 342  commits, err := container.CommitAnalyzer().Analyze(ctx, "")
 043  if err != nil {
 044    return fmt.Errorf("analyzing commits: %w", err)
 045  }
 46
 47  // Build release plan.
 348  branch, err := container.GitRepository().CurrentBranch(ctx)
 049  if err != nil {
 050    return fmt.Errorf("resolving current branch: %w", err)
 051  }
 352  policy := domain.FindBranchPolicy(cfg.Branches, branch)
 353
 354  plan, err := container.ReleasePlanner().Plan(ctx, projects, commits, cfg.ReleaseMode, policy, cfg.DryRun)
 055  if err != nil {
 056    return fmt.Errorf("planning release: %w", err)
 057  }
 58
 159  if !plan.HasReleasableProjects() {
 160    _, _ = fmt.Fprintln(cmd.OutOrStdout(), "No releasable changes found.")
 161    return nil
 162  }
 63
 64  // Warn when dry-run was automatically engaged because we are not in CI.
 65  // This fires only for the release command — plan/lint/verify/etc. are unaffected.
 266  if cfg.DryRun && !opts.dryRun && !cfg.CI {
 267    _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "note: not running in CI — defaulting to dry run (pass --no-ci to override)")
 268  }
 69
 70  // Interactive confirmation before release.
 171  if shouldPrompt(cfg, opts) {
 072    if planErr := printPlan(cmd.OutOrStdout(), plan, opts.jsonOut); planErr != nil {
 073      return planErr
 074    }
 175    prompter := prompt.NewTerminalPrompter()
 176    confirmed, promptErr := prompter.Confirm("Proceed with release?")
 077    if promptErr != nil {
 078      return fmt.Errorf("reading confirmation: %w", promptErr)
 079    }
 180    if !confirmed {
 181      _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Release cancelled.")
 182      return nil
 183    }
 84  }
 85
 86  // Resolve plugins once before the loop. container.Plugins() uses sync.Once so
 87  // the build cost is incurred at most once, but the error check and call itself
 88  // belong outside the per-project loop for clarity.
 189  allPlugins, pluginsErr := container.Plugins()
 090  if pluginsErr != nil {
 091    return fmt.Errorf("loading plugins: %w", pluginsErr)
 092  }
 93
 94  // Run prepare step (update CHANGELOG.md, VERSION, etc.) for each releasable project.
 95  //
 96  // Note: changelog notes are generated here for the prepare plugins and again
 97  // inside executeProject for the tag annotation and publish payload. Both calls
 98  // use the same generator with identical inputs so the output is deterministic.
 99  // The duplication avoids coupling the executor to the prepare layer; a future
 100  // refactor could pass notes through ReleaseContext to eliminate the second call.
 1101  gen := container.ChangelogGenerator()
 1102  releasable := plan.ReleasableProjects()
 1103  for i := range releasable {
 1104    notes, notesErr := gen.Generate(releasable[i].NextVersion, releasable[i].Project.Name, releasable[i].Commits, cfg.Ch
 0105    if notesErr != nil {
 0106      return fmt.Errorf("generating notes: %w", notesErr)
 0107    }
 108
 1109    rc := &domain.ReleaseContext{
 1110      Config:         cfg,
 1111      Branch:         branch,
 1112      BranchPolicy:   policy,
 1113      DryRun:         cfg.DryRun,
 1114      CI:             cfg.CI,
 1115      RepositoryRoot: workDir,
 1116      CurrentProject: &releasable[i],
 1117      Notes:          notes,
 1118    }
 1119
 1120    for _, plugin := range allPlugins {
 4121      // Use the canonical ports.PreparePlugin interface so that any signature
 4122      // change to the port is caught at compile time rather than silently
 4123      // skipping the prepare step at runtime.
 0124      if pp, ok := plugin.(ports.PreparePlugin); ok {
 0125        if prepErr := pp.Prepare(ctx, rc); prepErr != nil {
 0126          return fmt.Errorf("prepare step: %w", prepErr)
 0127        }
 128      }
 129    }
 130  }
 131
 132  // Execute release (create tags, push, publish).
 1133  result, err := container.ReleaseExecutor().Execute(ctx, plan)
 0134  if err != nil {
 0135    return fmt.Errorf("executing release: %w", err)
 0136  }
 137
 1138  return printReleaseResult(cmd.OutOrStdout(), cmd.ErrOrStderr(), result, opts.jsonOut)
 139}
 140
 141// printReleaseResult renders the release result to w (stdout) and errW (stderr).
 142// asJSON is passed explicitly rather than read from the package-level jsonOut flag
 143// so callers can exercise both rendering paths in tests without mutating shared state.
 7144func printReleaseResult(w, errW io.Writer, result *domain.ReleaseResult, asJSON bool) error {
 1145  if asJSON {
 1146    return json.NewEncoder(w).Encode(result)
 1147  }
 148
 6149  for i := range result.Projects {
 2150    if result.Projects[i].Skipped {
 2151      // Use SkipReason from the result so the label is accurate regardless of
 2152      // why the project was skipped (dry run, policy gate, etc.).
 2153      _, _ = fmt.Fprintf(w, "[%s] %s: %s → %s (tag: %s)\n",
 2154        result.Projects[i].SkipReason,
 2155        projectName(result.Projects[i]),
 2156        result.Projects[i].CurrentVersion.String(),
 2157        result.Projects[i].Version.String(),
 2158        result.Projects[i].TagName)
 2159      continue
 160    }
 2161    if result.Projects[i].Error != nil {
 2162      _, _ = fmt.Fprintf(errW, "ERROR %s: %v\n", projectName(result.Projects[i]), result.Projects[i].Error)
 2163      continue
 164    }
 2165    _, _ = fmt.Fprintf(w, "Released %s %s (tag: %s)\n", projectName(result.Projects[i]), result.Projects[i].Version, res
 1166    if result.Projects[i].PublishURL != "" {
 1167      _, _ = fmt.Fprintf(w, "  → %s\n", result.Projects[i].PublishURL)
 1168    }
 169  }
 170
 171  // Per-project errors were already printed to stderr above. Return ErrQuietExit
 172  // so main exits with code 1 without duplicating the error messages.
 2173  if result.HasErrors() {
 2174    return ErrQuietExit
 2175  }
 4176  return nil
 177}
 178
 8179func projectName(pr domain.ProjectReleaseResult) string {
 2180  if pr.Project.Name == "" {
 2181    return "repo"
 2182  }
 6183  return pr.Project.Name
 184}
 185
 5186func filterProject(projects []domain.Project, name string) []domain.Project {
 5187  for _, p := range projects {
 1188    if p.Name == name {
 1189      return []domain.Project{p}
 1190    }
 191  }
 4192  return nil
 193}
 194
 195// shouldPrompt determines whether to show an interactive confirmation.
 196// It uses cfg.CI (already resolved by applyFlagAndEnvOverrides) rather than
 197// re-inspecting environment variables, keeping CI detection in one place.
 7198func shouldPrompt(cfg domain.Config, opts *rootOptions) bool {
 2199  if opts.noInteractive {
 2200    return false
 2201  }
 1202  if opts.interactive {
 1203    return true
 1204  }
 2205  if cfg.Interactive != nil {
 2206    return *cfg.Interactive
 2207  }
 208  // Auto-detect: prompt when running locally in a terminal, not in CI.
 2209  return !cfg.CI && prompt.IsTerminal()
 210}

/home/runner/work/go-semantic-release/go-semantic-release/internal/adapters/cli/root.go

#LineLine coverage
 1package cli
 2
 3import (
 4  "errors"
 5  "fmt"
 6  "os"
 7  "strings"
 8
 9  "github.com/spf13/cobra"
 10
 11  adapterconfig "github.com/jedi-knights/go-semantic-release/internal/adapters/config"
 12  "github.com/jedi-knights/go-semantic-release/internal/di"
 13  "github.com/jedi-knights/go-semantic-release/internal/domain"
 14  "github.com/jedi-knights/go-semantic-release/internal/platform"
 15)
 16
 17// ErrQuietExit signals that the command has already printed its own error output
 18// and the caller (main) should exit with a non-zero code without printing anything
 19// further. This avoids duplicate error messages for commands like lint and verify
 20// that report structured output before failing.
 21var ErrQuietExit = errors.New("quiet exit")
 22
 23// rootOptions holds all flag values bound to the root command. Using a struct
 24// rather than package-level vars eliminates the shared-mutable-state data race
 25// that occurs when Execute is called concurrently (e.g., in parallel tests).
 26type rootOptions struct {
 27  // Original semantic-release flags.
 28  branches      []string
 29  repositoryURL string
 30  tagFormat     string
 31  plugins       []string
 32  extends       []string
 33  dryRun        bool
 34  ciFlag        bool
 35  noCIFlag      bool
 36  debug         bool
 37
 38  // Extension flags (Go-specific).
 39  cfgFile       string
 40  project       string
 41  jsonOut       bool
 42  interactive   bool
 43  noInteractive bool
 44}
 45
 46// NewRootCmd creates the root cobra command.
 47// The default action (no subcommand) runs the release, matching the original semantic-release behavior.
 548func NewRootCmd() *cobra.Command {
 549  opts := &rootOptions{}
 550
 551  root := &cobra.Command{
 552    Use:   "semantic-release [options]",
 553    Short: "Run automated package publishing",
 554    Long: `semantic-release automates the whole package release workflow including:
 555determining the next version number, generating the release notes,
 556and publishing the package.
 557
 558This is a native Go implementation compatible with the semantic-release CLI.`,
 559    // SilenceUsage prevents Cobra from printing the usage string on every RunE error.
 560    // SilenceErrors prevents Cobra from printing the error itself; main handles that
 561    // so it can filter ErrQuietExit without double-printing.
 562    SilenceUsage:  true,
 563    SilenceErrors: true,
 064    RunE: func(cmd *cobra.Command, args []string) error {
 065      return runRelease(cmd, args, opts)
 066    },
 67  }
 68
 69  // Signal-aware context is wired in main via ExecuteContext so the stop()
 70  // function is always deferred regardless of whether the command succeeds or fails.
 71
 72  // Flags matching the original semantic-release CLI (persistent so subcommands inherit them).
 573  pf := root.PersistentFlags()
 574  pf.StringArrayVarP(&opts.branches, "branches", "b", nil, "Git branches to release from (main/master are default; for o
 575  pf.StringVarP(&opts.repositoryURL, "repository-url", "r", "", "Git repository URL")
 576  pf.StringVarP(&opts.tagFormat, "tag-format", "t", "", "Git tag format")
 577  pf.StringArrayVarP(&opts.plugins, "plugins", "p", nil, "Plugins")
 578  pf.StringArrayVarP(&opts.extends, "extends", "e", nil, "Shareable configurations")
 579  pf.BoolVarP(&opts.dryRun, "dry-run", "d", false, "Skip publishing")
 580  pf.BoolVar(&opts.ciFlag, "ci", false, "Toggle CI verifications")
 581  pf.BoolVar(&opts.noCIFlag, "no-ci", false, "Skip CI verifications")
 582  pf.BoolVar(&opts.debug, "debug", false, "Output debugging information")
 583
 584  // Extension flags (Go-specific, also persistent).
 585  pf.StringVar(&opts.cfgFile, "config", "", "config file (default: .semantic-release.yaml)")
 586  pf.StringVar(&opts.project, "project", "", "target a specific project in a monorepo")
 587  pf.BoolVar(&opts.jsonOut, "json", false, "output in JSON format")
 588  pf.BoolVar(&opts.interactive, "interactive", false, "prompt for confirmation before release")
 589  pf.BoolVar(&opts.noInteractive, "no-interactive", false, "disable interactive prompts")
 590
 591  // Subcommands are Go-specific extensions beyond the original semantic-release.
 592  root.AddCommand(
 593    newPlanCmd(opts),
 594    newVersionCmd(opts),
 595    newChangelogCmd(opts),
 596    newDetectProjectsCmd(opts),
 597    newVerifyCmd(opts),
 598    newConfigCmd(),
 599    newLintCmd(opts),
 5100  )
 5101
 5102  // Enforce mutually exclusive flag pairs. Cobra 1.5+ fires a clear error
 5103  // message when both flags in a pair are set on the same invocation.
 5104  // Note: these checks apply to the root command; for subcommands that
 5105  // inherit the persistent flags, applyFlagAndEnvOverrides handles conflicts
 5106  // via switch/case with first-wins semantics (ciFlag wins over noCIFlag when both are set).
 5107  root.MarkFlagsMutuallyExclusive("ci", "no-ci")
 5108  root.MarkFlagsMutuallyExclusive("interactive", "no-interactive")
 5109
 5110  return root
 111}
 112
 113// buildContainerWithWorkDir creates a DI container and also returns the resolved
 114// working directory so callers do not need a second os.Getwd() call.
 17115func buildContainerWithWorkDir(opts *rootOptions) (*di.Container, string, error) {
 17116  provider := adapterconfig.NewViperProvider()
 17117  cfg, err := provider.Load(opts.cfgFile)
 0118  if err != nil {
 0119    return nil, "", fmt.Errorf("loading config: %w", err)
 0120  }
 121
 122  // Apply CLI flag and environment overrides.
 17123  applyFlagAndEnvOverrides(&cfg, opts)
 17124
 17125  workDir, err := os.Getwd()
 0126  if err != nil {
 0127    return nil, "", fmt.Errorf("getting working directory: %w", err)
 0128  }
 129
 17130  container, err := di.NewContainer(cfg, workDir)
 0131  if err != nil {
 0132    return nil, "", fmt.Errorf("creating container: %w", err)
 0133  }
 0134  if cfg.Debug {
 0135    container.WithLogger(platform.NewConsoleLogger(os.Stderr, platform.LogDebug))
 0136  }
 137
 17138  return container, workDir, nil
 139}
 140
 141// applyFlagAndEnvOverrides applies CLI flag values and environment-detected settings
 142// (e.g. CI auto-detection) on top of the loaded configuration.
 26143func applyFlagAndEnvOverrides(cfg *domain.Config, opts *rootOptions) {
 1144  if opts.dryRun {
 1145    cfg.DryRun = true
 1146  }
 1147  if opts.debug {
 1148    cfg.Debug = true
 1149  }
 150
 151  // --branches / -b overrides config branches.
 1152  if len(opts.branches) > 0 {
 1153    cfg.Branches = parseBranchFlags(opts.branches)
 1154  }
 155
 156  // --repository-url / -r overrides config.
 1157  if opts.repositoryURL != "" {
 1158    cfg.RepositoryURL = opts.repositoryURL
 1159  }
 160
 161  // --tag-format / -t overrides config.
 1162  if opts.tagFormat != "" {
 1163    cfg.TagFormat = opts.tagFormat
 1164  }
 165
 166  // --extends / -e overrides config.
 1167  if len(opts.extends) > 0 {
 1168    cfg.Extends = opts.extends
 1169  }
 170
 171  // --plugins / -p overrides config.
 1172  if len(opts.plugins) > 0 {
 1173    cfg.Plugins = opts.plugins
 1174  }
 175
 176  // CI detection: --ci forces CI mode, --no-ci disables it, otherwise
 177  // auto-detect via environment variables. platform.IsCI() is only called
 178  // when neither flag is set to avoid unnecessary env-var inspection.
 26179  var isCI bool
 26180  switch {
 23181  case opts.ciFlag:
 23182    isCI = true
 3183  case opts.noCIFlag:
 3184    isCI = false
 0185  default:
 0186    isCI = platform.IsCI()
 187  }
 26188  cfg.CI = isCI
 26189
 26190  // When not in CI and dry-run wasn't explicitly set, default to dry-run so
 26191  // local runs never accidentally publish. Users who intend a real local release
 26192  // must pass --no-ci explicitly.
 26193  // Note: the user-facing warning for this auto dry-run is printed by runRelease
 26194  // (not here) so it only appears for the release command, not plan/lint/verify/etc.
 3195  if !isCI && !opts.dryRun {
 3196    cfg.DryRun = true
 3197  }
 198}
 199
 200// parseBranchFlags converts CLI branch strings into BranchPolicy entries.
 201// Empty strings after trimming are silently dropped — this is intentional: a
 202// user passing --branches "" or trailing commas should not create a policy with
 203// a blank branch name.
 7204func parseBranchFlags(branchNames []string) []domain.BranchPolicy {
 7205  policies := make([]domain.BranchPolicy, 0, len(branchNames))
 7206  for _, name := range branchNames {
 9207    // Support comma-separated values.
 9208    for _, n := range strings.Split(name, ",") {
 10209      n = strings.TrimSpace(n)
 2210      if n == "" {
 2211        continue
 212      }
 213      // IsDefault is set for the two canonical branch names used by most
 214      // Git hosting platforms. Any other name passed via --branches gets
 215      // IsDefault: false. Users who use a different default branch name
 216      // (e.g. "trunk") must set is_default in the config file instead.
 8217      policies = append(policies, domain.BranchPolicy{
 8218        Name:      n,
 8219        IsDefault: n == "main" || n == "master",
 8220      })
 221    }
 222  }
 7223  return policies
 224}

/home/runner/work/go-semantic-release/go-semantic-release/internal/adapters/cli/verify.go

#LineLine coverage
 1package cli
 2
 3import (
 4  "fmt"
 5
 6  "github.com/spf13/cobra"
 7)
 8
 109func newVerifyCmd(opts *rootOptions) *cobra.Command {
 1010  return &cobra.Command{
 1011    Use:   "verify",
 1012    Short: "Verify release conditions are met",
 1013    Long:  `Check that all prerequisites for a release are satisfied (branch policy, GitHub config, etc.).`,
 014    RunE: func(cmd *cobra.Command, args []string) error {
 015      return runVerify(cmd, args, opts)
 016    },
 17  }
 18}
 19
 120func runVerify(cmd *cobra.Command, _ []string, opts *rootOptions) error {
 121  ctx := cmd.Context()
 122
 123  container, _, err := buildContainerWithWorkDir(opts)
 024  if err != nil {
 025    return err
 026  }
 27
 128  result, err := container.ConditionVerifier().Verify(ctx)
 029  if err != nil {
 030    return fmt.Errorf("verifying conditions: %w", err)
 031  }
 32
 033  if result.Passed {
 034    _, _ = fmt.Fprintln(cmd.OutOrStdout(), "All release conditions verified.")
 035    return nil
 036  }
 37
 38  // Failures already printed to stderr; return ErrQuietExit so main exits
 39  // with code 1 without printing a redundant error message.
 140  _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Release conditions not met:")
 141  for _, f := range result.Failures {
 142    _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "  [FAIL] %s\n", f)
 143  }
 144  return ErrQuietExit
 45}

/home/runner/work/go-semantic-release/go-semantic-release/internal/adapters/cli/version.go

#LineLine coverage
 1package cli
 2
 3import (
 4  "fmt"
 5
 6  "github.com/spf13/cobra"
 7
 8  "github.com/jedi-knights/go-semantic-release/internal/domain"
 9)
 10
 1511func newVersionCmd(opts *rootOptions) *cobra.Command {
 1512  return &cobra.Command{
 1513    Use:   "version",
 1514    Short: "Show the current and next version",
 015    RunE: func(cmd *cobra.Command, args []string) error {
 016      return runVersion(cmd, args, opts)
 017    },
 18  }
 19}
 20
 321func runVersion(cmd *cobra.Command, _ []string, opts *rootOptions) error {
 322  ctx := cmd.Context()
 323
 324  container, workDir, err := buildContainerWithWorkDir(opts)
 025  if err != nil {
 026    return err
 027  }
 28
 329  cfg := container.Config()
 330
 331  projects, err := container.ProjectDetector().Detect(ctx, workDir)
 032  if err != nil {
 033    return fmt.Errorf("detecting projects: %w", err)
 034  }
 35
 136  if opts.project != "" {
 137    projects = filterProject(projects, opts.project)
 138    if len(projects) == 0 {
 139      return fmt.Errorf("project %q not found", opts.project)
 140    }
 41  }
 42
 243  commits, err := container.CommitAnalyzer().Analyze(ctx, "")
 044  if err != nil {
 045    return fmt.Errorf("analyzing commits: %w", err)
 046  }
 47
 248  branch, err := container.GitRepository().CurrentBranch(ctx)
 049  if err != nil {
 050    return fmt.Errorf("resolving current branch: %w", err)
 051  }
 252  policy := domain.FindBranchPolicy(cfg.Branches, branch)
 253
 254  plan, err := container.ReleasePlanner().Plan(ctx, projects, commits, cfg.ReleaseMode, policy, true)
 055  if err != nil {
 056    return fmt.Errorf("planning release: %w", err)
 057  }
 58
 259  for i := range plan.Projects {
 260    name := displayProjectName(plan.Projects[i].Project)
 161    if plan.Projects[i].ShouldRelease {
 162      _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s: %s → %s\n", name, plan.Projects[i].CurrentVersion, plan.Projects[i].Nex
 163    } else {
 164      _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s: %s (no change)\n", name, plan.Projects[i].CurrentVersion)
 165    }
 66  }
 267  return nil
 68}