• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kubernetes-sigs / kubebuilder / 16719236638

04 Aug 2025 09:23AM UTC coverage: 70.724% (-0.07%) from 70.793%
16719236638

push

github

web-flow
Merge pull request #4968 from Shubhamag12/issue-4930-logrus

⚠️  Migrate from logrus to log/slog

21 of 58 new or added lines in 9 files covered. (36.21%)

1 existing line in 1 file now uncovered.

2911 of 4116 relevant lines covered (70.72%)

14.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.63
/pkg/cli/cli.go
1
/*
2
Copyright 2020 The Kubernetes Authors.
3

4
Licensed under the Apache License, Version 2.0 (the "License");
5
you may not use this file except in compliance with the License.
6
You may obtain a copy of the License at
7

8
    http://www.apache.org/licenses/LICENSE-2.0
9

10
Unless required by applicable law or agreed to in writing, software
11
distributed under the License is distributed on an "AS IS" BASIS,
12
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
See the License for the specific language governing permissions and
14
limitations under the License.
15
*/
16

17
package cli
18

19
import (
20
        "errors"
21
        "fmt"
22
        log "log/slog"
23
        "os"
24
        "strings"
25

26
        "github.com/spf13/afero"
27
        "github.com/spf13/cobra"
28
        "github.com/spf13/pflag"
29

30
        "sigs.k8s.io/kubebuilder/v4/pkg/config"
31
        yamlstore "sigs.k8s.io/kubebuilder/v4/pkg/config/store/yaml"
32
        "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
33
        "sigs.k8s.io/kubebuilder/v4/pkg/model/stage"
34
        "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
35
)
36

37
const (
38
        noticeColor    = "\033[1;33m%s\033[0m"
39
        deprecationFmt = "[Deprecation Notice] %s\n\n"
40

41
        pluginsFlag        = "plugins"
42
        projectVersionFlag = "project-version"
43
)
44

45
// CLI is the command line utility that is used to scaffold kubebuilder project files.
46
type CLI struct {
47
        /* Fields set by Option */
48

49
        // Root command name. It is injected downstream to provide correct help, usage, examples and errors.
50
        commandName string
51
        // Full CLI version string.
52
        version string
53
        // CLI version string (just the CLI version number, no extra information).
54
        cliVersion string
55
        // CLI root's command description.
56
        description string
57
        // Plugins registered in the CLI.
58
        plugins map[string]plugin.Plugin
59
        // Default plugins in case none is provided and a config file can't be found.
60
        defaultPlugins map[config.Version][]string
61
        // Default project version in case none is provided and a config file can't be found.
62
        defaultProjectVersion config.Version
63
        // Commands injected by options.
64
        extraCommands []*cobra.Command
65
        // Alpha commands injected by options.
66
        extraAlphaCommands []*cobra.Command
67
        // Whether to add a completion command to the CLI.
68
        completionCommand bool
69

70
        /* Internal fields */
71

72
        // Plugin keys to scaffold with.
73
        pluginKeys []string
74
        // Project version to scaffold.
75
        projectVersion config.Version
76

77
        // A filtered set of plugins that should be used by command constructors.
78
        resolvedPlugins []plugin.Plugin
79

80
        // Root command.
81
        cmd *cobra.Command
82

83
        // Underlying fs
84
        fs machinery.Filesystem
85
}
86

87
// New creates a new CLI instance.
88
//
89
// It follows the functional options pattern in order to customize the resulting CLI.
90
//
91
// It returns an error if any of the provided options fails. As some processing needs
92
// to be done, execution errors may be found here. Instead of returning an error, this
93
// function will return a valid CLI that errors in Run so that help is provided to the
94
// user.
95
func New(options ...Option) (*CLI, error) {
11✔
96
        // Create the CLI.
11✔
97
        c, err := newCLI(options...)
11✔
98
        if err != nil {
12✔
99
                return nil, err
1✔
100
        }
1✔
101

102
        // Build the cmd tree.
103
        if err := c.buildCmd(); err != nil {
11✔
104
                c.cmd.RunE = errCmdFunc(err)
1✔
105
                return c, nil
1✔
106
        }
1✔
107

108
        // Add extra commands injected by options.
109
        if err := c.addExtraCommands(); err != nil {
10✔
110
                return nil, err
1✔
111
        }
1✔
112

113
        // Add extra alpha commands injected by options.
114
        if err := c.addExtraAlphaCommands(); err != nil {
9✔
115
                return nil, err
1✔
116
        }
1✔
117

118
        // Write deprecation notices after all commands have been constructed.
119
        c.printDeprecationWarnings()
7✔
120

7✔
121
        return c, nil
7✔
122
}
123

