From 3f46a51d38a3a929bb694a9a06bc5ae47d460e83 Mon Sep 17 00:00:00 2001 From: Zachary Scott Date: Tue, 31 Jul 2018 13:06:47 +0900 Subject: [PATCH 01/11] Move collapse under config command --- cmd/collapse.go | 36 ----------------- cmd/collapse_test.go | 92 -------------------------------------------- cmd/config.go | 27 +++++++++++++ cmd/config_test.go | 82 +++++++++++++++++++++++++++++++++++++++ cmd/root.go | 1 - cmd/root_test.go | 2 +- 6 files changed, 110 insertions(+), 130 deletions(-) delete mode 100644 cmd/collapse.go delete mode 100644 cmd/collapse_test.go diff --git a/cmd/collapse.go b/cmd/collapse.go deleted file mode 100644 index f55df68b5..000000000 --- a/cmd/collapse.go +++ /dev/null @@ -1,36 +0,0 @@ -package cmd - -import ( - "github.com/CircleCI-Public/circleci-cli/filetree" - "github.com/pkg/errors" - "github.com/spf13/cobra" - yaml "gopkg.in/yaml.v2" -) - -var root string - -func newCollapseCommand() *cobra.Command { - - collapseCommand := &cobra.Command{ - Use: "collapse", - Short: "Collapse your CircleCI configuration to a single file", - RunE: collapse, - } - collapseCommand.Flags().StringVarP(&root, "root", "r", ".", "path to your configuration (default is current path)") - - return collapseCommand -} - -func collapse(cmd *cobra.Command, args []string) error { - tree, err := filetree.NewTree(root) - if err != nil { - return errors.Wrap(err, "An error occurred trying to build the tree") - } - - y, err := yaml.Marshal(&tree) - if err != nil { - return errors.Wrap(err, "Failed trying to marshal the tree to YAML ") - } - Logger.Infof("%s\n", string(y)) - return nil -} diff --git a/cmd/collapse_test.go b/cmd/collapse_test.go deleted file mode 100644 index 28ef77f4f..000000000 --- a/cmd/collapse_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package cmd_test - -import ( - "io/ioutil" - "os/exec" - "path/filepath" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/onsi/gomega/gexec" -) - -var _ = Describe("collapse", func() { - var ( - command *exec.Cmd - results []byte - ) - - Describe("a .circleci folder with config.yml and local orbs folder containing the hugo orb", func() { - BeforeEach(func() { - var err error - command = exec.Command(pathCLI, "collapse", "-r", "testdata/hugo-collapse/.circleci") - results, err = ioutil.ReadFile("testdata/hugo-collapse/result.yml") - Expect(err).ShouldNot(HaveOccurred()) - }) - - It("collapse all YAML contents as expected", func() { - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - session.Wait() - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Err.Contents()).Should(BeEmpty()) - Eventually(session.Out.Contents()).Should(MatchYAML(results)) - Eventually(session).Should(gexec.Exit(0)) - }) - }) - - Describe("local orbs folder with mixed inline and local commands, jobs, etc", func() { - BeforeEach(func() { - var err error - var path string = "nested-orbs-and-local-commands-etc" - command = exec.Command(pathCLI, "collapse", "-r", filepath.Join("testdata", path, "test")) - results, err = ioutil.ReadFile(filepath.Join("testdata", path, "result.yml")) - Expect(err).ShouldNot(HaveOccurred()) - }) - - It("collapse all YAML contents as expected", func() { - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - session.Wait() - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Err.Contents()).Should(BeEmpty()) - Eventually(session.Out.Contents()).Should(MatchYAML(results)) - Eventually(session).Should(gexec.Exit(0)) - }) - }) - - Describe("an orb containing local executors and commands in folder", func() { - BeforeEach(func() { - var err error - command = exec.Command(pathCLI, "collapse", "-r", "testdata/myorb/test") - results, err = ioutil.ReadFile("testdata/myorb/result.yml") - Expect(err).ShouldNot(HaveOccurred()) - }) - - It("collapse all YAML contents as expected", func() { - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - session.Wait() - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Err.Contents()).Should(BeEmpty()) - Eventually(session.Out.Contents()).Should(MatchYAML(results)) - Eventually(session).Should(gexec.Exit(0)) - }) - }) - - Describe("with a large nested config including rails orb", func() { - BeforeEach(func() { - var err error - var path string = "test-with-large-nested-rails-orb" - command = exec.Command(pathCLI, "collapse", "-r", filepath.Join("testdata", path, "test")) - results, err = ioutil.ReadFile(filepath.Join("testdata", path, "result.yml")) - Expect(err).ShouldNot(HaveOccurred()) - }) - - It("collapse all YAML contents as expected", func() { - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - session.Wait() - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Err.Contents()).Should(BeEmpty()) - Eventually(session.Out.Contents()).Should(MatchYAML(results)) - Eventually(session).Should(gexec.Exit(0)) - }) - }) -}) diff --git a/cmd/config.go b/cmd/config.go index 27aa6c638..f507e1af3 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -4,17 +4,29 @@ import ( "context" "github.com/CircleCI-Public/circleci-cli/api" + "github.com/CircleCI-Public/circleci-cli/filetree" + "github.com/pkg/errors" "github.com/spf13/cobra" + yaml "gopkg.in/yaml.v2" ) const defaultConfigPath = ".circleci/config.yml" +var root string + func newConfigCommand() *cobra.Command { configCmd := &cobra.Command{ Use: "config", Short: "Operate on build config files", } + collapseCommand := &cobra.Command{ + Use: "collapse", + Short: "Collapse your CircleCI configuration to a single file", + RunE: collapseConfig, + } + collapseCommand.Flags().StringVarP(&root, "root", "r", ".", "path to your configuration (default is current path)") + validateCommand := &cobra.Command{ Use: "validate [config.yml]", Aliases: []string{"check"}, @@ -30,6 +42,7 @@ func newConfigCommand() *cobra.Command { Args: cobra.MaximumNArgs(1), } + configCmd.AddCommand(collapseCommand) configCmd.AddCommand(validateCommand) configCmd.AddCommand(expandCommand) @@ -76,3 +89,17 @@ func expandConfig(cmd *cobra.Command, args []string) error { Logger.Info(response.OutputYaml) return nil } + +func collapseConfig(cmd *cobra.Command, args []string) error { + tree, err := filetree.NewTree(root) + if err != nil { + return errors.Wrap(err, "An error occurred trying to build the tree") + } + + y, err := yaml.Marshal(&tree) + if err != nil { + return errors.Wrap(err, "Failed trying to marshal the tree to YAML ") + } + Logger.Infof("%s\n", string(y)) + return nil +} diff --git a/cmd/config_test.go b/cmd/config_test.go index d23e004b3..ed693b64a 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -1,6 +1,7 @@ package cmd_test import ( + "io/ioutil" "net/http" "os" "os/exec" @@ -206,4 +207,85 @@ var _ = Describe("Config", func() { }) }) }) + + Describe("collapse", func() { + var ( + command *exec.Cmd + results []byte + ) + + Describe("a .circleci folder with config.yml and local orbs folder containing the hugo orb", func() { + BeforeEach(func() { + var err error + command = exec.Command(pathCLI, "config", "collapse", "-r", "testdata/hugo-collapse/.circleci") + results, err = ioutil.ReadFile("testdata/hugo-collapse/result.yml") + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("collapse all YAML contents as expected", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + session.Wait() + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err.Contents()).Should(BeEmpty()) + Eventually(session.Out.Contents()).Should(MatchYAML(results)) + Eventually(session).Should(gexec.Exit(0)) + }) + }) + + Describe("local orbs folder with mixed inline and local commands, jobs, etc", func() { + BeforeEach(func() { + var err error + var path string = "nested-orbs-and-local-commands-etc" + command = exec.Command(pathCLI, "config", "collapse", "-r", filepath.Join("testdata", path, "test")) + results, err = ioutil.ReadFile(filepath.Join("testdata", path, "result.yml")) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("collapse all YAML contents as expected", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + session.Wait() + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err.Contents()).Should(BeEmpty()) + Eventually(session.Out.Contents()).Should(MatchYAML(results)) + Eventually(session).Should(gexec.Exit(0)) + }) + }) + + Describe("an orb containing local executors and commands in folder", func() { + BeforeEach(func() { + var err error + command = exec.Command(pathCLI, "config", "collapse", "-r", "testdata/myorb/test") + results, err = ioutil.ReadFile("testdata/myorb/result.yml") + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("collapse all YAML contents as expected", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + session.Wait() + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err.Contents()).Should(BeEmpty()) + Eventually(session.Out.Contents()).Should(MatchYAML(results)) + Eventually(session).Should(gexec.Exit(0)) + }) + }) + + Describe("with a large nested config including rails orb", func() { + BeforeEach(func() { + var err error + var path string = "test-with-large-nested-rails-orb" + command = exec.Command(pathCLI, "config", "collapse", "-r", filepath.Join("testdata", path, "test")) + results, err = ioutil.ReadFile(filepath.Join("testdata", path, "result.yml")) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("collapse all YAML contents as expected", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + session.Wait() + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err.Contents()).Should(BeEmpty()) + Eventually(session.Out.Contents()).Should(MatchYAML(results)) + Eventually(session).Should(gexec.Exit(0)) + }) + }) + }) }) diff --git a/cmd/root.go b/cmd/root.go index 2c793d64b..4b5d436de 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -39,7 +39,6 @@ func MakeCommands() *cobra.Command { rootCmd.AddCommand(newDiagnosticCommand()) rootCmd.AddCommand(newQueryCommand()) - rootCmd.AddCommand(newCollapseCommand()) rootCmd.AddCommand(newConfigureCommand()) rootCmd.AddCommand(newConfigCommand()) rootCmd.AddCommand(newOrbCommand()) diff --git a/cmd/root_test.go b/cmd/root_test.go index 52af4e799..27d3bf0e3 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -16,7 +16,7 @@ var _ = Describe("Root", func() { It("can create commands", func() { commands := cmd.MakeCommands() - Expect(len(commands.Commands())).To(Equal(9)) + Expect(len(commands.Commands())).To(Equal(8)) }) }) From 0bce99eaceb76c0423cd7be06c9dacf44af09b63 Mon Sep 17 00:00:00 2001 From: Zachary Scott Date: Tue, 31 Jul 2018 13:29:59 +0900 Subject: [PATCH 02/11] Rename `circleci configure` to `circleci setup` --- cmd/root.go | 8 +++----- cmd/{configure.go => setup.go} | 21 ++++++++++----------- cmd/{configure_test.go => setup_test.go} | 8 ++++---- 3 files changed, 17 insertions(+), 20 deletions(-) rename cmd/{configure.go => setup.go} (82%) rename cmd/{configure_test.go => setup_test.go} (89%) diff --git a/cmd/root.go b/cmd/root.go index 4b5d436de..cd12536a3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -39,7 +39,7 @@ func MakeCommands() *cobra.Command { rootCmd.AddCommand(newDiagnosticCommand()) rootCmd.AddCommand(newQueryCommand()) - rootCmd.AddCommand(newConfigureCommand()) + rootCmd.AddCommand(newSetupCommand()) rootCmd.AddCommand(newConfigCommand()) rootCmd.AddCommand(newOrbCommand()) rootCmd.AddCommand(newBuildCommand()) @@ -74,8 +74,7 @@ func bindCobraFlagToViper(command *cobra.Command, flag string) { } func init() { - - cobra.OnInitialize(setup) + cobra.OnInitialize(prepare) configDir := path.Join(settings.UserHomeDir(), ".circleci") @@ -91,9 +90,8 @@ func init() { if err := viper.ReadInConfig(); err != nil { panic(err) } - } -func setup() { +func prepare() { Logger = logger.NewLogger(viper.GetBool("verbose")) } diff --git a/cmd/configure.go b/cmd/setup.go similarity index 82% rename from cmd/configure.go rename to cmd/setup.go index 8d21665b5..c5a296b37 100644 --- a/cmd/configure.go +++ b/cmd/setup.go @@ -12,20 +12,19 @@ import ( var testing = false -func newConfigureCommand() *cobra.Command { - - configureCommand := &cobra.Command{ - Use: "configure", - Short: "Configure the tool with your credentials", - RunE: configure, +func newSetupCommand() *cobra.Command { + setupCommand := &cobra.Command{ + Use: "setup", + Short: "Setup the CLI with your credentials", + RunE: setup, } - configureCommand.Flags().BoolVar(&testing, "testing", false, "Enable test mode to bypass interactive UI.") - if err := configureCommand.Flags().MarkHidden("testing"); err != nil { + setupCommand.Flags().BoolVar(&testing, "testing", false, "Enable test mode to bypass interactive UI.") + if err := setupCommand.Flags().MarkHidden("testing"); err != nil { panic(err) } - return configureCommand + return setupCommand } // We can't properly run integration tests on code that calls PromptUI. @@ -93,7 +92,7 @@ func shouldAskForToken(token string, ui userInterface) bool { return ui.askUserToConfirm("A CircleCI token is already set. Do you want to change it") } -func configure(cmd *cobra.Command, args []string) error { +func setup(cmd *cobra.Command, args []string) error { token := viper.GetString("token") var ui userInterface = interactiveUI{} @@ -121,6 +120,6 @@ func configure(cmd *cobra.Command, args []string) error { return errors.Wrap(err, "Failed to save config file") } - Logger.Info("Configuration has been saved.") + Logger.Info("Setup complete. Your configuration has been saved.") return nil } diff --git a/cmd/configure_test.go b/cmd/setup_test.go similarity index 89% rename from cmd/configure_test.go rename to cmd/setup_test.go index d324671f1..aa42e9050 100644 --- a/cmd/configure_test.go +++ b/cmd/setup_test.go @@ -13,7 +13,7 @@ import ( "github.com/onsi/gomega/gexec" ) -var _ = Describe("Configure", func() { +var _ = Describe("Setup", func() { var ( tempHome string command *exec.Cmd @@ -24,7 +24,7 @@ var _ = Describe("Configure", func() { tempHome, err = ioutil.TempDir("", "circleci-cli-test-") Expect(err).ToNot(HaveOccurred()) - command = exec.Command(pathCLI, "configure", "--testing") + command = exec.Command(pathCLI, "setup", "--testing") command.Env = append(os.Environ(), fmt.Sprintf("HOME=%s", tempHome), fmt.Sprintf("USERPROFILE=%s", tempHome), // windows @@ -66,7 +66,7 @@ var _ = Describe("Configure", func() { Eventually(session.Out).Should(gbytes.Say("API token has been set.")) Eventually(session.Out).Should(gbytes.Say("CircleCI API End Point")) Eventually(session.Out).Should(gbytes.Say("API endpoint has been set.")) - Eventually(session.Out).Should(gbytes.Say("Configuration has been saved.")) + Eventually(session.Out).Should(gbytes.Say("Setup complete. Your configuration has been saved.")) Eventually(session).Should(gexec.Exit(0)) }) }) @@ -89,7 +89,7 @@ token: fooBarBaz Eventually(session.Out).Should(gbytes.Say("API token has been set.")) Eventually(session.Out).Should(gbytes.Say("CircleCI API End Point")) Eventually(session.Out).Should(gbytes.Say("API endpoint has been set.")) - Eventually(session.Out).Should(gbytes.Say("Configuration has been saved.")) + Eventually(session.Out).Should(gbytes.Say("Setup complete. Your configuration has been saved.")) Eventually(session).Should(gexec.Exit(0)) }) }) From 844a4cf183bebed9a71dc929bf150e0f30fb3177 Mon Sep 17 00:00:00 2001 From: Zachary Scott Date: Tue, 31 Jul 2018 13:41:08 +0900 Subject: [PATCH 03/11] Rename `ns` to `namespace` (to opaque as `ns`) and add a description --- cmd/orb.go | 9 ++++++- cmd/orb_test.go | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/cmd/orb.go b/cmd/orb.go index 2b2dac251..5e00acb43 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -87,10 +87,16 @@ func newOrbCommand() *cobra.Command { createNamespace.PersistentFlags().StringVar(&organizationVcs, "vcs", "github", "organization vcs, e.g. 'github', 'bitbucket'") namespaceCommand := &cobra.Command{ - Use: "ns", + Use: "namespace", Short: "Operate on orb namespaces (create, etc.)", } namespaceCommand.AddCommand(createNamespace) + nsCommand := &cobra.Command{ + Use: "ns", + Short: "Operate on orb namespaces (create, etc.)", + Hidden: true, + } + nsCommand.AddCommand(createNamespace) orbCommand := &cobra.Command{ Use: "orb", @@ -105,6 +111,7 @@ func newOrbCommand() *cobra.Command { orbCommand.AddCommand(publishCommand) orbCommand.AddCommand(namespaceCommand) + orbCommand.AddCommand(nsCommand) orbCommand.AddCommand(sourceCommand) return orbCommand diff --git a/cmd/orb_test.go b/cmd/orb_test.go index 381ed9a14..ab515a31e 100644 --- a/cmd/orb_test.go +++ b/cmd/orb_test.go @@ -420,6 +420,71 @@ var _ = Describe("Orb integration tests", func() { }) }) + Describe("when using the full namespace command", func() { + BeforeEach(func() { + command = exec.Command(pathCLI, + "orb", "namespace", "create", + "-t", token, + "-e", testServer.URL(), + "foo-ns", + "--org-name", "test-org", + "--vcs", "BITBUCKET", + ) + }) + + It("works with organizationName and organizationVcs", func() { + By("setting up a mock server") + + gqlOrganizationResponse := `{ + "organization": { + "name": "test-org", + "id": "bb604b45-b6b0-4b81-ad80-796f15eddf87" + } + }` + + expectedOrganizationRequest := `{ + "query": "\n\t\t\tquery($organizationName: String!, $organizationVcs: VCSType!) {\n\t\t\t\torganization(\n\t\t\t\t\tname: $organizationName\n\t\t\t\t\tvcsType: $organizationVcs\n\t\t\t\t) {\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t}", + "variables": { + "organizationName": "test-org", + "organizationVcs": "BITBUCKET" + } + }` + + gqlNsResponse := `{ + "createNamespace": { + "errors": [], + "namespace": { + "id": "bb604b45-b6b0-4b81-ad80-796f15eddf87" + } + } + }` + + expectedNsRequest := `{ + "query": "\n\t\t\tmutation($name: String!, $organizationId: UUID!) {\n\t\t\t\tcreateNamespace(\n\t\t\t\t\tname: $name,\n\t\t\t\t\torganizationId: $organizationId\n\t\t\t\t) {\n\t\t\t\t\tnamespace {\n\t\t\t\t\t\tid\n\t\t\t\t\t}\n\t\t\t\t\terrors {\n\t\t\t\t\t\tmessage\n\t\t\t\t\t\ttype\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}", + "variables": { + "name": "foo-ns", + "organizationId": "bb604b45-b6b0-4b81-ad80-796f15eddf87" + } + }` + + appendPostHandler(testServer, token, MockRequestResponse{ + Status: http.StatusOK, + Request: expectedOrganizationRequest, + Response: gqlOrganizationResponse}) + appendPostHandler(testServer, token, MockRequestResponse{ + Status: http.StatusOK, + Request: expectedNsRequest, + Response: gqlNsResponse}) + + By("running the command") + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Out).Should(gbytes.Say("Namespace created")) + Eventually(session).Should(gexec.Exit(0)) + }) + }) + Describe("when creating / reserving a namespace", func() { BeforeEach(func() { command = exec.Command(pathCLI, From 57ad820a633929ad29bd2b187a6028c1a5b9e2a4 Mon Sep 17 00:00:00 2001 From: Zachary Scott Date: Tue, 31 Jul 2018 13:49:24 +0900 Subject: [PATCH 04/11] Make --org-name and --vcs positional arguments on `orb ns create` --- cmd/orb.go | 15 +++++---------- cmd/orb_test.go | 8 ++++---- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/cmd/orb.go b/cmd/orb.go index 5e00acb43..3b3c8ba90 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -18,8 +18,6 @@ import ( var orbVersion string var orbID string -var organizationName string -var organizationVcs string func newOrbCommand() *cobra.Command { @@ -74,17 +72,14 @@ func newOrbCommand() *cobra.Command { } createNamespace := &cobra.Command{ - Use: "create ", + Use: "create [name] [vcs] [org-name]", Short: "create an orb namespace", RunE: createOrbNamespace, - Args: cobra.ExactArgs(1), + Args: cobra.ExactArgs(3), } - createNamespace.PersistentFlags().StringVar(&organizationName, "org-name", "", "organization name (required)") - if err := createNamespace.MarkPersistentFlagRequired("org-name"); err != nil { - panic(err) - } - createNamespace.PersistentFlags().StringVar(&organizationVcs, "vcs", "github", "organization vcs, e.g. 'github', 'bitbucket'") + // "org-name", "", "organization name (required)" + // "vcs", "github", "organization vcs, e.g. 'github', 'bitbucket'" namespaceCommand := &cobra.Command{ Use: "namespace", @@ -343,7 +338,7 @@ func createOrbNamespace(cmd *cobra.Command, args []string) error { var err error ctx := context.Background() - response, err := api.CreateNamespace(ctx, Logger, args[0], organizationName, strings.ToUpper(organizationVcs)) + response, err := api.CreateNamespace(ctx, Logger, args[0], args[2], strings.ToUpper(args[1])) if err != nil { return err diff --git a/cmd/orb_test.go b/cmd/orb_test.go index ab515a31e..5865efe05 100644 --- a/cmd/orb_test.go +++ b/cmd/orb_test.go @@ -427,8 +427,8 @@ var _ = Describe("Orb integration tests", func() { "-t", token, "-e", testServer.URL(), "foo-ns", - "--org-name", "test-org", - "--vcs", "BITBUCKET", + "BITBUCKET", + "test-org", ) }) @@ -492,8 +492,8 @@ var _ = Describe("Orb integration tests", func() { "-t", token, "-e", testServer.URL(), "foo-ns", - "--org-name", "test-org", - "--vcs", "BITBUCKET", + "BITBUCKET", + "test-org", ) }) From d99aa3df10493e9f3ace62a8853ef9e342109c23 Mon Sep 17 00:00:00 2001 From: Zachary Scott Date: Tue, 31 Jul 2018 14:12:54 +0900 Subject: [PATCH 05/11] `config collapse` command should use positional argument for path --- cmd/config.go | 11 ++++++----- cmd/config_test.go | 8 ++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index f507e1af3..6ef709e27 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -12,8 +12,6 @@ import ( const defaultConfigPath = ".circleci/config.yml" -var root string - func newConfigCommand() *cobra.Command { configCmd := &cobra.Command{ Use: "config", @@ -21,11 +19,11 @@ func newConfigCommand() *cobra.Command { } collapseCommand := &cobra.Command{ - Use: "collapse", + Use: "collapse [path]", Short: "Collapse your CircleCI configuration to a single file", RunE: collapseConfig, + Args: cobra.MaximumNArgs(1), } - collapseCommand.Flags().StringVarP(&root, "root", "r", ".", "path to your configuration (default is current path)") validateCommand := &cobra.Command{ Use: "validate [config.yml]", @@ -50,7 +48,6 @@ func newConfigCommand() *cobra.Command { } func validateConfig(cmd *cobra.Command, args []string) error { - configPath := defaultConfigPath if len(args) == 1 { configPath = args[0] @@ -91,6 +88,10 @@ func expandConfig(cmd *cobra.Command, args []string) error { } func collapseConfig(cmd *cobra.Command, args []string) error { + root := "." + if len(args) > 0 { + root = args[0] + } tree, err := filetree.NewTree(root) if err != nil { return errors.Wrap(err, "An error occurred trying to build the tree") diff --git a/cmd/config_test.go b/cmd/config_test.go index ed693b64a..c10ac24c6 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -217,7 +217,7 @@ var _ = Describe("Config", func() { Describe("a .circleci folder with config.yml and local orbs folder containing the hugo orb", func() { BeforeEach(func() { var err error - command = exec.Command(pathCLI, "config", "collapse", "-r", "testdata/hugo-collapse/.circleci") + command = exec.Command(pathCLI, "config", "collapse", "testdata/hugo-collapse/.circleci") results, err = ioutil.ReadFile("testdata/hugo-collapse/result.yml") Expect(err).ShouldNot(HaveOccurred()) }) @@ -236,7 +236,7 @@ var _ = Describe("Config", func() { BeforeEach(func() { var err error var path string = "nested-orbs-and-local-commands-etc" - command = exec.Command(pathCLI, "config", "collapse", "-r", filepath.Join("testdata", path, "test")) + command = exec.Command(pathCLI, "config", "collapse", filepath.Join("testdata", path, "test")) results, err = ioutil.ReadFile(filepath.Join("testdata", path, "result.yml")) Expect(err).ShouldNot(HaveOccurred()) }) @@ -254,7 +254,7 @@ var _ = Describe("Config", func() { Describe("an orb containing local executors and commands in folder", func() { BeforeEach(func() { var err error - command = exec.Command(pathCLI, "config", "collapse", "-r", "testdata/myorb/test") + command = exec.Command(pathCLI, "config", "collapse", "testdata/myorb/test") results, err = ioutil.ReadFile("testdata/myorb/result.yml") Expect(err).ShouldNot(HaveOccurred()) }) @@ -273,7 +273,7 @@ var _ = Describe("Config", func() { BeforeEach(func() { var err error var path string = "test-with-large-nested-rails-orb" - command = exec.Command(pathCLI, "config", "collapse", "-r", filepath.Join("testdata", path, "test")) + command = exec.Command(pathCLI, "config", "collapse", filepath.Join("testdata", path, "test")) results, err = ioutil.ReadFile(filepath.Join("testdata", path, "result.yml")) Expect(err).ShouldNot(HaveOccurred()) }) From 7d31118c0b528424b3ed390f96ee216c3c1cdbe7 Mon Sep 17 00:00:00 2001 From: Zachary Scott Date: Tue, 31 Jul 2018 16:48:29 +0900 Subject: [PATCH 06/11] `circleci orb publish` should not require a UUID to work We can use the get-orb-by-name query to get the UUID by name instead. Here's the output from help for this command following this commit: ``` publish a version of an orb Usage: circleci orb publish / [flags] Flags: -h, --help help for publish -o, --orb-version string version of orb to publish (required) Global Flags: ... ``` --- api/api.go | 38 ++++++++++++++++++++++++++++++- cmd/orb.go | 19 +++++----------- cmd/orb_test.go | 60 +++++++++++++++++++++++++++++++++++++------------ 3 files changed, 88 insertions(+), 29 deletions(-) diff --git a/api/api.go b/api/api.go index e8774256b..0e6680aa2 100644 --- a/api/api.go +++ b/api/api.go @@ -152,7 +152,12 @@ func OrbQuery(ctx context.Context, logger *logger.Logger, configPath string) (*C // OrbPublish publishes a new version of an orb func OrbPublish(ctx context.Context, logger *logger.Logger, - configPath string, orbVersion string, orbID string) (*PublishOrbResponse, error) { + name string, configPath string, orbVersion string) (*PublishOrbResponse, error) { + orbID, err := getOrbID(ctx, logger, name) + if err != nil { + return nil, err + } + var response struct { PublishOrb struct { PublishOrbResponse @@ -194,6 +199,37 @@ func OrbPublish(ctx context.Context, logger *logger.Logger, return &response.PublishOrb.PublishOrbResponse, err } +func getOrbID(ctx context.Context, logger *logger.Logger, name string) (string, error) { + var response struct { + Orb struct { + ID string + } + } + + query := `query($name: String!) { + orb(name: $name) { + id + } + }` + + request := client.NewAuthorizedRequest(viper.GetString("token"), query) + request.Var("name", name) + + graphQLclient := client.NewClient(viper.GetString("endpoint"), logger) + + err := graphQLclient.Run(ctx, request, &response) + + if err != nil { + return "", err + } + + if response.Orb.ID == "" { + return "", fmt.Errorf("the %s orb could not be found", name) + } + + return response.Orb.ID, nil +} + func createNamespaceWithOwnerID(ctx context.Context, logger *logger.Logger, name string, ownerID string) (*CreateNamespaceResponse, error) { var response struct { CreateNamespace struct { diff --git a/cmd/orb.go b/cmd/orb.go index 3b3c8ba90..89ff008f3 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -17,7 +17,6 @@ import ( ) var orbVersion string -var orbID string func newOrbCommand() *cobra.Command { @@ -42,19 +41,15 @@ func newOrbCommand() *cobra.Command { } publishCommand := &cobra.Command{ - Use: "publish [orb.yml]", + Use: "publish / ", Short: "publish a version of an orb", RunE: publishOrb, - Args: cobra.MaximumNArgs(1), + Args: cobra.ExactArgs(2), } publishCommand.Flags().StringVarP(&orbVersion, "orb-version", "o", "", "version of orb to publish (required)") - publishCommand.Flags().StringVarP(&orbID, "orb-id", "i", "", "id of orb to publish (required)") - - for _, flag := range [2]string{"orb-version", "orb-id"} { - if err := publishCommand.MarkFlagRequired(flag); err != nil { - panic(err) - } + if err := publishCommand.MarkFlagRequired("orb-version"); err != nil { + panic(err) } sourceCommand := &cobra.Command{ @@ -291,12 +286,8 @@ func expandOrb(cmd *cobra.Command, args []string) error { func publishOrb(cmd *cobra.Command, args []string) error { ctx := context.Background() - orbPath := defaultOrbPath - if len(args) == 1 { - orbPath = args[0] - } - response, err := api.OrbPublish(ctx, Logger, orbPath, orbVersion, orbID) + response, err := api.OrbPublish(ctx, Logger, args[0], args[1], orbVersion) if err != nil { return err diff --git a/cmd/orb_test.go b/cmd/orb_test.go index 5865efe05..29c6430eb 100644 --- a/cmd/orb_test.go +++ b/cmd/orb_test.go @@ -332,9 +332,9 @@ var _ = Describe("Orb integration tests", func() { "orb", "publish", "-t", token, "-e", testServer.URL(), + "my/orb", orb.Path, "--orb-version", "0.0.1", - "--orb-id", "bb604b45-b6b0-4b81-ad80-796f15eddf87", ) }) @@ -348,7 +348,20 @@ var _ = Describe("Orb integration tests", func() { // assert write to test file successful Expect(err).ToNot(HaveOccurred()) - gqlResponse := `{ + gqlOrbIDResponse := `{ + "orb": { + "id": "bb604b45-b6b0-4b81-ad80-796f15eddf87" + } + }` + + expectedOrbIDRequest := `{ + "query": "query($name: String!) {\n\t\t\t orb(name: $name) {\n\t\t\t id\n\t\t\t }\n\t\t }", + "variables": { + "name": "my/orb" + } + }` + + gqlPublishResponse := `{ "publishOrb": { "errors": [], "orb": { @@ -357,7 +370,7 @@ var _ = Describe("Orb integration tests", func() { } }` - expectedRequestJson := `{ + expectedPublishRequest := `{ "query": "\n\t\tmutation($config: String!, $orbId: UUID!, $version: String!) {\n\t\t\tpublishOrb(\n\t\t\t\torbId: $orbId,\n\t\t\t\torbYaml: $config,\n\t\t\t\tversion: $version\n\t\t\t) {\n\t\t\t\torb {\n\t\t\t\t\tversion\n\t\t\t\t}\n\t\t\t\terrors { message }\n\t\t\t}\n\t\t}\n\t", "variables": { "config": "some orb", @@ -368,9 +381,12 @@ var _ = Describe("Orb integration tests", func() { appendPostHandler(testServer, token, MockRequestResponse{ Status: http.StatusOK, - Request: expectedRequestJson, - Response: gqlResponse, - }) + Request: expectedOrbIDRequest, + Response: gqlOrbIDResponse}) + appendPostHandler(testServer, token, MockRequestResponse{ + Status: http.StatusOK, + Request: expectedPublishRequest, + Response: gqlPublishResponse}) By("running the command") session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) @@ -385,17 +401,30 @@ var _ = Describe("Orb integration tests", func() { err := orb.write(`some orb`) Expect(err).ToNot(HaveOccurred()) - gqlResponse := `{ - "publishOrb": { + gqlOrbIDResponse := `{ + "orb": { + "id": "bb604b45-b6b0-4b81-ad80-796f15eddf87" + } + }` + + expectedOrbIDRequest := `{ + "query": "query($name: String!) {\n\t\t\t orb(name: $name) {\n\t\t\t id\n\t\t\t }\n\t\t }", + "variables": { + "name": "my/orb" + } + }` + + gqlPublishResponse := `{ + "publishOrb": { "errors": [ {"message": "error1"}, {"message": "error2"} ], "orb": null - } - }` + } + }` - expectedRequestJson := `{ + expectedPublishRequest := `{ "query": "\n\t\tmutation($config: String!, $orbId: UUID!, $version: String!) {\n\t\t\tpublishOrb(\n\t\t\t\torbId: $orbId,\n\t\t\t\torbYaml: $config,\n\t\t\t\tversion: $version\n\t\t\t) {\n\t\t\t\torb {\n\t\t\t\t\tversion\n\t\t\t\t}\n\t\t\t\terrors { message }\n\t\t\t}\n\t\t}\n\t", "variables": { "config": "some orb", @@ -406,9 +435,12 @@ var _ = Describe("Orb integration tests", func() { appendPostHandler(testServer, token, MockRequestResponse{ Status: http.StatusOK, - Request: expectedRequestJson, - Response: gqlResponse, - }) + Request: expectedOrbIDRequest, + Response: gqlOrbIDResponse}) + appendPostHandler(testServer, token, MockRequestResponse{ + Status: http.StatusOK, + Request: expectedPublishRequest, + Response: gqlPublishResponse}) By("running the command") session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) From 37dcc4d1598f69ee0389a941a242091830593714 Mon Sep 17 00:00:00 2001 From: Zachary Scott Date: Wed, 1 Aug 2018 17:33:28 +0900 Subject: [PATCH 07/11] Restructure diagnostic, setup, and update under `local` command * circleci diagnostic -> circleci local check * circleci setup -> circleci local setup * circleci update -> circleci local update Have to go back and fix compatibility for: * circleci update * circleci update check --- cmd/diagnostic.go | 33 ------- cmd/diagnostic_test.go | 96 -------------------- cmd/{setup.go => local.go} | 110 +++++++++++++++++++++- cmd/local_test.go | 181 +++++++++++++++++++++++++++++++++++++ cmd/root.go | 4 +- cmd/root_test.go | 2 +- cmd/setup_test.go | 97 -------------------- cmd/update.go | 83 ----------------- 8 files changed, 288 insertions(+), 318 deletions(-) delete mode 100644 cmd/diagnostic.go delete mode 100644 cmd/diagnostic_test.go rename cmd/{setup.go => local.go} (52%) create mode 100644 cmd/local_test.go delete mode 100644 cmd/setup_test.go delete mode 100644 cmd/update.go diff --git a/cmd/diagnostic.go b/cmd/diagnostic.go deleted file mode 100644 index 2d18e7789..000000000 --- a/cmd/diagnostic.go +++ /dev/null @@ -1,33 +0,0 @@ -package cmd - -import ( - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -func newDiagnosticCommand() *cobra.Command { - return &cobra.Command{ - Use: "diagnostic", - Short: "Check the status of your CircleCI CLI.", - RunE: diagnostic, - } -} - -func diagnostic(cmd *cobra.Command, args []string) error { - endpoint := viper.GetString("endpoint") - token := viper.GetString("token") - - Logger.Infoln("\n---\nCircleCI CLI Diagnostics\n---\n") - Logger.Infof("Config found: %v\n", viper.ConfigFileUsed()) - - Logger.Infof("GraphQL API endpoint: %s\n", endpoint) - - if token == "token" || token == "" { - return errors.New("please set a token") - } - Logger.Infoln("OK, got a token.") - Logger.Infof("Verbose mode: %v\n", viper.GetBool("verbose")) - - return nil -} diff --git a/cmd/diagnostic_test.go b/cmd/diagnostic_test.go deleted file mode 100644 index 9da949bc9..000000000 --- a/cmd/diagnostic_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package cmd_test - -import ( - "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/onsi/gomega/gbytes" - "github.com/onsi/gomega/gexec" -) - -var _ = Describe("Diagnostic", func() { - var ( - tempHome string - command *exec.Cmd - ) - - BeforeEach(func() { - var err error - tempHome, err = ioutil.TempDir("", "circleci-cli-test-") - Expect(err).ToNot(HaveOccurred()) - - command = exec.Command(pathCLI, "diagnostic") - command.Env = append(os.Environ(), - fmt.Sprintf("HOME=%s", tempHome), - ) - }) - - AfterEach(func() { - Expect(os.RemoveAll(tempHome)).To(Succeed()) - }) - - Describe("existing config file", func() { - var config *os.File - - BeforeEach(func() { - const ( - configDir = ".circleci" - configFile = "cli.yml" - ) - - Expect(os.Mkdir(filepath.Join(tempHome, configDir), 0700)).To(Succeed()) - - var err error - config, err = os.OpenFile( - filepath.Join(tempHome, configDir, configFile), - os.O_RDWR|os.O_CREATE, - 0600, - ) - Expect(err).ToNot(HaveOccurred()) - }) - - Describe("token and endpoint set in config file", func() { - BeforeEach(func() { - _, err := config.Write([]byte(` -endpoint: https://example.com/graphql -token: mytoken -`)) - Expect(err).ToNot(HaveOccurred()) - Expect(config.Close()).To(Succeed()) - }) - - It("print success", func() { - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Err.Contents()).Should(BeEmpty()) - Eventually(session.Out).Should(gbytes.Say("GraphQL API endpoint: https://example.com/graphql")) - Eventually(session.Out).Should(gbytes.Say("OK, got a token.")) - Eventually(session).Should(gexec.Exit(0)) - }) - }) - - Context("token set to empty string in config file", func() { - BeforeEach(func() { - _, err := config.Write([]byte(` -endpoint: https://example.com/graphql -token: -`)) - Expect(err).ToNot(HaveOccurred()) - Expect(config.Close()).To(Succeed()) - }) - - It("print error", func() { - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Err).Should(gbytes.Say("Error: please set a token")) - Eventually(session.Out).Should(gbytes.Say("GraphQL API endpoint: https://example.com/graphql")) - Eventually(session).Should(gexec.Exit(255)) - }) - }) - }) -}) diff --git a/cmd/setup.go b/cmd/local.go similarity index 52% rename from cmd/setup.go rename to cmd/local.go index c5a296b37..55b2ca775 100644 --- a/cmd/setup.go +++ b/cmd/local.go @@ -1,18 +1,27 @@ package cmd import ( + "encoding/json" + "io/ioutil" + "net/http" "strings" + "unicode/utf8" - "github.com/pkg/errors" - "github.com/spf13/viper" - + "github.com/CircleCI-Public/circleci-cli/version" "github.com/manifoldco/promptui" + "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/spf13/viper" ) var testing = false -func newSetupCommand() *cobra.Command { +func newLocalCommand() *cobra.Command { + localCommand := &cobra.Command{ + Use: "local", + Short: "Operate on your local CircleCI CLI", + } + setupCommand := &cobra.Command{ Use: "setup", Short: "Setup the CLI with your credentials", @@ -24,7 +33,98 @@ func newSetupCommand() *cobra.Command { panic(err) } - return setupCommand + updateCommand := &cobra.Command{ + Use: "update", + Short: "Update the tool", + RunE: update, + } + + checkCommand := &cobra.Command{ + Use: "check", + Short: "Check the status of your CircleCI CLI.", + RunE: check, + } + + localCommand.AddCommand(setupCommand) + localCommand.AddCommand(updateCommand) + localCommand.AddCommand(checkCommand) + + return localCommand +} + +func check(cmd *cobra.Command, args []string) error { + endpoint := viper.GetString("endpoint") + token := viper.GetString("token") + + Logger.Infoln("\n---\nCircleCI CLI Diagnostics\n---\n") + Logger.Infof("Config found: %v\n", viper.ConfigFileUsed()) + + Logger.Infof("GraphQL API endpoint: %s\n", endpoint) + + if token == "token" || token == "" { + return errors.New("please set a token") + } + Logger.Infoln("OK, got a token.") + Logger.Infof("Verbose mode: %v\n", viper.GetBool("verbose")) + + return nil +} + +func trimFirstRune(s string) string { + _, i := utf8.DecodeRuneInString(s) + return s[i:] +} + +func update(cmd *cobra.Command, args []string) error { + + url := "https://api.github.com/repos/CircleCI-Public/circleci-cli/releases/latest" + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return err + } + + req.Header.Set("User-Agent", version.UserAgent()) + + client := http.Client{} + res, err := client.Do(req) + if err != nil { + return err + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return err + } + + var release struct { + // There are other fields in this response that we could use to download the + // binaries on behalf of the user. + // https://developer.github.com/v3/repos/releases/#get-the-latest-release + HTML string `json:"html_url"` + Tag string `json:"tag_name"` + Published string `json:"published_at"` + } + + if err := json.Unmarshal(body, &release); err != nil { + return err + } + + latest := trimFirstRune(release.Tag) + + Logger.Debug("Latest version: %s", latest) + Logger.Debug("Published: %s", release.Published) + Logger.Debug("Current Version: %s", version.Version) + + if latest == version.Version { + Logger.Info("Already up-to-date.") + } else { + Logger.Infof("A new release is available (%s)", release.Tag) + Logger.Infof("You are running %s", version.Version) + Logger.Infof("You can download it from %s", release.HTML) + } + + return nil } // We can't properly run integration tests on code that calls PromptUI. diff --git a/cmd/local_test.go b/cmd/local_test.go new file mode 100644 index 000000000..7aeab44db --- /dev/null +++ b/cmd/local_test.go @@ -0,0 +1,181 @@ +package cmd_test + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Local", func() { + Describe("Check", func() { + var ( + tempHome string + command *exec.Cmd + ) + + BeforeEach(func() { + var err error + tempHome, err = ioutil.TempDir("", "circleci-cli-test-") + Expect(err).ToNot(HaveOccurred()) + + command = exec.Command(pathCLI, "local", "check") + command.Env = append(os.Environ(), + fmt.Sprintf("HOME=%s", tempHome), + ) + }) + + AfterEach(func() { + Expect(os.RemoveAll(tempHome)).To(Succeed()) + }) + + Describe("existing config file", func() { + var config *os.File + + BeforeEach(func() { + const ( + configDir = ".circleci" + configFile = "cli.yml" + ) + + Expect(os.Mkdir(filepath.Join(tempHome, configDir), 0700)).To(Succeed()) + + var err error + config, err = os.OpenFile( + filepath.Join(tempHome, configDir, configFile), + os.O_RDWR|os.O_CREATE, + 0600, + ) + Expect(err).ToNot(HaveOccurred()) + }) + + Describe("token and endpoint set in config file", func() { + BeforeEach(func() { + _, err := config.Write([]byte(` +endpoint: https://example.com/graphql +token: mytoken +`)) + Expect(err).ToNot(HaveOccurred()) + Expect(config.Close()).To(Succeed()) + }) + + It("print success", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err.Contents()).Should(BeEmpty()) + Eventually(session.Out).Should(gbytes.Say("GraphQL API endpoint: https://example.com/graphql")) + Eventually(session.Out).Should(gbytes.Say("OK, got a token.")) + Eventually(session).Should(gexec.Exit(0)) + }) + }) + + Context("token set to empty string in config file", func() { + BeforeEach(func() { + _, err := config.Write([]byte(` +endpoint: https://example.com/graphql +token: +`)) + Expect(err).ToNot(HaveOccurred()) + Expect(config.Close()).To(Succeed()) + }) + + It("print error", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err).Should(gbytes.Say("Error: please set a token")) + Eventually(session.Out).Should(gbytes.Say("GraphQL API endpoint: https://example.com/graphql")) + Eventually(session).Should(gexec.Exit(255)) + }) + }) + }) + }) + + Describe("Setup", func() { + var ( + tempHome string + command *exec.Cmd + ) + + BeforeEach(func() { + var err error + tempHome, err = ioutil.TempDir("", "circleci-cli-test-") + Expect(err).ToNot(HaveOccurred()) + + command = exec.Command(pathCLI, "local", "setup", "--testing") + command.Env = append(os.Environ(), + fmt.Sprintf("HOME=%s", tempHome), + fmt.Sprintf("USERPROFILE=%s", tempHome), // windows + ) + }) + + AfterEach(func() { + Expect(os.RemoveAll(tempHome)).To(Succeed()) + }) + + Describe("existing config file", func() { + var config *os.File + + BeforeEach(func() { + const ( + configDir = ".circleci" + configFile = "cli.yml" + ) + + Expect(os.Mkdir(filepath.Join(tempHome, configDir), 0700)).To(Succeed()) + + var err error + config, err = os.OpenFile( + filepath.Join(tempHome, configDir, configFile), + os.O_RDWR|os.O_CREATE, + 0600, + ) + Expect(err).ToNot(HaveOccurred()) + }) + + Describe("token and endpoint set in config file", func() { + + It("print success", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err.Contents()).Should(BeEmpty()) + + Eventually(session.Out).Should(gbytes.Say("CircleCI API Token")) + Eventually(session.Out).Should(gbytes.Say("API token has been set.")) + Eventually(session.Out).Should(gbytes.Say("CircleCI API End Point")) + Eventually(session.Out).Should(gbytes.Say("API endpoint has been set.")) + Eventually(session.Out).Should(gbytes.Say("Setup complete. Your configuration has been saved.")) + Eventually(session).Should(gexec.Exit(0)) + }) + }) + + Context("token set to some string in config file", func() { + BeforeEach(func() { + _, err := config.Write([]byte(` +endpoint: https://example.com/graphql +token: fooBarBaz +`)) + Expect(err).ToNot(HaveOccurred()) + Expect(config.Close()).To(Succeed()) + }) + + It("print error", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Out).Should(gbytes.Say("A CircleCI token is already set. Do you want to change it")) + Eventually(session.Out).Should(gbytes.Say("CircleCI API Token")) + Eventually(session.Out).Should(gbytes.Say("API token has been set.")) + Eventually(session.Out).Should(gbytes.Say("CircleCI API End Point")) + Eventually(session.Out).Should(gbytes.Say("API endpoint has been set.")) + Eventually(session.Out).Should(gbytes.Say("Setup complete. Your configuration has been saved.")) + Eventually(session).Should(gexec.Exit(0)) + }) + }) + }) + }) +}) diff --git a/cmd/root.go b/cmd/root.go index cd12536a3..fa38f426e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -37,14 +37,12 @@ func MakeCommands() *cobra.Command { Long: `Use CircleCI from the command line.`, } - rootCmd.AddCommand(newDiagnosticCommand()) rootCmd.AddCommand(newQueryCommand()) - rootCmd.AddCommand(newSetupCommand()) rootCmd.AddCommand(newConfigCommand()) rootCmd.AddCommand(newOrbCommand()) rootCmd.AddCommand(newBuildCommand()) rootCmd.AddCommand(newVersionCommand()) - rootCmd.AddCommand(newUpdateCommand()) + rootCmd.AddCommand(newLocalCommand()) rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose logging.") rootCmd.PersistentFlags().StringP("endpoint", "e", defaultEndpoint, "the endpoint of your CircleCI GraphQL API") rootCmd.PersistentFlags().StringP("token", "t", "", "your token for using CircleCI") diff --git a/cmd/root_test.go b/cmd/root_test.go index 27d3bf0e3..a42b042da 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -16,7 +16,7 @@ var _ = Describe("Root", func() { It("can create commands", func() { commands := cmd.MakeCommands() - Expect(len(commands.Commands())).To(Equal(8)) + Expect(len(commands.Commands())).To(Equal(6)) }) }) diff --git a/cmd/setup_test.go b/cmd/setup_test.go deleted file mode 100644 index aa42e9050..000000000 --- a/cmd/setup_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package cmd_test - -import ( - "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/onsi/gomega/gbytes" - "github.com/onsi/gomega/gexec" -) - -var _ = Describe("Setup", func() { - var ( - tempHome string - command *exec.Cmd - ) - - BeforeEach(func() { - var err error - tempHome, err = ioutil.TempDir("", "circleci-cli-test-") - Expect(err).ToNot(HaveOccurred()) - - command = exec.Command(pathCLI, "setup", "--testing") - command.Env = append(os.Environ(), - fmt.Sprintf("HOME=%s", tempHome), - fmt.Sprintf("USERPROFILE=%s", tempHome), // windows - ) - }) - - AfterEach(func() { - Expect(os.RemoveAll(tempHome)).To(Succeed()) - }) - - Describe("existing config file", func() { - var config *os.File - - BeforeEach(func() { - const ( - configDir = ".circleci" - configFile = "cli.yml" - ) - - Expect(os.Mkdir(filepath.Join(tempHome, configDir), 0700)).To(Succeed()) - - var err error - config, err = os.OpenFile( - filepath.Join(tempHome, configDir, configFile), - os.O_RDWR|os.O_CREATE, - 0600, - ) - Expect(err).ToNot(HaveOccurred()) - }) - - Describe("token and endpoint set in config file", func() { - - It("print success", func() { - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Err.Contents()).Should(BeEmpty()) - - Eventually(session.Out).Should(gbytes.Say("CircleCI API Token")) - Eventually(session.Out).Should(gbytes.Say("API token has been set.")) - Eventually(session.Out).Should(gbytes.Say("CircleCI API End Point")) - Eventually(session.Out).Should(gbytes.Say("API endpoint has been set.")) - Eventually(session.Out).Should(gbytes.Say("Setup complete. Your configuration has been saved.")) - Eventually(session).Should(gexec.Exit(0)) - }) - }) - - Context("token set to some string in config file", func() { - BeforeEach(func() { - _, err := config.Write([]byte(` -endpoint: https://example.com/graphql -token: fooBarBaz -`)) - Expect(err).ToNot(HaveOccurred()) - Expect(config.Close()).To(Succeed()) - }) - - It("print error", func() { - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Out).Should(gbytes.Say("A CircleCI token is already set. Do you want to change it")) - Eventually(session.Out).Should(gbytes.Say("CircleCI API Token")) - Eventually(session.Out).Should(gbytes.Say("API token has been set.")) - Eventually(session.Out).Should(gbytes.Say("CircleCI API End Point")) - Eventually(session.Out).Should(gbytes.Say("API endpoint has been set.")) - Eventually(session.Out).Should(gbytes.Say("Setup complete. Your configuration has been saved.")) - Eventually(session).Should(gexec.Exit(0)) - }) - }) - }) -}) diff --git a/cmd/update.go b/cmd/update.go deleted file mode 100644 index 82be450b9..000000000 --- a/cmd/update.go +++ /dev/null @@ -1,83 +0,0 @@ -package cmd - -import ( - "encoding/json" - "io/ioutil" - "net/http" - "unicode/utf8" - - "github.com/CircleCI-Public/circleci-cli/version" - "github.com/spf13/cobra" -) - -func newUpdateCommand() *cobra.Command { - update := &cobra.Command{ - Use: "update", - Short: "Update the tool", - } - - update.AddCommand(&cobra.Command{ - Use: "check", - Short: "Check if there are any updates available", - RunE: checkForUpdates, - }) - - return update -} - -func trimFirstRune(s string) string { - _, i := utf8.DecodeRuneInString(s) - return s[i:] -} - -func checkForUpdates(cmd *cobra.Command, args []string) error { - - url := "https://api.github.com/repos/CircleCI-Public/circleci-cli/releases/latest" - - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - return err - } - - req.Header.Set("User-Agent", version.UserAgent()) - - client := http.Client{} - res, err := client.Do(req) - if err != nil { - return err - } - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return err - } - - var release struct { - // There are other fields in this response that we could use to download the - // binaries on behalf of the user. - // https://developer.github.com/v3/repos/releases/#get-the-latest-release - HTML string `json:"html_url"` - Tag string `json:"tag_name"` - Published string `json:"published_at"` - } - - if err := json.Unmarshal(body, &release); err != nil { - return err - } - - latest := trimFirstRune(release.Tag) - - Logger.Debug("Latest version: %s", latest) - Logger.Debug("Published: %s", release.Published) - Logger.Debug("Current Version: %s", version.Version) - - if latest == version.Version { - Logger.Info("Already up-to-date.") - } else { - Logger.Infof("A new release is available (%s)", release.Tag) - Logger.Infof("You are running %s", version.Version) - Logger.Infof("You can download it from %s", release.HTML) - } - - return nil -} From c08f7efa3721589d1240af9a51a13a860d15b6da Mon Sep 17 00:00:00 2001 From: Zachary Scott Date: Wed, 1 Aug 2018 17:51:20 +0900 Subject: [PATCH 08/11] Add alias to old `circleci update` command for compatibility This command has moved to `circleci local update`. --- cmd/root.go | 1 + cmd/root_test.go | 2 +- cmd/update.go | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 cmd/update.go diff --git a/cmd/root.go b/cmd/root.go index fa38f426e..887d21244 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -43,6 +43,7 @@ func MakeCommands() *cobra.Command { rootCmd.AddCommand(newBuildCommand()) rootCmd.AddCommand(newVersionCommand()) rootCmd.AddCommand(newLocalCommand()) + rootCmd.AddCommand(newUpdateCommand()) rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose logging.") rootCmd.PersistentFlags().StringP("endpoint", "e", defaultEndpoint, "the endpoint of your CircleCI GraphQL API") rootCmd.PersistentFlags().StringP("token", "t", "", "your token for using CircleCI") diff --git a/cmd/root_test.go b/cmd/root_test.go index a42b042da..7df5eeadd 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -16,7 +16,7 @@ var _ = Describe("Root", func() { It("can create commands", func() { commands := cmd.MakeCommands() - Expect(len(commands.Commands())).To(Equal(6)) + Expect(len(commands.Commands())).To(Equal(7)) }) }) diff --git a/cmd/update.go b/cmd/update.go new file mode 100644 index 000000000..0c26e5204 --- /dev/null +++ b/cmd/update.go @@ -0,0 +1,16 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +func newUpdateCommand() *cobra.Command { + update := &cobra.Command{ + Use: "update", + Short: "Update the tool", + Hidden: true, + RunE: update, + } + + return update +} From 19f60ac9d9179e7efc120763d933b0eedf4599e8 Mon Sep 17 00:00:00 2001 From: Zachary Scott Date: Wed, 1 Aug 2018 18:07:22 +0900 Subject: [PATCH 09/11] Move `namespace` as a sub-command under new `registry` command This includes `namespace create`, so: $ circleci registry namespace create .... --- cmd/orb.go | 44 --------- cmd/orb_test.go | 187 ------------------------------------- cmd/registry.go | 55 +++++++++++ cmd/registry_test.go | 216 +++++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 1 + cmd/root_test.go | 2 +- 6 files changed, 273 insertions(+), 232 deletions(-) create mode 100644 cmd/registry.go create mode 100644 cmd/registry_test.go diff --git a/cmd/orb.go b/cmd/orb.go index 89ff008f3..330c79110 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -66,28 +66,6 @@ func newOrbCommand() *cobra.Command { Args: cobra.ExactArgs(1), } - createNamespace := &cobra.Command{ - Use: "create [name] [vcs] [org-name]", - Short: "create an orb namespace", - RunE: createOrbNamespace, - Args: cobra.ExactArgs(3), - } - - // "org-name", "", "organization name (required)" - // "vcs", "github", "organization vcs, e.g. 'github', 'bitbucket'" - - namespaceCommand := &cobra.Command{ - Use: "namespace", - Short: "Operate on orb namespaces (create, etc.)", - } - namespaceCommand.AddCommand(createNamespace) - nsCommand := &cobra.Command{ - Use: "ns", - Short: "Operate on orb namespaces (create, etc.)", - Hidden: true, - } - nsCommand.AddCommand(createNamespace) - orbCommand := &cobra.Command{ Use: "orb", Short: "Operate on orbs", @@ -95,13 +73,9 @@ func newOrbCommand() *cobra.Command { orbCommand.AddCommand(listCommand) orbCommand.AddCommand(orbCreate) - orbCommand.AddCommand(validateCommand) orbCommand.AddCommand(expandCommand) orbCommand.AddCommand(publishCommand) - - orbCommand.AddCommand(namespaceCommand) - orbCommand.AddCommand(nsCommand) orbCommand.AddCommand(sourceCommand) return orbCommand @@ -325,24 +299,6 @@ func createOrb(cmd *cobra.Command, args []string) error { return nil } -func createOrbNamespace(cmd *cobra.Command, args []string) error { - var err error - ctx := context.Background() - - response, err := api.CreateNamespace(ctx, Logger, args[0], args[2], strings.ToUpper(args[1])) - - if err != nil { - return err - } - - if len(response.Errors) > 0 { - return response.ToError() - } - - Logger.Info("Namespace created") - return nil -} - func showSource(cmd *cobra.Command, args []string) error { orb := args[0] source, err := api.OrbSource(context.Background(), Logger, orb) diff --git a/cmd/orb_test.go b/cmd/orb_test.go index 29c6430eb..c571412b3 100644 --- a/cmd/orb_test.go +++ b/cmd/orb_test.go @@ -452,193 +452,6 @@ var _ = Describe("Orb integration tests", func() { }) }) - Describe("when using the full namespace command", func() { - BeforeEach(func() { - command = exec.Command(pathCLI, - "orb", "namespace", "create", - "-t", token, - "-e", testServer.URL(), - "foo-ns", - "BITBUCKET", - "test-org", - ) - }) - - It("works with organizationName and organizationVcs", func() { - By("setting up a mock server") - - gqlOrganizationResponse := `{ - "organization": { - "name": "test-org", - "id": "bb604b45-b6b0-4b81-ad80-796f15eddf87" - } - }` - - expectedOrganizationRequest := `{ - "query": "\n\t\t\tquery($organizationName: String!, $organizationVcs: VCSType!) {\n\t\t\t\torganization(\n\t\t\t\t\tname: $organizationName\n\t\t\t\t\tvcsType: $organizationVcs\n\t\t\t\t) {\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t}", - "variables": { - "organizationName": "test-org", - "organizationVcs": "BITBUCKET" - } - }` - - gqlNsResponse := `{ - "createNamespace": { - "errors": [], - "namespace": { - "id": "bb604b45-b6b0-4b81-ad80-796f15eddf87" - } - } - }` - - expectedNsRequest := `{ - "query": "\n\t\t\tmutation($name: String!, $organizationId: UUID!) {\n\t\t\t\tcreateNamespace(\n\t\t\t\t\tname: $name,\n\t\t\t\t\torganizationId: $organizationId\n\t\t\t\t) {\n\t\t\t\t\tnamespace {\n\t\t\t\t\t\tid\n\t\t\t\t\t}\n\t\t\t\t\terrors {\n\t\t\t\t\t\tmessage\n\t\t\t\t\t\ttype\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}", - "variables": { - "name": "foo-ns", - "organizationId": "bb604b45-b6b0-4b81-ad80-796f15eddf87" - } - }` - - appendPostHandler(testServer, token, MockRequestResponse{ - Status: http.StatusOK, - Request: expectedOrganizationRequest, - Response: gqlOrganizationResponse}) - appendPostHandler(testServer, token, MockRequestResponse{ - Status: http.StatusOK, - Request: expectedNsRequest, - Response: gqlNsResponse}) - - By("running the command") - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Out).Should(gbytes.Say("Namespace created")) - Eventually(session).Should(gexec.Exit(0)) - }) - }) - - Describe("when creating / reserving a namespace", func() { - BeforeEach(func() { - command = exec.Command(pathCLI, - "orb", "ns", "create", - "-t", token, - "-e", testServer.URL(), - "foo-ns", - "BITBUCKET", - "test-org", - ) - }) - - It("works with organizationName and organizationVcs", func() { - By("setting up a mock server") - - gqlOrganizationResponse := `{ - "organization": { - "name": "test-org", - "id": "bb604b45-b6b0-4b81-ad80-796f15eddf87" - } - }` - - expectedOrganizationRequest := `{ - "query": "\n\t\t\tquery($organizationName: String!, $organizationVcs: VCSType!) {\n\t\t\t\torganization(\n\t\t\t\t\tname: $organizationName\n\t\t\t\t\tvcsType: $organizationVcs\n\t\t\t\t) {\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t}", - "variables": { - "organizationName": "test-org", - "organizationVcs": "BITBUCKET" - } - }` - - gqlNsResponse := `{ - "createNamespace": { - "errors": [], - "namespace": { - "id": "bb604b45-b6b0-4b81-ad80-796f15eddf87" - } - } - }` - - expectedNsRequest := `{ - "query": "\n\t\t\tmutation($name: String!, $organizationId: UUID!) {\n\t\t\t\tcreateNamespace(\n\t\t\t\t\tname: $name,\n\t\t\t\t\torganizationId: $organizationId\n\t\t\t\t) {\n\t\t\t\t\tnamespace {\n\t\t\t\t\t\tid\n\t\t\t\t\t}\n\t\t\t\t\terrors {\n\t\t\t\t\t\tmessage\n\t\t\t\t\t\ttype\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}", - "variables": { - "name": "foo-ns", - "organizationId": "bb604b45-b6b0-4b81-ad80-796f15eddf87" - } - }` - - appendPostHandler(testServer, token, MockRequestResponse{ - Status: http.StatusOK, - Request: expectedOrganizationRequest, - Response: gqlOrganizationResponse}) - appendPostHandler(testServer, token, MockRequestResponse{ - Status: http.StatusOK, - Request: expectedNsRequest, - Response: gqlNsResponse}) - - By("running the command") - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Out).Should(gbytes.Say("Namespace created")) - Eventually(session).Should(gexec.Exit(0)) - }) - - It("prints all errors returned by the GraphQL API", func() { - By("setting up a mock server") - - gqlOrganizationResponse := `{ - "organization": { - "name": "test-org", - "id": "bb604b45-b6b0-4b81-ad80-796f15eddf87" - } - }` - - expectedOrganizationRequest := `{ - "query": "\n\t\t\tquery($organizationName: String!, $organizationVcs: VCSType!) {\n\t\t\t\torganization(\n\t\t\t\t\tname: $organizationName\n\t\t\t\t\tvcsType: $organizationVcs\n\t\t\t\t) {\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t}", - "variables": { - "organizationName": "test-org", - "organizationVcs": "BITBUCKET" - } - }` - - gqlResponse := `{ - "createNamespace": { - "errors": [ - {"message": "error1"}, - {"message": "error2"} - ], - "namespace": null - } - }` - - expectedRequestJson := `{ - "query": "\n\t\t\tmutation($name: String!, $organizationId: UUID!) {\n\t\t\t\tcreateNamespace(\n\t\t\t\t\tname: $name,\n\t\t\t\t\torganizationId: $organizationId\n\t\t\t\t) {\n\t\t\t\t\tnamespace {\n\t\t\t\t\t\tid\n\t\t\t\t\t}\n\t\t\t\t\terrors {\n\t\t\t\t\t\tmessage\n\t\t\t\t\t\ttype\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}", - "variables": { - "name": "foo-ns", - "organizationId": "bb604b45-b6b0-4b81-ad80-796f15eddf87" - } - }` - - appendPostHandler(testServer, token, - MockRequestResponse{ - Status: http.StatusOK, - Request: expectedOrganizationRequest, - Response: gqlOrganizationResponse, - }) - appendPostHandler(testServer, token, - MockRequestResponse{ - Status: http.StatusOK, - Request: expectedRequestJson, - Response: gqlResponse, - }) - - By("running the command") - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Err).Should(gbytes.Say("Error: error1: error2")) - Eventually(session).ShouldNot(gexec.Exit(0)) - }) - }) - Describe("when creating / reserving an orb", func() { BeforeEach(func() { command = exec.Command(pathCLI, diff --git a/cmd/registry.go b/cmd/registry.go new file mode 100644 index 000000000..6168f61fd --- /dev/null +++ b/cmd/registry.go @@ -0,0 +1,55 @@ +package cmd + +import ( + "context" + "strings" + + "github.com/CircleCI-Public/circleci-cli/api" + "github.com/spf13/cobra" +) + +func newRegistryCommand() *cobra.Command { + registryCmd := &cobra.Command{ + Use: "registry", + Short: "Operate on the registry", + } + + namespaceCommand := &cobra.Command{ + Use: "namespace", + Short: "Operate on orb namespaces (create, etc.)", + } + + createNamespaceCommand := &cobra.Command{ + Use: "create [name] [vcs] [org-name]", + Short: "create an namespace", + RunE: createNamespace, + Args: cobra.ExactArgs(3), + } + + // "org-name", "", "organization name (required)" + // "vcs", "github", "organization vcs, e.g. 'github', 'bitbucket'" + + namespaceCommand.AddCommand(createNamespaceCommand) + + registryCmd.AddCommand(namespaceCommand) + + return registryCmd +} + +func createNamespace(cmd *cobra.Command, args []string) error { + var err error + ctx := context.Background() + + response, err := api.CreateNamespace(ctx, Logger, args[0], args[2], strings.ToUpper(args[1])) + + if err != nil { + return err + } + + if len(response.Errors) > 0 { + return response.ToError() + } + + Logger.Info("Namespace created") + return nil +} diff --git a/cmd/registry_test.go b/cmd/registry_test.go new file mode 100644 index 000000000..20395a1b6 --- /dev/null +++ b/cmd/registry_test.go @@ -0,0 +1,216 @@ +package cmd_test + +import ( + "net/http" + "os/exec" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/gexec" + "github.com/onsi/gomega/ghttp" +) + +var _ = Describe("Registry integration tests", func() { + var ( + testServer *ghttp.Server + token string = "testtoken" + command *exec.Cmd + ) + + BeforeEach(func() { + testServer = ghttp.NewServer() + }) + + AfterEach(func() { + testServer.Close() + }) + + Describe("registering a namespace", func() { + BeforeEach(func() { + command = exec.Command(pathCLI, + "registry", "namespace", "create", + "-t", token, + "-e", testServer.URL(), + "foo-ns", + "BITBUCKET", + "test-org", + ) + }) + + It("works with organizationName and organizationVcs", func() { + By("setting up a mock server") + + gqlOrganizationResponse := `{ + "organization": { + "name": "test-org", + "id": "bb604b45-b6b0-4b81-ad80-796f15eddf87" + } + }` + + expectedOrganizationRequest := `{ + "query": "\n\t\t\tquery($organizationName: String!, $organizationVcs: VCSType!) {\n\t\t\t\torganization(\n\t\t\t\t\tname: $organizationName\n\t\t\t\t\tvcsType: $organizationVcs\n\t\t\t\t) {\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t}", + "variables": { + "organizationName": "test-org", + "organizationVcs": "BITBUCKET" + } + }` + + gqlNsResponse := `{ + "createNamespace": { + "errors": [], + "namespace": { + "id": "bb604b45-b6b0-4b81-ad80-796f15eddf87" + } + } + }` + + expectedNsRequest := `{ + "query": "\n\t\t\tmutation($name: String!, $organizationId: UUID!) {\n\t\t\t\tcreateNamespace(\n\t\t\t\t\tname: $name,\n\t\t\t\t\torganizationId: $organizationId\n\t\t\t\t) {\n\t\t\t\t\tnamespace {\n\t\t\t\t\t\tid\n\t\t\t\t\t}\n\t\t\t\t\terrors {\n\t\t\t\t\t\tmessage\n\t\t\t\t\t\ttype\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}", + "variables": { + "name": "foo-ns", + "organizationId": "bb604b45-b6b0-4b81-ad80-796f15eddf87" + } + }` + + appendPostHandler(testServer, token, MockRequestResponse{ + Status: http.StatusOK, + Request: expectedOrganizationRequest, + Response: gqlOrganizationResponse}) + appendPostHandler(testServer, token, MockRequestResponse{ + Status: http.StatusOK, + Request: expectedNsRequest, + Response: gqlNsResponse}) + + By("running the command") + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Out).Should(gbytes.Say("Namespace created")) + Eventually(session).Should(gexec.Exit(0)) + }) + }) + + Describe("when creating / reserving a namespace", func() { + BeforeEach(func() { + command = exec.Command(pathCLI, + "registry", "namespace", "create", + "-t", token, + "-e", testServer.URL(), + "foo-ns", + "BITBUCKET", + "test-org", + ) + }) + + It("works with organizationName and organizationVcs", func() { + By("setting up a mock server") + + gqlOrganizationResponse := `{ + "organization": { + "name": "test-org", + "id": "bb604b45-b6b0-4b81-ad80-796f15eddf87" + } + }` + + expectedOrganizationRequest := `{ + "query": "\n\t\t\tquery($organizationName: String!, $organizationVcs: VCSType!) {\n\t\t\t\torganization(\n\t\t\t\t\tname: $organizationName\n\t\t\t\t\tvcsType: $organizationVcs\n\t\t\t\t) {\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t}", + "variables": { + "organizationName": "test-org", + "organizationVcs": "BITBUCKET" + } + }` + + gqlNsResponse := `{ + "createNamespace": { + "errors": [], + "namespace": { + "id": "bb604b45-b6b0-4b81-ad80-796f15eddf87" + } + } + }` + + expectedNsRequest := `{ + "query": "\n\t\t\tmutation($name: String!, $organizationId: UUID!) {\n\t\t\t\tcreateNamespace(\n\t\t\t\t\tname: $name,\n\t\t\t\t\torganizationId: $organizationId\n\t\t\t\t) {\n\t\t\t\t\tnamespace {\n\t\t\t\t\t\tid\n\t\t\t\t\t}\n\t\t\t\t\terrors {\n\t\t\t\t\t\tmessage\n\t\t\t\t\t\ttype\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}", + "variables": { + "name": "foo-ns", + "organizationId": "bb604b45-b6b0-4b81-ad80-796f15eddf87" + } + }` + + appendPostHandler(testServer, token, MockRequestResponse{ + Status: http.StatusOK, + Request: expectedOrganizationRequest, + Response: gqlOrganizationResponse}) + appendPostHandler(testServer, token, MockRequestResponse{ + Status: http.StatusOK, + Request: expectedNsRequest, + Response: gqlNsResponse}) + + By("running the command") + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Out).Should(gbytes.Say("Namespace created")) + Eventually(session).Should(gexec.Exit(0)) + }) + + It("prints all errors returned by the GraphQL API", func() { + By("setting up a mock server") + + gqlOrganizationResponse := `{ + "organization": { + "name": "test-org", + "id": "bb604b45-b6b0-4b81-ad80-796f15eddf87" + } + }` + + expectedOrganizationRequest := `{ + "query": "\n\t\t\tquery($organizationName: String!, $organizationVcs: VCSType!) {\n\t\t\t\torganization(\n\t\t\t\t\tname: $organizationName\n\t\t\t\t\tvcsType: $organizationVcs\n\t\t\t\t) {\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t}", + "variables": { + "organizationName": "test-org", + "organizationVcs": "BITBUCKET" + } + }` + + gqlResponse := `{ + "createNamespace": { + "errors": [ + {"message": "error1"}, + {"message": "error2"} + ], + "namespace": null + } + }` + + expectedRequestJson := `{ + "query": "\n\t\t\tmutation($name: String!, $organizationId: UUID!) {\n\t\t\t\tcreateNamespace(\n\t\t\t\t\tname: $name,\n\t\t\t\t\torganizationId: $organizationId\n\t\t\t\t) {\n\t\t\t\t\tnamespace {\n\t\t\t\t\t\tid\n\t\t\t\t\t}\n\t\t\t\t\terrors {\n\t\t\t\t\t\tmessage\n\t\t\t\t\t\ttype\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}", + "variables": { + "name": "foo-ns", + "organizationId": "bb604b45-b6b0-4b81-ad80-796f15eddf87" + } + }` + + appendPostHandler(testServer, token, + MockRequestResponse{ + Status: http.StatusOK, + Request: expectedOrganizationRequest, + Response: gqlOrganizationResponse, + }) + appendPostHandler(testServer, token, + MockRequestResponse{ + Status: http.StatusOK, + Request: expectedRequestJson, + Response: gqlResponse, + }) + + By("running the command") + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err).Should(gbytes.Say("Error: error1: error2")) + Eventually(session).ShouldNot(gexec.Exit(0)) + }) + }) + +}) diff --git a/cmd/root.go b/cmd/root.go index 887d21244..aba3f7948 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -44,6 +44,7 @@ func MakeCommands() *cobra.Command { rootCmd.AddCommand(newVersionCommand()) rootCmd.AddCommand(newLocalCommand()) rootCmd.AddCommand(newUpdateCommand()) + rootCmd.AddCommand(newRegistryCommand()) rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose logging.") rootCmd.PersistentFlags().StringP("endpoint", "e", defaultEndpoint, "the endpoint of your CircleCI GraphQL API") rootCmd.PersistentFlags().StringP("token", "t", "", "your token for using CircleCI") diff --git a/cmd/root_test.go b/cmd/root_test.go index 7df5eeadd..27d3bf0e3 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -16,7 +16,7 @@ var _ = Describe("Root", func() { It("can create commands", func() { commands := cmd.MakeCommands() - Expect(len(commands.Commands())).To(Equal(7)) + Expect(len(commands.Commands())).To(Equal(8)) }) }) From dc10296eda55a831afabe6633c3a9678cbc1054e Mon Sep 17 00:00:00 2001 From: Zachary Scott Date: Thu, 2 Aug 2018 13:19:38 +0900 Subject: [PATCH 10/11] Remove local command and move diagnostic and setup back --- cmd/diagnostic.go | 35 +++++++ cmd/diagnostic_test.go | 96 ++++++++++++++++++++ cmd/local_test.go | 181 ------------------------------------- cmd/root.go | 3 +- cmd/root_test.go | 2 +- cmd/{local.go => setup.go} | 105 +-------------------- cmd/setup_test.go | 97 ++++++++++++++++++++ cmd/update.go | 75 ++++++++++++++- 8 files changed, 304 insertions(+), 290 deletions(-) create mode 100644 cmd/diagnostic.go create mode 100644 cmd/diagnostic_test.go delete mode 100644 cmd/local_test.go rename cmd/{local.go => setup.go} (52%) create mode 100644 cmd/setup_test.go diff --git a/cmd/diagnostic.go b/cmd/diagnostic.go new file mode 100644 index 000000000..62e5d4dba --- /dev/null +++ b/cmd/diagnostic.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func newDiagnosticCommand() *cobra.Command { + diagnosticCommand := &cobra.Command{ + Use: "diagnostic", + Short: "Check the status of your CircleCI CLI.", + RunE: diagnostic, + } + + return diagnosticCommand +} + +func diagnostic(cmd *cobra.Command, args []string) error { + endpoint := viper.GetString("endpoint") + token := viper.GetString("token") + + Logger.Infoln("\n---\nCircleCI CLI Diagnostics\n---\n") + Logger.Infof("Config found: %v\n", viper.ConfigFileUsed()) + + Logger.Infof("GraphQL API endpoint: %s\n", endpoint) + + if token == "token" || token == "" { + return errors.New("please set a token") + } + Logger.Infoln("OK, got a token.") + Logger.Infof("Verbose mode: %v\n", viper.GetBool("verbose")) + + return nil +} diff --git a/cmd/diagnostic_test.go b/cmd/diagnostic_test.go new file mode 100644 index 000000000..9da949bc9 --- /dev/null +++ b/cmd/diagnostic_test.go @@ -0,0 +1,96 @@ +package cmd_test + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Diagnostic", func() { + var ( + tempHome string + command *exec.Cmd + ) + + BeforeEach(func() { + var err error + tempHome, err = ioutil.TempDir("", "circleci-cli-test-") + Expect(err).ToNot(HaveOccurred()) + + command = exec.Command(pathCLI, "diagnostic") + command.Env = append(os.Environ(), + fmt.Sprintf("HOME=%s", tempHome), + ) + }) + + AfterEach(func() { + Expect(os.RemoveAll(tempHome)).To(Succeed()) + }) + + Describe("existing config file", func() { + var config *os.File + + BeforeEach(func() { + const ( + configDir = ".circleci" + configFile = "cli.yml" + ) + + Expect(os.Mkdir(filepath.Join(tempHome, configDir), 0700)).To(Succeed()) + + var err error + config, err = os.OpenFile( + filepath.Join(tempHome, configDir, configFile), + os.O_RDWR|os.O_CREATE, + 0600, + ) + Expect(err).ToNot(HaveOccurred()) + }) + + Describe("token and endpoint set in config file", func() { + BeforeEach(func() { + _, err := config.Write([]byte(` +endpoint: https://example.com/graphql +token: mytoken +`)) + Expect(err).ToNot(HaveOccurred()) + Expect(config.Close()).To(Succeed()) + }) + + It("print success", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err.Contents()).Should(BeEmpty()) + Eventually(session.Out).Should(gbytes.Say("GraphQL API endpoint: https://example.com/graphql")) + Eventually(session.Out).Should(gbytes.Say("OK, got a token.")) + Eventually(session).Should(gexec.Exit(0)) + }) + }) + + Context("token set to empty string in config file", func() { + BeforeEach(func() { + _, err := config.Write([]byte(` +endpoint: https://example.com/graphql +token: +`)) + Expect(err).ToNot(HaveOccurred()) + Expect(config.Close()).To(Succeed()) + }) + + It("print error", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err).Should(gbytes.Say("Error: please set a token")) + Eventually(session.Out).Should(gbytes.Say("GraphQL API endpoint: https://example.com/graphql")) + Eventually(session).Should(gexec.Exit(255)) + }) + }) + }) +}) diff --git a/cmd/local_test.go b/cmd/local_test.go deleted file mode 100644 index 7aeab44db..000000000 --- a/cmd/local_test.go +++ /dev/null @@ -1,181 +0,0 @@ -package cmd_test - -import ( - "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/onsi/gomega/gbytes" - "github.com/onsi/gomega/gexec" -) - -var _ = Describe("Local", func() { - Describe("Check", func() { - var ( - tempHome string - command *exec.Cmd - ) - - BeforeEach(func() { - var err error - tempHome, err = ioutil.TempDir("", "circleci-cli-test-") - Expect(err).ToNot(HaveOccurred()) - - command = exec.Command(pathCLI, "local", "check") - command.Env = append(os.Environ(), - fmt.Sprintf("HOME=%s", tempHome), - ) - }) - - AfterEach(func() { - Expect(os.RemoveAll(tempHome)).To(Succeed()) - }) - - Describe("existing config file", func() { - var config *os.File - - BeforeEach(func() { - const ( - configDir = ".circleci" - configFile = "cli.yml" - ) - - Expect(os.Mkdir(filepath.Join(tempHome, configDir), 0700)).To(Succeed()) - - var err error - config, err = os.OpenFile( - filepath.Join(tempHome, configDir, configFile), - os.O_RDWR|os.O_CREATE, - 0600, - ) - Expect(err).ToNot(HaveOccurred()) - }) - - Describe("token and endpoint set in config file", func() { - BeforeEach(func() { - _, err := config.Write([]byte(` -endpoint: https://example.com/graphql -token: mytoken -`)) - Expect(err).ToNot(HaveOccurred()) - Expect(config.Close()).To(Succeed()) - }) - - It("print success", func() { - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Err.Contents()).Should(BeEmpty()) - Eventually(session.Out).Should(gbytes.Say("GraphQL API endpoint: https://example.com/graphql")) - Eventually(session.Out).Should(gbytes.Say("OK, got a token.")) - Eventually(session).Should(gexec.Exit(0)) - }) - }) - - Context("token set to empty string in config file", func() { - BeforeEach(func() { - _, err := config.Write([]byte(` -endpoint: https://example.com/graphql -token: -`)) - Expect(err).ToNot(HaveOccurred()) - Expect(config.Close()).To(Succeed()) - }) - - It("print error", func() { - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Err).Should(gbytes.Say("Error: please set a token")) - Eventually(session.Out).Should(gbytes.Say("GraphQL API endpoint: https://example.com/graphql")) - Eventually(session).Should(gexec.Exit(255)) - }) - }) - }) - }) - - Describe("Setup", func() { - var ( - tempHome string - command *exec.Cmd - ) - - BeforeEach(func() { - var err error - tempHome, err = ioutil.TempDir("", "circleci-cli-test-") - Expect(err).ToNot(HaveOccurred()) - - command = exec.Command(pathCLI, "local", "setup", "--testing") - command.Env = append(os.Environ(), - fmt.Sprintf("HOME=%s", tempHome), - fmt.Sprintf("USERPROFILE=%s", tempHome), // windows - ) - }) - - AfterEach(func() { - Expect(os.RemoveAll(tempHome)).To(Succeed()) - }) - - Describe("existing config file", func() { - var config *os.File - - BeforeEach(func() { - const ( - configDir = ".circleci" - configFile = "cli.yml" - ) - - Expect(os.Mkdir(filepath.Join(tempHome, configDir), 0700)).To(Succeed()) - - var err error - config, err = os.OpenFile( - filepath.Join(tempHome, configDir, configFile), - os.O_RDWR|os.O_CREATE, - 0600, - ) - Expect(err).ToNot(HaveOccurred()) - }) - - Describe("token and endpoint set in config file", func() { - - It("print success", func() { - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Err.Contents()).Should(BeEmpty()) - - Eventually(session.Out).Should(gbytes.Say("CircleCI API Token")) - Eventually(session.Out).Should(gbytes.Say("API token has been set.")) - Eventually(session.Out).Should(gbytes.Say("CircleCI API End Point")) - Eventually(session.Out).Should(gbytes.Say("API endpoint has been set.")) - Eventually(session.Out).Should(gbytes.Say("Setup complete. Your configuration has been saved.")) - Eventually(session).Should(gexec.Exit(0)) - }) - }) - - Context("token set to some string in config file", func() { - BeforeEach(func() { - _, err := config.Write([]byte(` -endpoint: https://example.com/graphql -token: fooBarBaz -`)) - Expect(err).ToNot(HaveOccurred()) - Expect(config.Close()).To(Succeed()) - }) - - It("print error", func() { - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - Expect(err).ShouldNot(HaveOccurred()) - Eventually(session.Out).Should(gbytes.Say("A CircleCI token is already set. Do you want to change it")) - Eventually(session.Out).Should(gbytes.Say("CircleCI API Token")) - Eventually(session.Out).Should(gbytes.Say("API token has been set.")) - Eventually(session.Out).Should(gbytes.Say("CircleCI API End Point")) - Eventually(session.Out).Should(gbytes.Say("API endpoint has been set.")) - Eventually(session.Out).Should(gbytes.Say("Setup complete. Your configuration has been saved.")) - Eventually(session).Should(gexec.Exit(0)) - }) - }) - }) - }) -}) diff --git a/cmd/root.go b/cmd/root.go index aba3f7948..83ec66c7e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -42,7 +42,8 @@ func MakeCommands() *cobra.Command { rootCmd.AddCommand(newOrbCommand()) rootCmd.AddCommand(newBuildCommand()) rootCmd.AddCommand(newVersionCommand()) - rootCmd.AddCommand(newLocalCommand()) + rootCmd.AddCommand(newDiagnosticCommand()) + rootCmd.AddCommand(newSetupCommand()) rootCmd.AddCommand(newUpdateCommand()) rootCmd.AddCommand(newRegistryCommand()) rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose logging.") diff --git a/cmd/root_test.go b/cmd/root_test.go index 27d3bf0e3..52af4e799 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -16,7 +16,7 @@ var _ = Describe("Root", func() { It("can create commands", func() { commands := cmd.MakeCommands() - Expect(len(commands.Commands())).To(Equal(8)) + Expect(len(commands.Commands())).To(Equal(9)) }) }) diff --git a/cmd/local.go b/cmd/setup.go similarity index 52% rename from cmd/local.go rename to cmd/setup.go index 55b2ca775..31bcf513b 100644 --- a/cmd/local.go +++ b/cmd/setup.go @@ -1,13 +1,8 @@ package cmd import ( - "encoding/json" - "io/ioutil" - "net/http" "strings" - "unicode/utf8" - "github.com/CircleCI-Public/circleci-cli/version" "github.com/manifoldco/promptui" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -16,12 +11,7 @@ import ( var testing = false -func newLocalCommand() *cobra.Command { - localCommand := &cobra.Command{ - Use: "local", - Short: "Operate on your local CircleCI CLI", - } - +func newSetupCommand() *cobra.Command { setupCommand := &cobra.Command{ Use: "setup", Short: "Setup the CLI with your credentials", @@ -33,98 +23,7 @@ func newLocalCommand() *cobra.Command { panic(err) } - updateCommand := &cobra.Command{ - Use: "update", - Short: "Update the tool", - RunE: update, - } - - checkCommand := &cobra.Command{ - Use: "check", - Short: "Check the status of your CircleCI CLI.", - RunE: check, - } - - localCommand.AddCommand(setupCommand) - localCommand.AddCommand(updateCommand) - localCommand.AddCommand(checkCommand) - - return localCommand -} - -func check(cmd *cobra.Command, args []string) error { - endpoint := viper.GetString("endpoint") - token := viper.GetString("token") - - Logger.Infoln("\n---\nCircleCI CLI Diagnostics\n---\n") - Logger.Infof("Config found: %v\n", viper.ConfigFileUsed()) - - Logger.Infof("GraphQL API endpoint: %s\n", endpoint) - - if token == "token" || token == "" { - return errors.New("please set a token") - } - Logger.Infoln("OK, got a token.") - Logger.Infof("Verbose mode: %v\n", viper.GetBool("verbose")) - - return nil -} - -func trimFirstRune(s string) string { - _, i := utf8.DecodeRuneInString(s) - return s[i:] -} - -func update(cmd *cobra.Command, args []string) error { - - url := "https://api.github.com/repos/CircleCI-Public/circleci-cli/releases/latest" - - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - return err - } - - req.Header.Set("User-Agent", version.UserAgent()) - - client := http.Client{} - res, err := client.Do(req) - if err != nil { - return err - } - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return err - } - - var release struct { - // There are other fields in this response that we could use to download the - // binaries on behalf of the user. - // https://developer.github.com/v3/repos/releases/#get-the-latest-release - HTML string `json:"html_url"` - Tag string `json:"tag_name"` - Published string `json:"published_at"` - } - - if err := json.Unmarshal(body, &release); err != nil { - return err - } - - latest := trimFirstRune(release.Tag) - - Logger.Debug("Latest version: %s", latest) - Logger.Debug("Published: %s", release.Published) - Logger.Debug("Current Version: %s", version.Version) - - if latest == version.Version { - Logger.Info("Already up-to-date.") - } else { - Logger.Infof("A new release is available (%s)", release.Tag) - Logger.Infof("You are running %s", version.Version) - Logger.Infof("You can download it from %s", release.HTML) - } - - return nil + return setupCommand } // We can't properly run integration tests on code that calls PromptUI. diff --git a/cmd/setup_test.go b/cmd/setup_test.go new file mode 100644 index 000000000..aa42e9050 --- /dev/null +++ b/cmd/setup_test.go @@ -0,0 +1,97 @@ +package cmd_test + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Setup", func() { + var ( + tempHome string + command *exec.Cmd + ) + + BeforeEach(func() { + var err error + tempHome, err = ioutil.TempDir("", "circleci-cli-test-") + Expect(err).ToNot(HaveOccurred()) + + command = exec.Command(pathCLI, "setup", "--testing") + command.Env = append(os.Environ(), + fmt.Sprintf("HOME=%s", tempHome), + fmt.Sprintf("USERPROFILE=%s", tempHome), // windows + ) + }) + + AfterEach(func() { + Expect(os.RemoveAll(tempHome)).To(Succeed()) + }) + + Describe("existing config file", func() { + var config *os.File + + BeforeEach(func() { + const ( + configDir = ".circleci" + configFile = "cli.yml" + ) + + Expect(os.Mkdir(filepath.Join(tempHome, configDir), 0700)).To(Succeed()) + + var err error + config, err = os.OpenFile( + filepath.Join(tempHome, configDir, configFile), + os.O_RDWR|os.O_CREATE, + 0600, + ) + Expect(err).ToNot(HaveOccurred()) + }) + + Describe("token and endpoint set in config file", func() { + + It("print success", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err.Contents()).Should(BeEmpty()) + + Eventually(session.Out).Should(gbytes.Say("CircleCI API Token")) + Eventually(session.Out).Should(gbytes.Say("API token has been set.")) + Eventually(session.Out).Should(gbytes.Say("CircleCI API End Point")) + Eventually(session.Out).Should(gbytes.Say("API endpoint has been set.")) + Eventually(session.Out).Should(gbytes.Say("Setup complete. Your configuration has been saved.")) + Eventually(session).Should(gexec.Exit(0)) + }) + }) + + Context("token set to some string in config file", func() { + BeforeEach(func() { + _, err := config.Write([]byte(` +endpoint: https://example.com/graphql +token: fooBarBaz +`)) + Expect(err).ToNot(HaveOccurred()) + Expect(config.Close()).To(Succeed()) + }) + + It("print error", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Out).Should(gbytes.Say("A CircleCI token is already set. Do you want to change it")) + Eventually(session.Out).Should(gbytes.Say("CircleCI API Token")) + Eventually(session.Out).Should(gbytes.Say("API token has been set.")) + Eventually(session.Out).Should(gbytes.Say("CircleCI API End Point")) + Eventually(session.Out).Should(gbytes.Say("API endpoint has been set.")) + Eventually(session.Out).Should(gbytes.Say("Setup complete. Your configuration has been saved.")) + Eventually(session).Should(gexec.Exit(0)) + }) + }) + }) +}) diff --git a/cmd/update.go b/cmd/update.go index 0c26e5204..82be450b9 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -1,16 +1,83 @@ package cmd import ( + "encoding/json" + "io/ioutil" + "net/http" + "unicode/utf8" + + "github.com/CircleCI-Public/circleci-cli/version" "github.com/spf13/cobra" ) func newUpdateCommand() *cobra.Command { update := &cobra.Command{ - Use: "update", - Short: "Update the tool", - Hidden: true, - RunE: update, + Use: "update", + Short: "Update the tool", } + update.AddCommand(&cobra.Command{ + Use: "check", + Short: "Check if there are any updates available", + RunE: checkForUpdates, + }) + return update } + +func trimFirstRune(s string) string { + _, i := utf8.DecodeRuneInString(s) + return s[i:] +} + +func checkForUpdates(cmd *cobra.Command, args []string) error { + + url := "https://api.github.com/repos/CircleCI-Public/circleci-cli/releases/latest" + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return err + } + + req.Header.Set("User-Agent", version.UserAgent()) + + client := http.Client{} + res, err := client.Do(req) + if err != nil { + return err + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return err + } + + var release struct { + // There are other fields in this response that we could use to download the + // binaries on behalf of the user. + // https://developer.github.com/v3/repos/releases/#get-the-latest-release + HTML string `json:"html_url"` + Tag string `json:"tag_name"` + Published string `json:"published_at"` + } + + if err := json.Unmarshal(body, &release); err != nil { + return err + } + + latest := trimFirstRune(release.Tag) + + Logger.Debug("Latest version: %s", latest) + Logger.Debug("Published: %s", release.Published) + Logger.Debug("Current Version: %s", version.Version) + + if latest == version.Version { + Logger.Info("Already up-to-date.") + } else { + Logger.Infof("A new release is available (%s)", release.Tag) + Logger.Infof("You are running %s", version.Version) + Logger.Infof("You can download it from %s", release.HTML) + } + + return nil +} From e8f77a8d00ff6a94842181f05232804a687fbb43 Mon Sep 17 00:00:00 2001 From: Zachary Scott Date: Thu, 2 Aug 2018 13:25:35 +0900 Subject: [PATCH 11/11] Make `namespace` a top-level command We call namespaces RegistryNamespace in the API because they live inside the registry. Users of the CLI don't have to care where namespaces are stored, and there isn't a great argument for keeping `registry` at the top-level which further nests namespaces. --- cmd/{registry.go => namespace.go} | 19 ++++++------------- cmd/{registry_test.go => namespace_test.go} | 6 +++--- cmd/root.go | 2 +- 3 files changed, 10 insertions(+), 17 deletions(-) rename cmd/{registry.go => namespace.go} (66%) rename cmd/{registry_test.go => namespace_test.go} (98%) diff --git a/cmd/registry.go b/cmd/namespace.go similarity index 66% rename from cmd/registry.go rename to cmd/namespace.go index 6168f61fd..32bbece45 100644 --- a/cmd/registry.go +++ b/cmd/namespace.go @@ -8,18 +8,13 @@ import ( "github.com/spf13/cobra" ) -func newRegistryCommand() *cobra.Command { - registryCmd := &cobra.Command{ - Use: "registry", - Short: "Operate on the registry", - } - - namespaceCommand := &cobra.Command{ +func newNamespaceCommand() *cobra.Command { + namespaceCmd := &cobra.Command{ Use: "namespace", - Short: "Operate on orb namespaces (create, etc.)", + Short: "Operate on namespaces", } - createNamespaceCommand := &cobra.Command{ + createCmd := &cobra.Command{ Use: "create [name] [vcs] [org-name]", Short: "create an namespace", RunE: createNamespace, @@ -29,11 +24,9 @@ func newRegistryCommand() *cobra.Command { // "org-name", "", "organization name (required)" // "vcs", "github", "organization vcs, e.g. 'github', 'bitbucket'" - namespaceCommand.AddCommand(createNamespaceCommand) - - registryCmd.AddCommand(namespaceCommand) + namespaceCmd.AddCommand(createCmd) - return registryCmd + return namespaceCmd } func createNamespace(cmd *cobra.Command, args []string) error { diff --git a/cmd/registry_test.go b/cmd/namespace_test.go similarity index 98% rename from cmd/registry_test.go rename to cmd/namespace_test.go index 20395a1b6..50e3e4603 100644 --- a/cmd/registry_test.go +++ b/cmd/namespace_test.go @@ -11,7 +11,7 @@ import ( "github.com/onsi/gomega/ghttp" ) -var _ = Describe("Registry integration tests", func() { +var _ = Describe("Namespace integration tests", func() { var ( testServer *ghttp.Server token string = "testtoken" @@ -29,7 +29,7 @@ var _ = Describe("Registry integration tests", func() { Describe("registering a namespace", func() { BeforeEach(func() { command = exec.Command(pathCLI, - "registry", "namespace", "create", + "namespace", "create", "-t", token, "-e", testServer.URL(), "foo-ns", @@ -94,7 +94,7 @@ var _ = Describe("Registry integration tests", func() { Describe("when creating / reserving a namespace", func() { BeforeEach(func() { command = exec.Command(pathCLI, - "registry", "namespace", "create", + "namespace", "create", "-t", token, "-e", testServer.URL(), "foo-ns", diff --git a/cmd/root.go b/cmd/root.go index 83ec66c7e..498f2481b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -45,7 +45,7 @@ func MakeCommands() *cobra.Command { rootCmd.AddCommand(newDiagnosticCommand()) rootCmd.AddCommand(newSetupCommand()) rootCmd.AddCommand(newUpdateCommand()) - rootCmd.AddCommand(newRegistryCommand()) + rootCmd.AddCommand(newNamespaceCommand()) rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose logging.") rootCmd.PersistentFlags().StringP("endpoint", "e", defaultEndpoint, "the endpoint of your CircleCI GraphQL API") rootCmd.PersistentFlags().StringP("token", "t", "", "your token for using CircleCI")