Skip to content

feat(cli): add --output={text,json} to version cmd #7010

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 5, 2023
Next Next commit
feat(cli): add --json output to version cmd
  • Loading branch information
johnstcn committed Apr 5, 2023
commit af0508e511032a4c173154676cb68a87b9037b2f
30 changes: 0 additions & 30 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,36 +370,6 @@ func LoggerFromContext(ctx context.Context) (slog.Logger, bool) {
return l, ok
}

// version prints the coder version
func (*RootCmd) version() *clibase.Cmd {
return &clibase.Cmd{
Use: "version",
Short: "Show coder version",
Handler: func(inv *clibase.Invocation) error {
var str strings.Builder
_, _ = str.WriteString("Coder ")
if buildinfo.IsAGPL() {
_, _ = str.WriteString("(AGPL) ")
}
_, _ = str.WriteString(buildinfo.Version())
buildTime, valid := buildinfo.Time()
if valid {
_, _ = str.WriteString(" " + buildTime.Format(time.UnixDate))
}
_, _ = str.WriteString("\r\n" + buildinfo.ExternalURL() + "\r\n\r\n")

if buildinfo.IsSlim() {
_, _ = str.WriteString(fmt.Sprintf("Slim build of Coder, does not support the %s subcommand.\n", cliui.Styles.Code.Render("server")))
} else {
_, _ = str.WriteString(fmt.Sprintf("Full build of Coder, supports the %s subcommand.\n", cliui.Styles.Code.Render("server")))
}

_, _ = fmt.Fprint(inv.Stdout, str.String())
return nil
},
}
}

func isTest() bool {
return flag.Lookup("test.v") != nil
}
Expand Down
79 changes: 79 additions & 0 deletions cli/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package cli

import (
"encoding/json"
"fmt"
"strings"
"time"

"github.com/coder/coder/buildinfo"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/cli/cliui"
)

// version prints the coder version
func (*RootCmd) version() *clibase.Cmd {
handleHuman := func(inv *clibase.Invocation) error {
var str strings.Builder
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: do you need a string builder if there is no way to break execution in the middle of processing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, but this is just the previous code I copy-pasta'ed.

_, _ = str.WriteString("Coder ")
if buildinfo.IsAGPL() {
_, _ = str.WriteString("(AGPL) ")
}
_, _ = str.WriteString(buildinfo.Version())
buildTime, valid := buildinfo.Time()
if valid {
_, _ = str.WriteString(" " + buildTime.Format(time.UnixDate))
}
_, _ = str.WriteString("\r\n" + buildinfo.ExternalURL() + "\r\n\r\n")

if buildinfo.IsSlim() {
_, _ = str.WriteString(fmt.Sprintf("Slim build of Coder, does not support the %s subcommand.\n", cliui.Styles.Code.Render("server")))
} else {
_, _ = str.WriteString(fmt.Sprintf("Full build of Coder, supports the %s subcommand.\n", cliui.Styles.Code.Render("server")))
}

_, _ = fmt.Fprint(inv.Stdout, str.String())
return nil
}

handleJSON := func(inv *clibase.Invocation) error {
buildTime, _ := buildinfo.Time()
versionInfo := struct {
Version string `json:"version"`
BuildTime string `json:"build_time"`
ExternalURL string `json:"external_url"`
Slim bool `json:"slim"`
AGPL bool `json:"agpl"`
}{
Version: buildinfo.Version(),
BuildTime: buildTime.Format(time.UnixDate),
ExternalURL: buildinfo.ExternalURL(),
Slim: buildinfo.IsSlim(),
AGPL: buildinfo.IsAGPL(),
}

enc := json.NewEncoder(inv.Stdout)
enc.SetIndent("", " ")
return enc.Encode(versionInfo)
}

var outputJSON bool

return &clibase.Cmd{
Use: "version",
Short: "Show coder version",
Options: clibase.OptionSet{
{
Flag: "json",
Description: "Emit version information in machine-readable JSON format.",
Value: clibase.BoolOf(&outputJSON),
},
},
Handler: func(inv *clibase.Invocation) error {
if outputJSON {
return handleJSON(inv)
}
return handleHuman(inv)
},
}
}
67 changes: 67 additions & 0 deletions cli/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package cli_test

import (
"bytes"
"context"
"regexp"
"strings"
"testing"

"github.com/stretchr/testify/require"

"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/testutil"
)

func TestVersion(t *testing.T) {
t.Parallel()
ansiExpr := regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))")
clean := func(s string) string {
s = ansiExpr.ReplaceAllString(s, "")
s = strings.Replace(s, "\r\n", "\n", -1)
return s
}
expectedHuman := `Coder v0.0.0-devel
https://github.com/coder/coder

Full build of Coder, supports the server subcommand.
`
expectedJSON := `{
"version": "v0.0.0-devel",
"build_time": "Mon Jan 1 00:00:00 UTC 0001",
"external_url": "https://github.com/coder/coder",
"slim": false,
"agpl": false
}
`
for _, tt := range []struct {
Name string
Args []string
Expected string
}{
{
Name: "Defaults to human-readable output",
Args: []string{"version"},
Expected: expectedHuman,
},
{
Name: "JSON output",
Args: []string{"version", "--json"},
Expected: expectedJSON,
},
} {
tt := tt
t.Run(tt.Name, func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
t.Cleanup(cancel)
inv, _ := clitest.New(t, tt.Args...)
buf := new(bytes.Buffer)
inv.Stdout = buf
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
actual := clean(buf.String())
require.Equal(t, tt.Expected, actual)
})
}
}