124
// newCLI creates a default CLI instance and applies the provided options.
125
// It is as a separate function for test purposes.
126
func newCLI(options ...Option) (*CLI, error) {
40✔
127
        // Default CLI options.
40✔
128
        c := &CLI{
40✔
129
                commandName: "kubebuilder",
40✔
130
                description: `CLI tool for building Kubernetes extensions and tools.
40✔
131
`,
40✔
132
                plugins:        make(map[string]plugin.Plugin),
40✔
133
                defaultPlugins: make(map[config.Version][]string),
40✔
134
                fs:             machinery.Filesystem{FS: afero.NewOsFs()},
40✔
135
        }
40✔
136

40✔
137
        // Apply provided options.
40✔
138
        for _, option := range options {
91✔
139
                if err := option(c); err != nil {
68✔
140
                        return nil, err
17✔
141
                }
17✔
142
        }
143

144
        return c, nil
23✔
145
}
146

147
// buildCmd creates the underlying cobra command and stores it internally.
148
func (c *CLI) buildCmd() error {
12✔
149
        c.cmd = c.newRootCmd()
12✔
150

12✔
151
        var uve config.UnsupportedVersionError
12✔
152

12✔
153
        // Get project version and plugin keys.
12✔
154
        switch err := c.getInfo(); {
12✔
155
        case err == nil:
10✔
156
        case errors.As(err, &uve) && uve.Version.Compare(config.Version{Number: 3, Stage: stage.Alpha}) == 0:
1✔
157
                // Check if the corresponding stable version exists, set c.projectVersion and break
1✔
158
                stableVersion := config.Version{
1✔
159
                        Number: uve.Version.Number,
1✔
160
                }
1✔
161
                if config.IsRegistered(stableVersion) {
2✔
162
                        // Use the stableVersion
1✔
163
                        c.projectVersion = stableVersion
1✔
164
                } else {
1✔
165
                        // stable version not registered, let's bail out
×
166
                        return err
×
167
                }
×
168
        default:
1✔
169
                return err
1✔
170
        }
171

172
        // Resolve plugins for project version and plugin keys.
173
        if err := c.resolvePlugins(); err != nil {
12✔
174
                return err
1✔
175
        }
1✔
176

177
        // Add the subcommands
178
        c.addSubcommands()
10✔
179

10✔
180
        return nil
10✔
181
}
182

183
// getInfo obtains the plugin keys and project version resolving conflicts between the project config file and flags.
184
func (c *CLI) getInfo() error {
12✔
185
        // Get plugin keys and project version from project configuration file
12✔
186
        // We discard the error if file doesn't exist because not being able to read a project configuration
12✔
187
        // file is not fatal for some commands. The ones that require it need to check its existence later.
12✔
188
        hasConfigFile := true
12✔
189
        if err := c.getInfoFromConfigFile(); errors.Is(err, os.ErrNotExist) {
22✔
190
                hasConfigFile = false
10✔
191
        } else if err != nil {
14✔
192
                return err
2✔
193
        }
2✔
194

195
        // We can't early return here in case a project configuration file was found because
196
        // this command call may override the project plugins.
197

198
        // Get project version and plugin info from flags
199
        if err := c.getInfoFromFlags(hasConfigFile); err != nil {
10✔
200
                return err
×
201
        }
×
202

203
        // Get project version and plugin info from defaults
204
        c.getInfoFromDefaults()
10✔
205

10✔
206
        return nil
10✔
207
}
208

