Skip to content

Commit

Permalink
Add CLI version subcommand and flag
Browse files Browse the repository at this point in the history
I was trying to help which River CLI I had installed and realized
there's no easy way to know this currently because it can't currently
reveal its version information in any built-in way.

Here, add a `river version` subcommand that prints the version of the Go
module it's distributed as, along with the version of Go used to build
it, making accessing version information easy.

I also added support for a `river --version` flag. There's no common
standard on whether version should be revealed by subcommand or flag,
and since `--version`'s (unlike `-v`) never going to be used for
anything else, we may as well be permissable in what we accept.

I also add a basic framework for a CLI "integration" test suite that
allows CLI commands to be tested from the level of the Cobra command,
which is useful for checking flags and such. It has the downside in that
there's no way to inject a test transaction into it, so it's not always
appropriate for use right now.
  • Loading branch information
brandur committed Aug 21, 2024
1 parent 6068e66 commit e79357e
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 36 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ jobs:
run: river validate --database-url $DATABASE_URL
shell: bash

- name: river version
run: river version
shell: bash

- name: river bench
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## Added

- A new `river migrate-list` command is available which lists available migrations and which version a target database is migrated to. [PR #534](https://github.com/riverqueue/river/pull/534).
- `river version` or `river --version` now prints River version information. [PR #537](https://github.com/riverqueue/river/pull/537).

## [0.11.4] - 2024-08-20

### Fixed
Expand Down
5 changes: 4 additions & 1 deletion cmd/river/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ func (p *DriverProcurer) ProcurePgxV5(pool *pgxpool.Pool) riverdriver.Driver[pgx
}

func main() {
cli := rivercli.NewCLI(&DriverProcurer{})
cli := rivercli.NewCLI(&rivercli.Config{
DriverProcurer: &DriverProcurer{},
Name: "River",
})

if err := cli.BaseCommandSet().Execute(); err != nil {
// Cobra will already print an error on problems like an unknown command
Expand Down
5 changes: 3 additions & 2 deletions cmd/river/rivercli/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ type RunCommandBundle struct {
DatabaseURL *string
DriverProcurer DriverProcurer
Logger *slog.Logger
OutStd io.Writer
}

// RunCommand bootstraps and runs a River CLI subcommand.
Expand All @@ -85,7 +86,7 @@ func RunCommand[TOpts CommandOpts](ctx context.Context, bundle *RunCommandBundle
commandBase := &CommandBase{
DriverProcurer: bundle.DriverProcurer,
Logger: bundle.Logger,
Out: os.Stdout,
Out: bundle.OutStd,
}

switch {
Expand Down Expand Up @@ -124,7 +125,7 @@ func RunCommand[TOpts CommandOpts](ctx context.Context, bundle *RunCommandBundle

ok, err := procureAndRun()
if err != nil {
fmt.Fprintf(os.Stderr, "failed: %s\n", err)
fmt.Fprintf(os.Stdout, "failed: %s\n", err)
}
if err != nil || !ok {
os.Exit(1)
Expand Down
117 changes: 97 additions & 20 deletions cmd/river/rivercli/river_cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"io"
"log/slog"
"os"
"runtime/debug"
"slices"
"strings"
"time"
Expand All @@ -23,8 +24,20 @@ import (

"github.com/riverqueue/river/riverdriver"
"github.com/riverqueue/river/rivermigrate"
"github.com/riverqueue/river/rivershared/util/valutil"
)

type Config struct {
// DriverProcurer provides a way of procuring drivers for various supported
// databases.
DriverProcurer DriverProcurer

// Name is the human-friendly named of the executable, used while showing
// version output. Usually this is just "River", but it could be "River
// Pro".
Name string
}

// DriverProcurer is an interface that provides a way of procuring drivers for
// various supported databases.
type DriverProcurer interface {
Expand All @@ -34,42 +47,33 @@ type DriverProcurer interface {
// CLI provides a common base of commands for the River CLI.
type CLI struct {
driverProcurer DriverProcurer
name string
out io.Writer
}

func NewCLI(driverProcurer DriverProcurer) *CLI {
func NewCLI(config *Config) *CLI {
return &CLI{
driverProcurer: driverProcurer,
driverProcurer: config.DriverProcurer,
name: config.Name,
out: os.Stdout,
}
}

// BaseCommandSet provides a base River CLI command set which may be further
// augmented with additional commands.
func (c *CLI) BaseCommandSet() *cobra.Command {
var rootOpts struct {
ctx := context.Background()

var globalOpts struct {
Debug bool
Verbose bool
}
rootCmd := &cobra.Command{
Use: "river",
Short: "Provides command line facilities for the River job queue",
Long: strings.TrimSpace(`
Provides command line facilities for the River job queue.
`),
Run: func(cmd *cobra.Command, args []string) {
_ = cmd.Usage()
},
}
rootCmd.PersistentFlags().BoolVar(&rootOpts.Debug, "debug", false, "output maximum logging verbosity (debug level)")
rootCmd.PersistentFlags().BoolVarP(&rootOpts.Verbose, "verbose", "v", false, "output additional logging verbosity (info level)")
rootCmd.MarkFlagsMutuallyExclusive("debug", "verbose")

ctx := context.Background()

makeLogger := func() *slog.Logger {
switch {
case rootOpts.Debug:
case globalOpts.Debug:
return slog.New(tint.NewHandler(os.Stdout, &tint.Options{Level: slog.LevelDebug}))
case rootOpts.Verbose:
case globalOpts.Verbose:
return slog.New(tint.NewHandler(os.Stdout, nil))
default:
return slog.New(tint.NewHandler(os.Stdout, &tint.Options{Level: slog.LevelWarn}))
Expand All @@ -82,9 +86,39 @@ Provides command line facilities for the River job queue.
DatabaseURL: databaseURL,
DriverProcurer: c.driverProcurer,
Logger: makeLogger(),
OutStd: c.out,
}
}

var rootCmd *cobra.Command
{
var rootOpts struct {
Version bool
}

rootCmd = &cobra.Command{
Use: "river",
Short: "Provides command line facilities for the River job queue",
Long: strings.TrimSpace(`
Provides command line facilities for the River job queue.
`),
Run: func(cmd *cobra.Command, args []string) {
if rootOpts.Version {
RunCommand(ctx, makeCommandBundle(nil), &version{}, &versionOpts{Name: c.name})
} else {
_ = cmd.Usage()
}
},
}
rootCmd.SetOut(c.out)

rootCmd.PersistentFlags().BoolVar(&globalOpts.Debug, "debug", false, "output maximum logging verbosity (debug level)")
rootCmd.PersistentFlags().BoolVarP(&globalOpts.Verbose, "verbose", "v", false, "output additional logging verbosity (info level)")
rootCmd.MarkFlagsMutuallyExclusive("debug", "verbose")

rootCmd.Flags().BoolVar(&rootOpts.Version, "version", false, "print version information")
}

mustMarkFlagRequired := func(cmd *cobra.Command, name string) {
// We just panic here because this will never happen outside of an error
// in development.
Expand Down Expand Up @@ -283,9 +317,27 @@ migrations that need to be run, but without running them.
rootCmd.AddCommand(cmd)
}

// version
{
cmd := &cobra.Command{
Use: "version",
Short: "Print version information",
Long: strings.TrimSpace(`
Print River and Go version information.
`),
Run: func(cmd *cobra.Command, args []string) {
RunCommand(ctx, makeCommandBundle(nil), &version{}, &versionOpts{Name: c.name})
},
}
rootCmd.AddCommand(cmd)
}

return rootCmd
}

// SetOut sets standard output. Should be called before BaseCommandSet.
func (c *CLI) SetOut(out io.Writer) { c.out = out }

type benchOpts struct {
DatabaseURL string
Debug bool
Expand Down Expand Up @@ -555,3 +607,28 @@ func (c *validate) Run(ctx context.Context, opts *validateOpts) (bool, error) {

return res.OK, nil
}

type versionOpts struct {
Name string
}

func (o *versionOpts) Validate() error {
if o.Name == "" {
return errors.New("name should be set")
}

return nil
}

type version struct {
CommandBase
}

func (c *version) Run(ctx context.Context, opts *versionOpts) (bool, error) {
buildInfo, _ := debug.ReadBuildInfo()

fmt.Fprintf(c.Out, "%s version %s\n", opts.Name, valutil.ValOrDefault(buildInfo.Main.Version, "(unknown)"))
fmt.Fprintf(c.Out, "Built with %s\n", buildInfo.GoVersion)

return true, nil
}
Loading

0 comments on commit e79357e

Please sign in to comment.