Repository: Shopify/themekit (go)

Issue:
Live Theme Backups using themeid: live
**Is your feature request related to a problem? Please describe.**
In the latest version of themekit the 'live' theme id is no longer available, we use this for getting live backups for deployments.

**Describe the solution you'd like**
Allow a theme id of 'live' to be set which targets the live theme for making daily backups.

**Describe alternatives you've considered**
Although it's possible to add a theme_id to a continuous deployment process, using the "live" theme id allows us to target the live theme and download backups. 


Pull request:
Adding a Live flags to allow easy access to downloading live theme
fixes #785 

This adds a `--live` flag to `get` `download` and `config` so that the live theme is easily access from commands that only pull data. It prefills the theme ID for those commands.

### Warn Checklist
- [ ] This changes the interface and requires a Major/Minor version change.
- [x] I have :tophat:'d these changes by using the commands I changed by hand.
- [ ] I have added a dependancy to the project.
- [x] I have considered any potential impact on [node-themekit](https://github.com/Shopify/node-themekit)


Patch:
diff --git a/changelog.txt b/changelog.txt
index cf97a96..c7ab241 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -3,6 +3,8 @@ v1.1.3
 - Added an error summary (#826)
 - Added error count to action summary (#826)
 - Set status error codes when errors occur for better scriptability (#826)
+- Added --live flag to get, download, configure to automatically set
+configuration to the published theme
 
 v1.1.2 (Sep 29, 2020)
 =====================
diff --git a/cmd/configure.go b/cmd/configure.go
index 549113c..5ecec9f 100644
--- a/cmd/configure.go
+++ b/cmd/configure.go
@@ -1,6 +1,8 @@
 package cmd
 
 import (
+	"strconv"
+
 	"github.com/spf13/cobra"
 
 	"github.com/Shopify/themekit/src/cmdutil"
@@ -15,6 +17,15 @@ var configureCmd = &cobra.Command{
  For more documentation please see https://shopify.github.io/themekit/commands/#configure
  `,
 	RunE: func(cmd *cobra.Command, args []string) error {
+		// get should not care about the live theme
+		flags.AllowLive = true
+		if flags.Live {
+			theme, err := getLiveTheme(flags, args)
+			if err != nil {
+				return err
+			}
+			flags.ThemeID = strconv.Itoa(int(theme.ID))
+		}
 		return cmdutil.ForDefaultClient(flags, args, createConfig)
 	},
 }
diff --git a/cmd/download.go b/cmd/download.go
index 992f69c..5d5d708 100644
--- a/cmd/download.go
+++ b/cmd/download.go
@@ -3,6 +3,7 @@ package cmd
 import (
 	"fmt"
 	"path/filepath"
+	"strconv"
 	"sync"
 
 	"github.com/spf13/cobra"
@@ -24,6 +25,13 @@ var downloadCmd = &cobra.Command{
 	RunE: func(cmd *cobra.Command, args []string) error {
 		// download should not care about the live theme
 		flags.AllowLive = true
+		if flags.Live {
+			theme, err := getLiveTheme(flags, args)
+			if err != nil {
+				return err
+			}
+			flags.ThemeID = strconv.Itoa(int(theme.ID))
+		}
 		return cmdutil.ForEachClient(flags, args, download)
 	},
 }
diff --git a/cmd/get.go b/cmd/get.go
index 6ea5d20..4641d84 100644
--- a/cmd/get.go
+++ b/cmd/get.go
@@ -3,11 +3,14 @@ package cmd
 import (
 	"bytes"
 	"errors"
+	"fmt"
+	"strconv"
 	"text/template"
 
 	"github.com/spf13/cobra"
 
 	"github.com/Shopify/themekit/src/cmdutil"
+	"github.com/Shopify/themekit/src/shopify"
 )
 
 var (
@@ -28,32 +31,79 @@ var getCmd = &cobra.Command{
  For more documentation please see https://shopify.github.io/themekit/commands/#get
  `,
 	RunE: func(cmd *cobra.Command, args []string) error {
-		// This is a hack to get around theme ID validation for the list operation which doesnt need it
+		// get should not care about the live theme
+		flags.AllowLive = true
 		if flags.List {
-			flags.ThemeID = "1337"
+			return listThemes(flags, args)
+		} else if flags.Live {
+			theme, err := getLiveTheme(flags, args)
+			if err != nil {
+				return err
+			}
+			flags.ThemeID = strconv.Itoa(int(theme.ID))
 		}
 		return cmdutil.ForDefaultClient(flags, args, getTheme)
 	},
 }
 
 func getTheme(ctx *cmdutil.Ctx) error {
-	if ctx.Flags.List {
-		themes, err := ctx.Client.Themes()
-		if err != nil {
-			return err
-		} else if len(themes) == 0 {
-			return errNoThemes
-		}
+	if err := createConfig(ctx); err != nil {
+		return err
+	}
+	return download(ctx)
+}
 
+func listThemes(flags cmdutil.Flags, args []string) error {
+	themes, err := getDefaultThemes(flags, args)
+	if err != nil {
+		return err
+	}
+	return cmdutil.ForDefaultClient(flags, args, func(ctx *cmdutil.Ctx) error {
 		var tpl bytes.Buffer
 		availableThemes.Execute(&tpl, themes)
 		ctx.Log.Println(tpl.String())
 		return nil
-	}
+	})
+}
 
-	if err := createConfig(ctx); err != nil {
-		return err
+func getLiveTheme(flags cmdutil.Flags, args []string) (shopify.Theme, error) {
+	themes, err := getDefaultThemes(flags, args)
+	if err != nil {
+		return shopify.Theme{}, err
 	}
+	for _, theme := range themes {
+		if theme.Role == "main" {
+			return theme, nil
+		}
+	}
+	return shopify.Theme{}, fmt.Errorf("No live theme found")
+}
 
-	return download(ctx)
+func getDefaultThemes(flags cmdutil.Flags, args []string) ([]shopify.Theme, error) {
+	// This is a hack to get around theme ID validation for the list operation which doesnt need it
+	flags.ThemeID = "1337"
+	var err error
+	var themes []shopify.Theme
+	return themes, cmdutil.ForDefaultClient(flags, args, func(ctx *cmdutil.Ctx) error {
+		if themes, err = ctx.Client.Themes(); err != nil {
+			return err
+		} else if len(themes) == 0 {
+			return errNoThemes
+		}
+		return nil
+	})
+}
+
+func withThemes(flags cmdutil.Flags, args []string, fn func(ctx *cmdutil.Ctx, themes []shopify.Theme) error) error {
+	// This is a hack to get around theme ID validation for the list operation which doesnt need it
+	flags.ThemeID = "1337"
+	return cmdutil.ForDefaultClient(flags, args, func(ctx *cmdutil.Ctx) error {
+		themes, err := ctx.Client.Themes()
+		if err != nil {
+			return err
+		} else if len(themes) == 0 {
+			return errNoThemes
+		}
+		return fn(ctx, themes)
+	})
 }
diff --git a/cmd/theme.go b/cmd/theme.go
index 64ddfb7..a53fad3 100644
--- a/cmd/theme.go
+++ b/cmd/theme.go
@@ -103,17 +103,21 @@ func init() {
 	getCmd.Flags().BoolVarP(&flags.List, "list", "l", false, "list available themes.")
 	deployCmd.Flags().BoolVarP(&flags.NoDelete, "nodelete", "n", false, "do not delete files on shopify during deploy.")
 
+	getCmd.Flags().BoolVar(&flags.Live, "live", false, "will allow themekit to autofill the theme ID as the currently published theme ID")
+	downloadCmd.Flags().BoolVar(&flags.Live, "live", false, "will allow themekit to autofill the theme ID as the currently published theme ID")
+	configureCmd.Flags().BoolVar(&flags.Live, "live", false, "will allow themekit to autofill the theme ID as the currently published theme ID")
+
 	ThemeCmd.AddCommand(
-		publishCmd,
-		openCmd,
-		versionCmd,
-		newCmd,
 		configureCmd,
+		deployCmd,
 		downloadCmd,
+		getCmd,
+		newCmd,
+		openCmd,
+		publishCmd,
 		removeCmd,
 		updateCmd,
+		versionCmd,
 		watchCmd,
-		getCmd,
-		deployCmd,
 	)
 }
diff --git a/docs/commands/index.md b/docs/commands/index.md
index ee727d6..7eaf3cb 100644
--- a/docs/commands/index.md
+++ b/docs/commands/index.md
@@ -50,6 +50,9 @@ development:
 |`-s`|`--store   `| Your store's domain for changes to take effect
 |`-t`|`--themeid `| The ID of the theme that you want changes to take effect
 
+|**Optional Flags**||
+|    |`--live`| Will set theme id to the published theme
+
 ## Deploy
 Deploy will completely replace what is on Shopify with what is in your current
 project directory. This means that any files that are on Shopify but are not on
@@ -79,6 +82,9 @@ theme download templates/404.liquid templates/article.liquid
 
 Similar to the `deploy` command, Theme Kit will skip downloading any unchanged files.
 
+|**Optional Flags**||
+|    |`--live`| Will set theme id to the published theme
+
 ## Get
 Get can be used to setup your theme on your local machine. It will both create
 a config file and download the theme you request. If you have existing
@@ -111,6 +117,9 @@ development:
 |`-s`|`--store   `| Your store's domain for changes to take effect
 |`-t`|`--themeid `| The ID of the theme that you want changes to take effect.
 
+|**Optional Flags**||
+|    |`--live`| Will set theme id to the published theme
+
 ## New
 
 If you are starting a new theme and want to have some sane defaults, you can use
diff --git a/src/cmdutil/util.go b/src/cmdutil/util.go
index 84c1a08..4ff7e55 100644
--- a/src/cmdutil/util.go
+++ b/src/cmdutil/util.go
@@ -55,6 +55,7 @@ type Flags struct {
 	List                  bool
 	NoDelete              bool
 	AllowLive             bool
+	Live                  bool
 }
 
 // Ctx is a specific context that a command will run in
@@ -75,7 +76,7 @@ type Ctx struct {
 
 type clientFact func(*env.Env) (shopifyClient, error)
 
-func createCtx(newClient clientFact, conf env.Conf, e *env.Env, flags Flags, args []string, progress *mpb.Progress, setTheme bool) (*Ctx, error) {
+func createCtx(newClient clientFact, conf env.Conf, e *env.Env, flags Flags, args []string, progress *mpb.Progress) (*Ctx, error) {
 	if e.Proxy != "" {
 		colors.ColorStdOut.Printf(
 			"[%s] Proxy URL detected from Configuration [%s] SSL Certificate Validation will be disabled!",
@@ -111,25 +112,23 @@ func createCtx(newClient clientFact, conf env.Conf, e *env.Env, flags Flags, arg
 		return &Ctx{}, err
 	}
 
-	if setTheme {
-		for _, theme := range themes {
-			if theme.Role == "main" {
-				if fmt.Sprintf("%v", theme.ID) == e.ThemeID && flags.AllowLive {
-					colors.ColorStdOut.Printf(
-						"[%s] Warning, this is the live theme on %s.",
-						colors.Yellow(e.Name),
-						colors.Yellow(shop.Name),
-					)
-				} else if fmt.Sprintf("%v", theme.ID) == e.ThemeID && !flags.AllowLive {
-					colors.ColorStdOut.Printf(
-						"[%s] This is the live theme on %s. If you wish to make changes to it, then you will have to pass the --allow-live flag",
-						colors.Red(e.Name),
-						colors.Yellow(shop.Name),
-					)
-					return &Ctx{}, ErrLiveTheme
-				}
-				break
+	for _, theme := range themes {
+		if theme.Role == "main" {
+			if fmt.Sprintf("%v", theme.ID) == e.ThemeID && flags.AllowLive {
+				colors.ColorStdOut.Printf(
+					"[%s] Warning, this is the live theme on %s.",
+					colors.Yellow(e.Name),
+					colors.Yellow(shop.Name),
+				)
+			} else if fmt.Sprintf("%v", theme.ID) == e.ThemeID && !flags.AllowLive {
+				colors.ColorStdOut.Printf(
+					"[%s] This is the live theme on %s. If you wish to make changes to it, then you will have to pass the --allow-live flag",
+					colors.Red(e.Name),
+					colors.Yellow(shop.Name),
+				)
+				return &Ctx{}, ErrLiveTheme
 			}
+			break
 		}
 	}
 
@@ -213,7 +212,7 @@ func generateContexts(newClient clientFact, progress *mpb.Progress, flags Flags,
 			}
 		}
 
-		ctx, err := createCtx(newClient, config, e, flags, args, progress, true)
+		ctx, err := createCtx(newClient, config, e, flags, args, progress)
 		if err != nil {
 			return ctxs, err
 		}
@@ -361,7 +360,7 @@ func forDefaultClient(newClient clientFact, flags Flags, args []string, handler
 		}
 	}
 
-	ctx, err := createCtx(newClient, config, e, flags, args, progressBarGroup, false)
+	ctx, err := createCtx(newClient, config, e, flags, args, progressBarGroup)
 	if err != nil {
 		return err
 	}


Test patch:
diff --git a/cmd/get_test.go b/cmd/get_test.go
index 2297d3b..3bea538 100644
--- a/cmd/get_test.go
+++ b/cmd/get_test.go
@@ -40,20 +40,4 @@ func TestGet(t *testing.T) {
 	conf.On("Save").Return(nil)
 	client.On("GetAllAssets").Return([]shopify.Asset{}, nil)
 	assert.Error(t, getTheme(ctx), "No files to download")
-
-	ctx, client, conf, _, _ = createTestCtx()
-	ctx.Flags.List = true
-	client.On("Themes").Return([]shopify.Theme{}, nil)
-	assert.EqualError(t, getTheme(ctx), errNoThemes.Error())
-
-	ctx, client, conf, _, _ = createTestCtx()
-	ctx.Flags.List = true
-	client.On("Themes").Return([]shopify.Theme{}, fmt.Errorf("server error"))
-	assert.EqualError(t, getTheme(ctx), "server error")
-
-	ctx, client, conf, stdOut, _ := createTestCtx()
-	ctx.Flags.List = true
-	client.On("Themes").Return([]shopify.Theme{{ID: 1234, Role: "main", Name: "test"}}, nil)
-	assert.Nil(t, getTheme(ctx))
-	assert.Contains(t, stdOut.String(), "[1234][live] test")
 }
diff --git a/src/cmdutil/util_test.go b/src/cmdutil/util_test.go
index fd27554..d4ab638 100644
--- a/src/cmdutil/util_test.go
+++ b/src/cmdutil/util_test.go
@@ -20,7 +20,7 @@ func TestCreateCtx(t *testing.T) {
 	client := new(mocks.ShopifyClient)
 	factory := func(*env.Env) (shopifyClient, error) { return client, nil }
 	client.On("GetShop").Return(shopify.Shop{}, shopify.ErrShopDomainNotFound)
-	_, err := createCtx(factory, env.Conf{}, e, Flags{}, []string{}, nil, false)
+	_, err := createCtx(factory, env.Conf{}, e, Flags{}, []string{}, nil)
 	if assert.NotNil(t, err) {
 		assert.Contains(t, err.Error(), "invalid domain")
 	}
@@ -29,7 +29,7 @@ func TestCreateCtx(t *testing.T) {
 	client = new(mocks.ShopifyClient)
 	factory = func(*env.Env) (shopifyClient, error) { return client, nil }
 	client.On("GetShop").Return(shopify.Shop{}, fmt.Errorf("This is bad"))
-	_, err = createCtx(factory, env.Conf{}, e, Flags{}, []string{}, nil, false)
+	_, err = createCtx(factory, env.Conf{}, e, Flags{}, []string{}, nil)
 	if assert.NotNil(t, err) {
 		assert.Contains(t, err.Error(), "This is bad")
 	}
@@ -38,7 +38,7 @@ func TestCreateCtx(t *testing.T) {
 	client.On("GetShop").Return(shopify.Shop{}, nil)
 	client.On("Themes").Return([]shopify.Theme{}, nil)
 	badFactory := func(*env.Env) (shopifyClient, error) { return nil, fmt.Errorf("no such file or directory") }
-	_, err = createCtx(badFactory, env.Conf{}, &env.Env{}, Flags{}, []string{}, nil, true)
+	_, err = createCtx(badFactory, env.Conf{}, &env.Env{}, Flags{}, []string{}, nil)
 	if assert.NotNil(t, err) {
 		assert.Contains(t, err.Error(), "no such file or directory")
 	}
@@ -46,7 +46,7 @@ func TestCreateCtx(t *testing.T) {
 	client = new(mocks.ShopifyClient)
 	client.On("GetShop").Return(shopify.Shop{}, nil)
 	client.On("Themes").Return([]shopify.Theme{}, fmt.Errorf("[API] Invalid API key or access token (unrecognized login or wrong password)"))
-	_, err = createCtx(factory, env.Conf{}, &env.Env{}, Flags{}, []string{}, nil, true)
+	_, err = createCtx(factory, env.Conf{}, &env.Env{}, Flags{}, []string{}, nil)
 	if assert.NotNil(t, err) {
 		assert.Contains(t, err.Error(), "[API] Invalid API key or access token (unrecognized login or wrong password)")
 	}
@@ -55,7 +55,7 @@ func TestCreateCtx(t *testing.T) {
 	client = new(mocks.ShopifyClient)
 	client.On("GetShop").Return(shopify.Shop{}, nil)
 	client.On("Themes").Return([]shopify.Theme{{ID: 65443, Role: "unpublished"}, {ID: 1234, Role: "main"}}, nil)
-	_, err = createCtx(factory, env.Conf{}, e, Flags{DisableIgnore: true}, []string{}, nil, true)
+	_, err = createCtx(factory, env.Conf{}, e, Flags{DisableIgnore: true}, []string{}, nil)
 	assert.Equal(t, ErrLiveTheme, err)
 	assert.Equal(t, e.ThemeID, "1234")
 }