209
// getInfoFromConfigFile obtains the project version and plugin keys from the project config file.
210
func (c *CLI) getInfoFromConfigFile() error {
12✔
211
        // Read the project configuration file
12✔
212
        cfg := yamlstore.New(c.fs)
12✔
213

12✔
214
        // Workaround for https://github.com/kubernetes-sigs/kubebuilder/issues/4433
12✔
215
        //
12✔
216
        // This allows the `kubebuilder alpha generate` command to work with old projects
12✔
217
        // that use plugin versions no longer supported (like go.kubebuilder.io/v3).
12✔
218
        //
12✔
219
        // We read the PROJECT file into memory and update the plugin version (e.g. from v3 to v4)
12✔
220
        // before the CLI tries to load it. This avoids errors during config loading
12✔
221
        // and lets users migrate their project layout from go/v3 to go/v4.
12✔
222

12✔
223
        if isAlphaGenerateCommand(os.Args[1:]) {
12✔
224
                // Patch raw file bytes before unmarshalling
×
225
                if err := patchProjectFileInMemoryIfNeeded(c.fs.FS, yamlstore.DefaultPath); err != nil {
×
226
                        return err
×
227
                }
×
228
        }
229

230
        if err := cfg.Load(); err != nil {
24✔
231
                return fmt.Errorf("error loading configuration: %w", err)
12✔
232
        }
12✔
233

234
        return c.getInfoFromConfig(cfg.Config())
×
235
}
236

237
// isAlphaGenerateCommand checks if the command invocation is `kubebuilder alpha generate`
238
// by scanning os.Args (excluding global flags). It returns true if "alpha" is followed by "generate".
239
func isAlphaGenerateCommand(args []string) bool {
12✔
240
        positional := []string{}
12✔
241
        skip := false
12✔
242

12✔
243
        for i := 0; i < len(args); i++ {
75✔
244
                arg := args[i]
63✔
245

63✔
246
                // Skip flags and their values
63✔
247
                if strings.HasPrefix(arg, "-") {
124✔
248
                        // If the flag is in --flag=value format, skip only this one
61✔
249
                        if strings.Contains(arg, "=") {
109✔
250
                                continue
48✔
251
                        }
252
                        // If it's --flag value format, skip next one too
253
                        if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
14✔
254
                                skip = true
1✔
255
                        }
1✔
256
                        continue
13✔
257
                }
258
                if skip {
3✔
259
                        skip = false
1✔
260
                        continue
1✔
261
                }
262
                positional = append(positional, arg)
1✔
263
        }
264

265
        // Check for `alpha generate` in positional arguments
266
        for i := 0; i < len(positional)-1; i++ {
12✔
267
                if positional[i] == "alpha" && positional[i+1] == "generate" {
×
268
                        return true
×
269
                }
×
270
        }
271

272
        return false
12✔
273
}
274

275
// patchProjectFileInMemoryIfNeeded updates deprecated plugin keys in the PROJECT file in place,
276
// so that users can run `kubebuilder alpha generate` even with older plugin layouts.
277
//
278
// See: https://github.com/kubernetes-sigs/kubebuilder/issues/4433
279
//
280
// This ensures the CLI can successfully load the config without failing on unsupported plugin versions.
281
func patchProjectFileInMemoryIfNeeded(fs afero.Fs, path string) error {
×
282
        type pluginReplacement struct {
×
283
                Old string
×
284
                New string
×
285
        }
×
286

×
287
        replacements := []pluginReplacement{
×
288
                {"go.kubebuilder.io/v2", "go.kubebuilder.io/v4"},
×
289
                {"go.kubebuilder.io/v3", "go.kubebuilder.io/v4"},
×
290
                {"go.kubebuilder.io/v3-alpha", "go.kubebuilder.io/v4"},
×
291
        }
×
292

×
293
        content, err := afero.ReadFile(fs, path)
×
294
        if err != nil {
×
295
                return nil
×
296
        }
×
297

298
        original := string(content)
×
299
        modified := original
×
300

×
301
        for _, rep := range replacements {
×
302
                if strings.Contains(modified, rep.Old) {
×
303
                        modified = strings.ReplaceAll(modified, rep.Old, rep.New)
×
NEW
304
                        log.Warn("Project is using an old and unsupported plugin layout",
×
NEW
305
                                "old_layout", rep.Old,
×
NEW
306
                                "new_layout", rep.New,
×
NEW
307
                                "note", "Replace in memory to allow `alpha generate` to work.",
×
NEW
308
                        )
×
UNCOV
309
                }
×
310
        }
311

312
        if modified != original {
×
313
                err := afero.WriteFile(fs, path, []byte(modified), 0o755)
×
314
                if err != nil {
×
315
                        return fmt.Errorf("failed to write patched PROJECT file: %w", err)
×
316
                }
×
317
        }
318

319
        return nil
×
320
}
321

