Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ and fix it with:
echo 'export PATH="$(go env GOPATH)/bin:$PATH"' >> ~/.zshrc # or ~/.bashrc
source ~/.zshrc

**Upgrading:** re-run the install script, download the new release and
replace the binary, or run `go install github.com/flexprice/cli/cmd/flexprice@latest`
again. Whichever you pick, your config and stored keys are untouched.
**Upgrading:** the CLI checks GitHub once a day and prints a notice when a
newer release exists. Run `flexprice update` to replace the binary in place
(`--check` only reports). Set `FLEXPRICE_NO_UPDATE=1` to silence the notice.
On Windows, download the release by hand or re-run `go install`. Your config
and stored keys are untouched either way.

## Quickstart

Expand Down
1 change: 1 addition & 0 deletions docs/flexprice.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ flexprice [flags]
* [flexprice tax-associations](flexprice_tax-associations.md) - Which tax rates apply to which entities
* [flexprice tax-rates](flexprice_tax-rates.md) - Tax rates available to apply
* [flexprice tenants](flexprice_tenants.md) - Your top-level account
* [flexprice update](flexprice_update.md) - Update the CLI to the latest release
* [flexprice users](flexprice_users.md) - People with access to your tenant
* [flexprice version](flexprice_version.md) - Print the CLI version and embedded spec build
* [flexprice wallets](flexprice_wallets.md) - Prepaid credit balances held by a customer
Expand Down
41 changes: 41 additions & 0 deletions docs/flexprice_update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## flexprice update

Update the CLI to the latest release

### Synopsis

Check GitHub for a newer release and, if there is one, run the install script (https://cli.flexprice.io) to replace this binary in place.

Every other command checks once a day and prints a notice when a newer release exists; set FLEXPRICE_NO_UPDATE=1 to turn that off.

```
flexprice update [flags]
```

### Options

```
--check report whether a newer release exists without installing it
-h, --help help for update
```

### Options inherited from parent commands

```
--all page through every record (prints the last page; use --output json with --limit for bulk export)
--api-key string API key (CI use; prefer flexprice login)
--base-url string override the API base URL
--columns strings columns to show in table output
--debug dump requests and responses, secrets redacted
--limit int maximum records to return (default 20)
--no-color disable coloured output
--no-input never prompt; fail instead of asking
--output string output format: table, json, yaml (default "table")
-p, --profile string profile to use for this command
--quiet suppress progress output
--region string region key, e.g. us or in
```

### SEE ALSO

* [flexprice](flexprice.md) - Flexprice CLI — usage-based billing from your terminal
1 change: 1 addition & 0 deletions internal/cmd/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ hand-written (`login`, `whoami`, `open`, ...) and spec-dispatched
| `config.go` | `config list`/`config use` |
| `init.go` | `init` — guided first run |
| `misc.go` | `open`, `version` |
| `update.go` | `update`, `wantsUpdateNotice`, `printUpdateNotice` |

## Startup wiring order

Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ var resourceGroups = map[string]resourceEntry{
var builtinGroups = map[string]string{
"init": groupSetup, "login": groupSetup, "logout": groupSetup,
"whoami": groupSetup, "env": groupSetup, "config": groupSetup,
"open": groupSetup, "version": groupSetup,
"open": groupSetup, "version": groupSetup, "update": groupSetup,

"get": groupAdvanced, "post": groupAdvanced, "delete": groupAdvanced,
"resources": groupAdvanced,
Expand Down
6 changes: 6 additions & 0 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/flexprice/cli/internal/spec"
"github.com/flexprice/cli/internal/style"
"github.com/flexprice/cli/internal/ui"
"github.com/flexprice/cli/internal/update"
)

// Created per root: pflag writes flag defaults into the bound pointer at
Expand Down Expand Up @@ -53,6 +54,7 @@ func NewRootCommand(version string) *cobra.Command {
}

bindGlobals(root.PersistentFlags(), g)
checker := update.NewChecker()

// Flags are not populated until Execute() parses them, so this hook is the
// first point where g's fields are real.
Expand All @@ -61,6 +63,9 @@ func NewRootCommand(version string) *cobra.Command {
style.Disable()
}
g.UI = ui.FromEnv(g.Quiet, g.NoInput, !g.NoColor)
if wantsUpdateNotice(cmd, g, version, term.IsTerminal(int(os.Stderr.Fd()))) {
printUpdateNotice(cmd.Context(), g, version, checker)
}
return nil
}

Expand All @@ -86,6 +91,7 @@ func NewRootCommand(version string) *cobra.Command {
newConfigCommand(g),
newOpenCommand(g, version),
newVersionCommand(g, version),
newUpdateCommand(g, version, checker, installScriptURL),
)

// Must run before any command carrying a GroupID reaches Execute: cobra
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/testdata/root_help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Setup
login Store credentials for a region and environment
logout Remove a stored profile and its key
open Open Flexprice in your browser
update Update the CLI to the latest release
version Print the CLI version and embedded spec build
whoami Show the active profile, environment and key backend

Expand Down
141 changes: 141 additions & 0 deletions internal/cmd/update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package cmd

import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"

"github.com/spf13/cobra"

"github.com/flexprice/cli/internal/update"
)

