From c02fbe504db5fe7409747c9206b5cf2aada0cf36 Mon Sep 17 00:00:00 2001 From: Simon KP Date: Fri, 13 Mar 2026 20:01:03 +1100 Subject: [PATCH 1/6] fix(ci): update golangci-lint config for v2 schema and regenerate docs - Move exclude-rules to linters.exclusions.rules (golangci-lint v2.11+) - Regenerate docs to include kh_update command - Add missing cobra/doc dependency for docs generation --- .golangci.yml | 10 +++++----- docs/kh.md | 1 + docs/kh_update.md | 43 +++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 ++ go.sum | 2 ++ 5 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 docs/kh_update.md diff --git a/.golangci.yml b/.golangci.yml index 7b1475e..7b49494 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -9,11 +9,11 @@ linters: - gosimple - ineffassign - revive + exclusions: + rules: + - path: "_test.go" + linters: + - errcheck formatters: enable: - gofmt -issues: - exclude-rules: - - path: "_test.go" - linters: - - errcheck diff --git a/docs/kh.md b/docs/kh.md index 000a93a..1b9283b 100644 --- a/docs/kh.md +++ b/docs/kh.md @@ -29,6 +29,7 @@ KeeperHub CLI * [kh serve](kh_serve.md) - Start a server * [kh tag](kh_tag.md) - Manage tags * [kh template](kh_template.md) - Manage workflow templates +* [kh update](kh_update.md) - Update kh to the latest version * [kh version](kh_version.md) - Show CLI version * [kh wallet](kh_wallet.md) - Manage wallets * [kh workflow](kh_workflow.md) - Manage workflows diff --git a/docs/kh_update.md b/docs/kh_update.md new file mode 100644 index 0000000..114a14d --- /dev/null +++ b/docs/kh_update.md @@ -0,0 +1,43 @@ +## kh update + +Update kh to the latest version + +### Synopsis + +Update kh to the latest version by downloading the newest release from GitHub. + +If kh was installed via Homebrew, this command will print the appropriate +brew command to use instead of replacing the binary directly. Homebrew manages +its own binary lifecycle and must be used to keep the installation consistent. + +``` +kh update [flags] +``` + +### Examples + +``` + # Check for and install the latest version + kh update +``` + +### Options + +``` + -h, --help help for update +``` + +### Options inherited from parent commands + +``` + -H, --host string KeeperHub host (default: app.keeperhub.io) + --jq string Filter JSON output with a jq expression + --json Output as JSON + --no-color Disable color output + -y, --yes Skip confirmation prompts +``` + +### SEE ALSO + +* [kh](kh.md) - KeeperHub CLI + diff --git a/go.mod b/go.mod index 3a44412..91fb393 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.3.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/danieljoos/wincred v1.2.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect @@ -39,6 +40,7 @@ require ( github.com/mattn/go-runewidth v0.0.19 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/segmentio/asm v1.1.3 // indirect github.com/segmentio/encoding v0.5.3 // indirect github.com/spf13/pflag v1.0.8 // indirect diff --git a/go.sum b/go.sum index b3a6d65..f36136d 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,7 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creativeprojects/go-selfupdate v1.5.2 h1:3KR3JLrq70oplb9yZzbmJ89qRP78D1AN/9u+l3k0LJ4= github.com/creativeprojects/go-selfupdate v1.5.2/go.mod h1:BCOuwIl1dRRCmPNRPH0amULeZqayhKyY2mH/h4va7Dk= @@ -75,6 +76,7 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= From 783ae204b435fd4122fb656912b0ef042971f043 Mon Sep 17 00:00:00 2001 From: Simon KP Date: Fri, 13 Mar 2026 20:05:36 +1100 Subject: [PATCH 2/6] fix(ci): remove defunct gosimple linter and fix docs-check working dir - gosimple merged into staticcheck in golangci-lint v2, no longer valid - docs-check: cd docs broke git diff path; use go generate ./docs/ instead --- .github/workflows/ci.yml | 2 +- .golangci.yml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 747b712..c17095e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: go-version-file: go.mod - name: Check docs are up to date run: | - cd docs && go generate + go generate ./docs/ git diff --exit-code docs/ integration-test: runs-on: ubuntu-latest diff --git a/.golangci.yml b/.golangci.yml index 7b49494..9830a66 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -6,7 +6,6 @@ linters: - govet - staticcheck - unused - - gosimple - ineffassign - revive exclusions: From b4f8bd31168b2f08d86ea19ccc89ee2687b52bba Mon Sep 17 00:00:00 2001 From: Simon KP Date: Fri, 13 Mar 2026 20:09:37 +1100 Subject: [PATCH 3/6] fix(ci): exclude fmt.Fprint* and Body.Close from errcheck These return values are universally ignored in CLI code -- writing to stdout/stderr and closing HTTP response bodies. --- .golangci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index 9830a66..3c352e3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,6 +8,14 @@ linters: - unused - ineffassign - revive + settings: + errcheck: + exclude-functions: + - fmt.Fprintln + - fmt.Fprintf + - fmt.Fprint + - (io.Closer).Close + - (*net/http.Response).Body.Close exclusions: rules: - path: "_test.go" From 44b25557f46b3781e2e9f37b280f6107bf6878cf Mon Sep 17 00:00:00 2001 From: Simon KP Date: Fri, 13 Mar 2026 20:11:50 +1100 Subject: [PATCH 4/6] fix(ci): use default:none and enable only core linters Previous config used default:standard which pulled in revive and gofmt checks the codebase was never written against. Start with core linters (errcheck, govet, staticcheck, unused, ineffassign) and add more later. --- .golangci.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 3c352e3..4f5c792 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,13 +1,12 @@ version: "2" linters: - default: standard + default: none enable: - errcheck - govet - staticcheck - unused - ineffassign - - revive settings: errcheck: exclude-functions: @@ -15,12 +14,8 @@ linters: - fmt.Fprintf - fmt.Fprint - (io.Closer).Close - - (*net/http.Response).Body.Close exclusions: rules: - path: "_test.go" linters: - errcheck -formatters: - enable: - - gofmt From ee08b009d3338bcdd3cb475493f439af0296e0f0 Mon Sep 17 00:00:00 2001 From: Simon KP Date: Fri, 13 Mar 2026 20:14:50 +1100 Subject: [PATCH 5/6] fix(ci): resolve all lint errors across codebase - Remove unused validConfigKeys var - Fix ineffectual assignment in pause_test.go - Lowercase error strings per Go convention (ST1005) - Remove trailing punctuation from error strings - Replace deprecated cobra.ExactValidArgs with MatchAll --- cmd/auth/status.go | 2 +- cmd/completion/completion.go | 2 +- cmd/config/get.go | 4 ++-- cmd/config/set.go | 4 +--- cmd/workflow/go_live.go | 2 +- cmd/workflow/pause_test.go | 1 + internal/config/config.go | 2 +- 7 files changed, 8 insertions(+), 9 deletions(-) diff --git a/cmd/auth/status.go b/cmd/auth/status.go index 4e27eec..8aedfae 100644 --- a/cmd/auth/status.go +++ b/cmd/auth/status.go @@ -61,7 +61,7 @@ func NewStatusCmd(f *cmdutil.Factory) *cobra.Command { } if resolved.Method == internalauth.AuthMethodNone { - return errors.New("Not authenticated. Run 'kh auth login' to sign in.") + return errors.New("not authenticated, run 'kh auth login' to sign in") } info, err := FetchTokenInfoFunc(host, resolved.Token) diff --git a/cmd/completion/completion.go b/cmd/completion/completion.go index 0f1a40a..2f4a8bd 100644 --- a/cmd/completion/completion.go +++ b/cmd/completion/completion.go @@ -8,7 +8,7 @@ func NewCompletionCmd() *cobra.Command { cmd := &cobra.Command{ Use: "completion ", Short: "Generate shell completion scripts", - Args: cobra.ExactValidArgs(1), + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, Long: `Generate shell completion scripts for kh. Source the output in your shell profile to enable tab completion for all kh commands and flags. diff --git a/cmd/config/get.go b/cmd/config/get.go index 383aa72..8a2f73d 100644 --- a/cmd/config/get.go +++ b/cmd/config/get.go @@ -27,11 +27,11 @@ func NewGetCmd(f *cmdutil.Factory) *cobra.Command { case "default_host": value = cfg.DefaultHost default: - return fmt.Errorf("X Config key not set: %s", key) + return fmt.Errorf("config key not set: %s", key) } if value == "" { - return fmt.Errorf("X Config key not set: %s", key) + return fmt.Errorf("config key not set: %s", key) } fmt.Fprintln(f.IOStreams.Out, value) diff --git a/cmd/config/set.go b/cmd/config/set.go index 6d1a2bc..9c43cc1 100644 --- a/cmd/config/set.go +++ b/cmd/config/set.go @@ -8,8 +8,6 @@ import ( "github.com/spf13/cobra" ) -var validConfigKeys = []string{"default_host"} - func NewSetCmd(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "set ", @@ -38,7 +36,7 @@ See also: kh config list, kh config get`, case "default_host": cfg.DefaultHost = value default: - return fmt.Errorf("X Unknown config key: %s\nHint: use 'kh config list' to see available keys", key) + return fmt.Errorf("unknown config key: %s\nhint: use 'kh config list' to see available keys", key) } if err := internalconfig.WriteConfig(cfg); err != nil { diff --git a/cmd/workflow/go_live.go b/cmd/workflow/go_live.go index 3af799e..b79351a 100644 --- a/cmd/workflow/go_live.go +++ b/cmd/workflow/go_live.go @@ -81,7 +81,7 @@ func NewGoLiveCmd(f *cmdutil.Factory) *cobra.Command { defer resp.Body.Close() if resp.StatusCode == http.StatusUnauthorized { - return fmt.Errorf("HTTP 401: Unauthorized. This command requires interactive login. Run 'kh auth login' first.") + return fmt.Errorf("HTTP 401: unauthorized, this command requires interactive login, run 'kh auth login' first") } if resp.StatusCode != http.StatusOK { return khhttp.NewAPIError(resp) diff --git a/cmd/workflow/pause_test.go b/cmd/workflow/pause_test.go index 8297aa7..17a8901 100644 --- a/cmd/workflow/pause_test.go +++ b/cmd/workflow/pause_test.go @@ -168,6 +168,7 @@ func TestPauseHasYesFlag(t *testing.T) { if flag == nil { flag = pauseCmd.InheritedFlags().Lookup("yes") } + _ = flag // The test just verifies pause command exists and is wired correctly assert.NotNil(t, pauseCmd) } diff --git a/internal/config/config.go b/internal/config/config.go index 9b70225..826065c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -37,7 +37,7 @@ func ReadConfig() (Config, error) { var cfg Config if err := yaml.Unmarshal(data, &cfg); err != nil { - return Config{}, fmt.Errorf("Config file is invalid. Run 'kh auth login' to reset it.") + return Config{}, fmt.Errorf("config file is invalid, run 'kh auth login' to reset it") } return cfg, nil From 986f7d0d0b124822f135836f3984be440daf9a45 Mon Sep 17 00:00:00 2001 From: Simon KP Date: Fri, 13 Mar 2026 20:17:09 +1100 Subject: [PATCH 6/6] fix(ci): fix remaining ST1005 errors and update test assertion --- internal/auth/device.go | 4 ++-- internal/config/config_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/auth/device.go b/internal/auth/device.go index ba75b61..03bb1ab 100644 --- a/internal/auth/device.go +++ b/internal/auth/device.go @@ -112,9 +112,9 @@ func pollDeviceToken(ctx context.Context, host, deviceCode string, interval time case "slow_down": interval += 5 * time.Second case "expired_token": - return "", errors.New("Device code expired. Run 'kh auth login --no-browser' again.") + return "", errors.New("device code expired, run 'kh auth login --no-browser' again") case "access_denied": - return "", errors.New("Authentication denied.") + return "", errors.New("authentication denied") default: return "", fmt.Errorf("unexpected device token error: %s", tokenResp.Error) } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 3a47516..39cee41 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -73,7 +73,7 @@ func TestReadConfigInvalidYAML(t *testing.T) { if err == nil { t.Fatal("expected error for invalid YAML, got nil") } - if err.Error() != "Config file is invalid. Run 'kh auth login' to reset it." { + if err.Error() != "config file is invalid, run 'kh auth login' to reset it" { t.Errorf("unexpected error message: %q", err.Error()) } }