322
// getInfoFromConfig obtains the project version and plugin keys from the project config.
323
// It is extracted from getInfoFromConfigFile for testing purposes.
324
func (c *CLI) getInfoFromConfig(projectConfig config.Config) error {
3✔
325
        c.pluginKeys = projectConfig.GetPluginChain()
3✔
326
        c.projectVersion = projectConfig.GetVersion()
3✔
327

3✔
328
        for _, pluginKey := range c.pluginKeys {
7✔
329
                if err := plugin.ValidateKey(pluginKey); err != nil {
5✔
330
                        return fmt.Errorf("invalid plugin key found in project configuration file: %w", err)
1✔
331
                }
1✔
332
        }
333

334
        return nil
2✔
335
}
336

337
// getInfoFromFlags obtains the project version and plugin keys from flags.
338
func (c *CLI) getInfoFromFlags(hasConfigFile bool) error {
22✔
339
        // Partially parse the command line arguments
22✔
340
        fs := pflag.NewFlagSet("base", pflag.ContinueOnError)
22✔
341

22✔
342
        // Load the base command global flags
22✔
343
        fs.AddFlagSet(c.cmd.PersistentFlags())
22✔
344

22✔
345
        // If we were unable to load the project configuration, we should also accept the project version flag
22✔
346
        var projectVersionStr string
22✔
347
        if !hasConfigFile {
44✔
348
                fs.StringVar(&projectVersionStr, projectVersionFlag, "", "project version")
22✔
349
        }
22✔
350

351
        // FlagSet special cases --help and -h, so we need to create a dummy flag with these 2 values to prevent the default
352
        // behavior (printing the usage of this FlagSet) as we want to print the usage message of the underlying command.
353
        fs.BoolP("help", "h", false, fmt.Sprintf("help for %s", c.commandName))
22✔
354

22✔
355
        // Omit unknown flags to avoid parsing errors
22✔
356
        fs.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true}
22✔
357

22✔
358
        // Parse the arguments
22✔
359
        if err := fs.Parse(os.Args[1:]); err != nil {
22✔
360
                return fmt.Errorf("could not parse flags: %w", err)
×
361
        }
×
362

363
        // If any plugin key was provided, replace those from the project configuration file
364
        if pluginKeys, err := fs.GetStringSlice(pluginsFlag); err != nil {
22✔
365
                return fmt.Errorf("invalid flag %q: %w", pluginsFlag, err)
×
366
        } else if len(pluginKeys) != 0 {
30✔
367
                // Remove leading and trailing spaces and validate the plugin keys
8✔
368
                for i, key := range pluginKeys {
24✔
369
                        pluginKeys[i] = strings.TrimSpace(key)
16✔
370
                        if err := plugin.ValidateKey(pluginKeys[i]); err != nil {
17✔
371
                                return fmt.Errorf("invalid plugin %q found in flags: %w", pluginKeys[i], err)
1✔
372
                        }
1✔
373
                }
374

375
                c.pluginKeys = pluginKeys
7✔
376
        }
377

378
        // If the project version flag was accepted but not provided keep the empty version and try to resolve it later,
379
        // else validate the provided project version
380
        if projectVersionStr != "" {
26✔
381
                if err := c.projectVersion.Parse(projectVersionStr); err != nil {
6✔
382
                        return fmt.Errorf("invalid project version flag: %w", err)
1✔
383
                }
1✔
384
        }
385

386
        return nil
20✔
387
}
388