const installScriptURL = "https://cli.flexprice.io"

var noUpdateNotice = map[string]bool{
"version": true,
"update": true,
"help": true,
"completion": true,
cobra.ShellCompRequestCmd: true,
cobra.ShellCompNoDescRequestCmd: true,
}

func wantsUpdateNotice(cmd *cobra.Command, g *Globals, version string, stderrTTY bool) bool {
if !stderrTTY || g.Quiet || version == "dev" {
return false
}
if os.Getenv("FLEXPRICE_NO_UPDATE") != "" {
return false
}
for c := cmd; c != nil; c = c.Parent() {
if noUpdateNotice[c.Name()] {
return false
}
}
return true
}

func printUpdateNotice(ctx context.Context, g *Globals, version string, c *update.Checker) {
if ctx == nil {
ctx = context.Background()
}
latest, err := c.Latest(ctx)
if err != nil && g.Debug {
g.UI.Info("update check skipped: %v", err)
}
if !update.IsNewer(version, latest) {
return
}
g.UI.Info("A new version of flexprice is available: %s → %s", withV(version), latest)
g.UI.Info("Run `flexprice update` to upgrade.")
}

// Builds stamp "1.0.1"; release tags read "v1.0.1". Shown the tag way.
func withV(v string) string {
if v == "" || v[0] == 'v' {
return v
}
return "v" + v
}

func newUpdateCommand(g *Globals, version string, c *update.Checker, scriptURL string) *cobra.Command {
var check bool
cmd := &cobra.Command{
Use: "update",
Short: "Update the CLI to the latest release",
Long: "Check GitHub for a newer release and, if there is one, run the install " +
"script (" + installScriptURL + ") to replace this binary in place.\n\n" +
"Every other command checks once a day and prints a notice when a newer " +
"release exists; set FLEXPRICE_NO_UPDATE=1 to turn that off.",
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
if ctx == nil {
ctx = context.Background()
}
latest, err := c.Fetch(ctx)
if err != nil {
return err
}
if !update.IsNewer(version, latest) {
g.UI.Data("flexprice %s is up to date (latest release: %s)", withV(version), latest)
return nil
}
if check {
g.UI.Data("flexprice %s is available (installed: %s)", latest, withV(version))
g.UI.Info("Run `flexprice update` to upgrade.")
return nil
}
if runtime.GOOS == "windows" {
return fmt.Errorf("the install script does not support Windows; download %s from %s/latest", latest, update.ReleasesURL)
}
g.UI.Info("Updating flexprice %s to %s...", withV(version), latest)
return runInstallScript(ctx, scriptURL, latest, cmd.OutOrStdout(), cmd.ErrOrStderr())
},
}
cmd.Flags().BoolVar(&check, "check", false, "report whether a newer release exists without installing it")
return cmd
}

func runInstallScript(ctx context.Context, scriptURL, version string, stdout, stderr io.Writer) error {
exe, err := os.Executable()
if err != nil {
return err
}
if resolved, err := filepath.EvalSymlinks(exe); err == nil {
exe = resolved
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, scriptURL, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("download install script: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("download install script: unexpected %s from %s", resp.Status, scriptURL)
}
script, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return fmt.Errorf("download install script: %w", err)
}

sh := exec.CommandContext(ctx, "sh", "-s")
sh.Stdin = bytes.NewReader(script)
sh.Stdout = stdout
sh.Stderr = stderr
sh.Env = append(os.Environ(),
"FLEXPRICE_VERSION="+version,
"FLEXPRICE_INSTALL_DIR="+filepath.Dir(exe),
)
return sh.Run()
}
Loading
Loading