389
// getInfoFromDefaults obtains the plugin keys, and maybe the project version from the default values
390
func (c *CLI) getInfoFromDefaults() {
14✔
391
        // Should not use default values if a plugin was already set
14✔
392
        // This checks includes the case where a project configuration file was found,
14✔
393
        // as it will always have at least one plugin key set by now
14✔
394
        if len(c.pluginKeys) != 0 {
16✔
395
                // We don't assign a default value for project version here because we may be able to
2✔
396
                // resolve the project version after resolving the plugins.
2✔
397
                return
2✔
398
        }
2✔
399

400
        // If the user provided a project version, use the default plugins for that project version
401
        if c.projectVersion.Validate() == nil {
13✔
402
                c.pluginKeys = c.defaultPlugins[c.projectVersion]
1✔
403
                return
1✔
404
        }
1✔
405

406
        // Else try to use the default plugins for the default project version
407
        if c.defaultProjectVersion.Validate() == nil {
13✔
408
                var found bool
2✔
409
                if c.pluginKeys, found = c.defaultPlugins[c.defaultProjectVersion]; found {
4✔
410
                        c.projectVersion = c.defaultProjectVersion
2✔
411
                        return
2✔
412
                }
2✔
413
        }
414

415
        // Else check if only default plugins for a project version were provided
416
        if len(c.defaultPlugins) == 1 {
16✔
417
                for projectVersion, defaultPlugins := range c.defaultPlugins {
14✔
418
                        c.pluginKeys = defaultPlugins
7✔
419
                        c.projectVersion = projectVersion
7✔
420
                        return
7✔
421
                }
7✔
422
        }
423
}
424

425
const unstablePluginMsg = " (plugin version is unstable, there may be an upgrade available: " +
426
        "https://kubebuilder.io/migration/plugin/plugins.html)"
427

428
// resolvePlugins selects from the available plugins those that match the project version and plugin keys provided.
429
func (c *CLI) resolvePlugins() error {
27✔
430
        knownProjectVersion := c.projectVersion.Validate() == nil
27✔
431

27✔
432
        for _, pluginKey := range c.pluginKeys {
55✔
433
                var extraErrMsg string
28✔
434

28✔
435
                plugins := make([]plugin.Plugin, 0, len(c.plugins))
28✔
436
                for _, p := range c.plugins {
295✔
437
                        plugins = append(plugins, p)
267✔
438
                }
267✔
439
                // We can omit the error because plugin keys have already been validated
440
                plugins, _ = plugin.FilterPluginsByKey(plugins, pluginKey)
28✔
441
                if knownProjectVersion {
47✔
442
                        plugins = plugin.FilterPluginsByProjectVersion(plugins, c.projectVersion)
19✔
443
                        extraErrMsg += fmt.Sprintf(" for project version %q", c.projectVersion)
19✔
444
                }
19✔
445

446
                // Plugins are often released as "unstable" (alpha/beta) versions, then upgraded to "stable".
447
                // This upgrade effectively removes a plugin, which is fine because unstable plugins are
448
                // under no support contract. However users should be notified _why_ their plugin cannot be found.
449
                if _, version := plugin.SplitKey(pluginKey); version != "" {
42✔
450
                        var ver plugin.Version
14✔
451
                        if err := ver.Parse(version); err != nil {
14✔
452
                                return fmt.Errorf("error parsing input plugin version from key %q: %w", pluginKey, err)
×
453
                        }
×
454
                        if !ver.IsStable() {
14✔
455
                                extraErrMsg += unstablePluginMsg
×
456
                        }
×
457
                }
458

459
                // Only 1 plugin can match
460
                switch len(plugins) {
28✔
461
                case 1:
19✔
462
                        c.resolvedPlugins = append(c.resolvedPlugins, plugins[0])
19✔
463
                case 0:
6✔
464
                        return fmt.Errorf("no plugin could be resolved with key %q%s", pluginKey, extraErrMsg)
6✔
465
                default:
3✔
466
                        return fmt.Errorf("ambiguous plugin %q%s", pluginKey, extraErrMsg)
3✔
467
                }
468
        }
469

470
        // Now we can try to resolve the project version if not known by this point
471
        if !knownProjectVersion && len(c.resolvedPlugins) > 0 {
22✔
472
                // Extract the common supported project versions
4✔
473
                supportedProjectVersions := plugin.CommonSupportedProjectVersions(c.resolvedPlugins...)
4✔
474

4✔
475
                // If there is only one common supported project version, resolve to it
4✔
476
        ProjectNumberVersionSwitch:
4✔
477
                switch len(supportedProjectVersions) {
4✔
478
                case 1:
1✔
479
                        c.projectVersion = supportedProjectVersions[0]
1✔
480
                case 0:
1✔
481
                        return fmt.Errorf("no project version supported by all the resolved plugins")
1✔
482
                default:
2✔
483
                        supportedProjectVersionStrings := make([]string, 0, len(supportedProjectVersions))
2✔
484
                        for _, supportedProjectVersion := range supportedProjectVersions {
6✔
485
                                // In case one of the multiple supported versions is the default one, choose that and exit the switch
4✔
486
                                if supportedProjectVersion.Compare(c.defaultProjectVersion) == 0 {
5✔
487
                                        c.projectVersion = c.defaultProjectVersion
1✔
488
                                        break ProjectNumberVersionSwitch
1✔
489
                                }
490
                                supportedProjectVersionStrings = append(supportedProjectVersionStrings,
3✔
491
                                        fmt.Sprintf("%q", supportedProjectVersion))
3✔
492
                        }
493
                        return fmt.Errorf("ambiguous project version, resolved plugins support the following project versions: %s",
1✔
494
                                strings.Join(supportedProjectVersionStrings, ", "))
1✔
495
                }
496
        }
497

498
        return nil
16✔
499
}
500

501
// addSubcommands returns a root command with a subcommand tree reflecting the
502
// current project's state.
503
func (c *CLI) addSubcommands() {
10✔
504
        // add the alpha command if it has any subcommands enabled
10✔
505
        c.addAlphaCmd()
10✔
506

10✔
507
        // kubebuilder completion
10✔
508
        // Only add completion if requested
10✔
509
        if c.completionCommand {
11✔
510
                c.cmd.AddCommand(c.newCompletionCmd())
1✔
511
        }
1✔
512

513
        // kubebuilder create
514
        createCmd := c.newCreateCmd()
10✔
515
        // kubebuilder create api
10✔
516
        createCmd.AddCommand(c.newCreateAPICmd())
10✔
517
        createCmd.AddCommand(c.newCreateWebhookCmd())
10✔
518
        if createCmd.HasSubCommands() {
20✔
519
                c.cmd.AddCommand(createCmd)
10✔
520
        }
10✔
521

522
        // kubebuilder edit
523
        c.cmd.AddCommand(c.newEditCmd())
10✔
524

10✔
525
        // kubebuilder init
10✔
526
        c.cmd.AddCommand(c.newInitCmd())
10✔
527

10✔
528
        // kubebuilder version
10✔
529
        // Only add version if a version string was provided
10✔
530
        if c.version != "" {
11✔
531
                c.cmd.AddCommand(c.newVersionCmd())
1✔
532
        }
1✔
533
}
534

535
// addExtraCommands adds the additional commands.
536
func (c *CLI) addExtraCommands() error {
9✔
537
        for _, cmd := range c.extraCommands {
11✔
538
                for _, subCmd := range c.cmd.Commands() {
10✔
539
                        if cmd.Name() == subCmd.Name() {
9✔
540
                                return fmt.Errorf("command %q already exists", cmd.Name())
1✔
541
                        }
1✔
542
                }
543
                c.cmd.AddCommand(cmd)
1✔
544
        }
545
        return nil
8✔
546
}
547

548
// printDeprecationWarnings prints the deprecation warnings of the resolved plugins.
549
func (c CLI) printDeprecationWarnings() {
7✔
550
        for _, p := range c.resolvedPlugins {
12✔
551
                if p != nil && p.(plugin.Deprecated) != nil && len(p.(plugin.Deprecated).DeprecationWarning()) > 0 {
6✔
552
                        _, _ = fmt.Fprintf(os.Stderr, noticeColor, fmt.Sprintf(deprecationFmt, p.(plugin.Deprecated).DeprecationWarning()))
1✔
553
                }
1✔
554
        }
555
}
556

557
// metadata returns CLI's metadata.
558
func (c CLI) metadata() plugin.CLIMetadata {
24✔
559
        return plugin.CLIMetadata{
24✔
560
                CommandName: c.commandName,
24✔
561
        }
24✔
562
}
24✔
563

564
// Run executes the CLI utility.
565
//
566
// If an error is found, command help and examples will be printed.
567
func (c CLI) Run() error {
1✔
568
        if err := c.cmd.Execute(); err != nil {
2✔
569
                return fmt.Errorf("error executing command: %w", err)
1✔
570
        }
1✔
571

572
        return nil
×
573
}
574

575
// Command returns the underlying root command.
576
func (c CLI) Command() *cobra.Command {
2✔
577
        return c.cmd
2✔
578
}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc