From 6fcd7a691edff685eaae993a2bf92aa5cfadb1bf Mon Sep 17 00:00:00 2001 From: elliotforbes Date: Tue, 25 Mar 2025 17:09:32 +0000 Subject: [PATCH 1/2] Removes the now redundant homebrew deploy jobs from our CI pipeline As part of my efforts to get config validation working with the newly exposed pipeline values, I noted that the homebrew deploy job fails due to this error: ``` Whoops, the ******** formula has its version update pull requests automatically opened by BrewTestBot every ~3 hours! We'd still love your contributions, though, so try another one that's not in the autobump list: ``` As such, it makes sense to now remove this job and the shell script and simplify our CI. As part of this change, I've modified the README to include the new instructions as to how a release is picked up by homebrew from our main branch. --- .circleci/brew-deploy.sh | 11 ----------- .circleci/config.yml | 29 ----------------------------- README.md | 8 ++++++-- 3 files changed, 6 insertions(+), 42 deletions(-) delete mode 100755 .circleci/brew-deploy.sh diff --git a/.circleci/brew-deploy.sh b/.circleci/brew-deploy.sh deleted file mode 100755 index d352dc23c..000000000 --- a/.circleci/brew-deploy.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -set -e - -# Install the latest circleci from homebrew -brew update - -VERSION=$("$DESTDIR"/circleci version) -TAG="v$(ruby -e "puts '$VERSION'.split(/[ +]/)[0]")" -REVISION=$(git rev-parse "$(ruby -e "puts '$VERSION'.split(/[ +]/)[1]")") -echo "Bumping circleci to $TAG+$REVISION" -brew bump-formula-pr --strict --tag="$TAG" --revision="$REVISION" circleci diff --git a/.circleci/config.yml b/.circleci/config.yml index 9a51e29fc..422f23bda 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -265,25 +265,6 @@ jobs: echo $SNAPCRAFT_LOGIN_FILE | base64 --decode --ignore-garbage > .snapcraft/snapcraft.cfg snapcraft push *.snap --release stable - brew-deploy: - executor: mac - environment: - USER: circleci - TRAVIS: circleci - DESTDIR: /Users/distiller/dest - steps: - - checkout - - force-http-1 - - run: | - mkdir $DESTDIR - curl -fLSs https://circle.ci/cli | DESTDIR="$DESTDIR" bash - - run: | - git config --global user.email "$GH_EMAIL" > /dev/null 2>&1 - git config --global user.name "$GH_NAME" > /dev/null 2>&1 - - run: brew --version - - run: brew tap --force homebrew/core - - run: ./.circleci/brew-deploy.sh - chocolatey-deploy: executor: windows/default steps: @@ -368,16 +349,6 @@ workflows: - chocolatey-deploy: requires: - deploy - # Only deploy to homebrew after manual approval. - - run-brew-deploy-gate: - type: approval - requires: - - deploy - - brew-deploy: - requires: - - run-brew-deploy-gate - context: - - devex-release - deploy: requires: - cucumber diff --git a/README.md b/README.md index b318a9f3a..cb4a887cd 100644 --- a/README.md +++ b/README.md @@ -160,9 +160,13 @@ We publish the tool to [Homebrew](https://brew.sh/). The tool is [part of `homeb The particular considerations that we make are: - 1. Since Homebrew [doesn't "like tools that upgrade themselves"](https://docs.brew.sh/Acceptable-Formulae#we-dont-like-tools-that-upgrade-themselves), we disable the `circleci update` command when the tool is released through homebrew. We do this by [defining the PackageManager](https://github.com/Homebrew/homebrew-core/blob/eb1fdb84e2924289bcc8c85ee45081bf83dc024d/Formula/circleci.rb#L28) constant to `homebrew`, which allows us to [disable the `update` command at runtime](https://github.com/CircleCI-Public/circleci-cli/blob/67c7d52bace63846f87a1ed79f67f257c94a55b4/cmd/root.go#L119-L123). -1. We want to avoid every push to `main` from creating a Pull Request to the `circleci` formula on Homebrew. We want to avoid overloading the Homebrew team with pull requests to update our formula for small changes (changes to docs or other files that don't change functionality in the tool). + +#### Releasing to Homebrew + +This project is on Homebrew's special [autobump list](https://github.com/Homebrew/homebrew-core/blob/master/.github/autobump.txt) which effectively means that it will check our `main` branch every 3 hours for updates and create a PR automagically if there are any changes. This is great, but you do have to monitor the generated PRs to ensure they pass and do get merged in successfully. The PRs will be raised in this repo: [github.com/Homebrew/homebrew-core](https://github.com/Homebrew/homebrew-core) and you can search the Pull requests for `circleci` to see the generated PRs. + +Upon successful merge, you'll be able to upgrade the tool by running `brew upgrade circleci` and then you can validate any changes you may have made. ### Snap From e6308b5d99fb6f183cb9b52f6ca906eff600b12c Mon Sep 17 00:00:00 2001 From: Joe Del Nano Date: Sun, 13 Apr 2025 18:35:12 -0700 Subject: [PATCH 2/2] Add json flag to remaining table.Render-ing cmds --- cmd/context.go | 46 ++++++++++---- cmd/create_telemetry_test.go | 6 +- cmd/info/info.go | 38 +++++++++--- cmd/info/info_test.go | 2 +- cmd/project/environment_variable.go | 60 ++++++++++++++++--- cmd/runner/instance.go | 26 ++++++-- .../runner/instance-expected-usage.txt | 3 + .../runner/instance/list-expected-usage.txt | 5 +- .../testdata/runner/token-expected-usage.txt | 3 + .../runner/token/create-expected-usage.txt | 5 +- .../runner/token/delete-expected-usage.txt | 5 +- .../runner/token/list-expected-usage.txt | 5 +- cmd/runner/token.go | 28 +++++++-- 13 files changed, 188 insertions(+), 44 deletions(-) diff --git a/cmd/context.go b/cmd/context.go index f359885ee..1012b76f9 100644 --- a/cmd/context.go +++ b/cmd/context.go @@ -1,6 +1,7 @@ package cmd import ( + "encoding/json" "fmt" "io" "os" @@ -67,6 +68,8 @@ func newContextCommand(config *settings.Config) *cobra.Command { return validateToken(config) } + jsonFormat := false + command := &cobra.Command{ Use: "context", Long: `Contexts provide a mechanism for securing and sharing environment variables across @@ -94,13 +97,14 @@ are injected at runtime.`, return err } - return listContexts(contextClient, org.Organization.Name, org.Organization.ID) + return listContexts(cmd, contextClient, org.Organization.Name, org.Organization.ID) }, Args: MultiExactArgs(0, 2), Example: `circleci context list --org-id 00000000-0000-0000-0000-000000000000 (deprecated usage) circleci context list `, } listCommand.Flags().StringVar(&orgID, "org-id", "", orgIDUsage) + listCommand.Flags().BoolVar(&jsonFormat, "json", false, "Return output back in JSON format") showContextCommand := &cobra.Command{ Short: "Show a context", @@ -206,23 +210,41 @@ are injected at runtime.`, return command } -func listContexts(contextClient context.ContextInterface, orgName string, orgId string) error { +func listContexts(cmd *cobra.Command, contextClient context.ContextInterface, orgName string, orgId string) error { contexts, err := contextClient.Contexts() if err != nil { return err } - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"Organization", "Org ID", "Name", "Created At"}) - for _, context := range contexts { - table.Append([]string{ - orgName, - orgId, - context.Name, - context.CreatedAt.Format(time.RFC3339), - }) + jsonVal, err := cmd.Flags().GetBool("json") + if err != nil { + return err } - table.Render() + + if jsonVal { + // return JSON formatted for output + jsonCtxs, err := json.Marshal(contexts) + if err != nil { + return err + } + jsonWriter := cmd.OutOrStdout() + if _, err := jsonWriter.Write(jsonCtxs); err != nil { + return err + } + } else { + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"Organization", "Org ID", "Name", "Created At"}) + for _, context := range contexts { + table.Append([]string{ + orgName, + orgId, + context.Name, + context.CreatedAt.Format(time.RFC3339), + }) + } + table.Render() + } + return nil } diff --git a/cmd/create_telemetry_test.go b/cmd/create_telemetry_test.go index 92b5b9e9f..c8c06d970 100644 --- a/cmd/create_telemetry_test.go +++ b/cmd/create_telemetry_test.go @@ -60,12 +60,14 @@ func TestLoadTelemetrySettings(t *testing.T) { UniqueID: uniqueId, }, telemetryEvents: []telemetry.Event{ - {Object: "cli-telemetry", Action: "enabled", + { + Object: "cli-telemetry", Action: "enabled", Properties: map[string]interface{}{ "UUID": uniqueId, "user_id": userId, "is_self_hosted": false, - }}, + }, + }, }, }, }, diff --git a/cmd/info/info.go b/cmd/info/info.go index fce0bad59..330a95b57 100644 --- a/cmd/info/info.go +++ b/cmd/info/info.go @@ -1,6 +1,8 @@ package info import ( + "encoding/json" + "github.com/CircleCI-Public/circleci-cli/api/info" "github.com/CircleCI-Public/circleci-cli/cmd/validator" "github.com/CircleCI-Public/circleci-cli/settings" @@ -20,6 +22,8 @@ type infoOptions struct { func NewInfoCommand(config *settings.Config, preRunE validator.Validator) *cobra.Command { client, _ := info.NewInfoClient(*config) + jsonFormat := false + opts := infoOptions{ cfg: config, validator: preRunE, @@ -29,6 +33,8 @@ func NewInfoCommand(config *settings.Config, preRunE validator.Validator) *cobra Short: "Check information associated to your user account.", } orgInfoCmd := orgInfoCommand(client, opts) + orgInfoCmd.PersistentFlags().BoolVar(&jsonFormat, "json", false, + "Return output back in JSON format") infoCommand.AddCommand(orgInfoCmd) return infoCommand @@ -63,15 +69,33 @@ func getOrgInformation(cmd *cobra.Command, client info.InfoClient) error { return err } - table := tablewriter.NewWriter(cmd.OutOrStdout()) + jsonVal, err := cmd.Flags().GetBool("json") + if err != nil { + return err + } - table.SetHeader([]string{"ID", "Name"}) + if jsonVal { + // return JSON formatted for output + jsonResp, err := json.Marshal(resp) + if err != nil { + return err + } + jsonWriter := cmd.OutOrStdout() + if _, err := jsonWriter.Write(jsonResp); err != nil { + return err + } + } else { + table := tablewriter.NewWriter(cmd.OutOrStdout()) - for _, info := range *resp { - table.Append([]string{ - info.ID, info.Name, - }) + table.SetHeader([]string{"ID", "Name"}) + + for _, info := range *resp { + table.Append([]string{ + info.ID, info.Name, + }) + } + table.Render() } - table.Render() + return nil } diff --git a/cmd/info/info_test.go b/cmd/info/info_test.go index e1828c7cd..91177f9eb 100644 --- a/cmd/info/info_test.go +++ b/cmd/info/info_test.go @@ -152,7 +152,7 @@ func TestTelemetry(t *testing.T) { assert.DeepEqual(t, telemetryClient.events, []telemetry.Event{ telemetry.CreateInfoEvent(telemetry.CommandInfo{ Name: "org", - LocalArgs: map[string]string{"help": "false"}, + LocalArgs: map[string]string{"help": "false", "json": "false"}, }, nil), }) } diff --git a/cmd/project/environment_variable.go b/cmd/project/environment_variable.go index aded40cac..fc23ebb72 100644 --- a/cmd/project/environment_variable.go +++ b/cmd/project/environment_variable.go @@ -1,6 +1,7 @@ package project import ( + "encoding/json" "fmt" "strings" @@ -16,6 +17,8 @@ func newProjectEnvironmentVariableCommand(ops *projectOpts, preRunE validator.Va Short: "Operate on environment variables of projects", } + jsonFormat := false + listVarsCommand := &cobra.Command{ Short: "List all environment variables of a project", Use: "list ", @@ -39,6 +42,11 @@ func newProjectEnvironmentVariableCommand(ops *projectOpts, preRunE validator.Va createVarCommand.Flags().StringVar(&envValue, "env-value", "", "An environment variable value to be created. You can also pass it by stdin without this option.") + listVarsCommand.PersistentFlags().BoolVar(&jsonFormat, "json", false, + "Return output back in JSON format") + createVarCommand.PersistentFlags().BoolVar(&jsonFormat, "json", false, + "Return output back in JSON format") + cmd.AddCommand(listVarsCommand) cmd.AddCommand(createVarCommand) return cmd @@ -50,14 +58,31 @@ func listProjectEnvironmentVariables(cmd *cobra.Command, client projectapi.Proje return err } - table := tablewriter.NewWriter(cmd.OutOrStdout()) + jsonVal, err := cmd.Flags().GetBool("json") + if err != nil { + return err + } + + if jsonVal { + // return JSON formatted for output + jsonEnvVars, err := json.Marshal(envVars) + if err != nil { + return err + } + jsonWriter := cmd.OutOrStdout() + if _, err := jsonWriter.Write(jsonEnvVars); err != nil { + return err + } + } else { + table := tablewriter.NewWriter(cmd.OutOrStdout()) - table.SetHeader([]string{"Environment Variable", "Value"}) + table.SetHeader([]string{"Environment Variable", "Value"}) - for _, envVar := range envVars { - table.Append([]string{envVar.Name, envVar.Value}) + for _, envVar := range envVars { + table.Append([]string{envVar.Name, envVar.Value}) + } + table.Render() } - table.Render() return nil } @@ -95,11 +120,28 @@ func createProjectEnvironmentVariable(cmd *cobra.Command, client projectapi.Proj return err } - table := tablewriter.NewWriter(cmd.OutOrStdout()) + jsonVal, err := cmd.Flags().GetBool("json") + if err != nil { + return err + } - table.SetHeader([]string{"Environment Variable", "Value"}) - table.Append([]string{v.Name, v.Value}) - table.Render() + if jsonVal { + // return JSON formatted for output + jsonV, err := json.Marshal(v) + if err != nil { + return err + } + jsonWriter := cmd.OutOrStdout() + if _, err := jsonWriter.Write(jsonV); err != nil { + return err + } + } else { + table := tablewriter.NewWriter(cmd.OutOrStdout()) + + table.SetHeader([]string{"Environment Variable", "Value"}) + table.Append([]string{v.Name, v.Value}) + table.Render() + } return nil } diff --git a/cmd/runner/instance.go b/cmd/runner/instance.go index a4da71338..6b449a9db 100644 --- a/cmd/runner/instance.go +++ b/cmd/runner/instance.go @@ -1,6 +1,7 @@ package runner import ( + "encoding/json" "io" "time" @@ -18,6 +19,8 @@ func newRunnerInstanceCommand(o *runnerOpts, preRunE validator.Validator) *cobra Short: "Operate on runner instances", } + jsonFormat := false + cmd.AddCommand(&cobra.Command{ Use: "list ", Short: "List runner instances", @@ -42,16 +45,31 @@ func newRunnerInstanceCommand(o *runnerOpts, preRunE validator.Validator) *cobra return err } - table := newRunnerInstanceTable(cmd.OutOrStdout()) - defer table.Render() - for _, r := range runners { - appendRunnerInstance(table, r) + if jsonFormat { + // return JSON formatted for output + jsonRunners, err := json.Marshal(runners) + if err != nil { + return err + } + jsonWriter := cmd.OutOrStdout() + if _, err := jsonWriter.Write(jsonRunners); err != nil { + return err + } + } else { + table := newRunnerInstanceTable(cmd.OutOrStdout()) + defer table.Render() + for _, r := range runners { + appendRunnerInstance(table, r) + } } return nil }, }) + cmd.PersistentFlags().BoolVar(&jsonFormat, "json", false, + "Return output back in JSON format") + return cmd } diff --git a/cmd/runner/testdata/runner/instance-expected-usage.txt b/cmd/runner/testdata/runner/instance-expected-usage.txt index 7ae1f46bd..09233e32a 100644 --- a/cmd/runner/testdata/runner/instance-expected-usage.txt +++ b/cmd/runner/testdata/runner/instance-expected-usage.txt @@ -4,4 +4,7 @@ Usage: Available Commands: list List runner instances +Flags: + --json Return output back in JSON format + Use "runner instance [command] --help" for more information about a command. diff --git a/cmd/runner/testdata/runner/instance/list-expected-usage.txt b/cmd/runner/testdata/runner/instance/list-expected-usage.txt index 0376cdfe8..b67586ac1 100644 --- a/cmd/runner/testdata/runner/instance/list-expected-usage.txt +++ b/cmd/runner/testdata/runner/instance/list-expected-usage.txt @@ -1,5 +1,5 @@ Usage: - runner instance list + runner instance list [flags] Aliases: list, ls @@ -7,3 +7,6 @@ Aliases: Examples: circleci runner instance ls my-namespace circleci runner instance ls my-namespace/my-resource-class + +Global Flags: + --json Return output back in JSON format diff --git a/cmd/runner/testdata/runner/token-expected-usage.txt b/cmd/runner/testdata/runner/token-expected-usage.txt index d7a16f2c5..dad78a157 100644 --- a/cmd/runner/testdata/runner/token-expected-usage.txt +++ b/cmd/runner/testdata/runner/token-expected-usage.txt @@ -6,4 +6,7 @@ Available Commands: delete Delete a token list List tokens for a resource-class +Flags: + --json Return output back in JSON format + Use "runner token [command] --help" for more information about a command. diff --git a/cmd/runner/testdata/runner/token/create-expected-usage.txt b/cmd/runner/testdata/runner/token/create-expected-usage.txt index 0e8823ce7..433303502 100644 --- a/cmd/runner/testdata/runner/token/create-expected-usage.txt +++ b/cmd/runner/testdata/runner/token/create-expected-usage.txt @@ -1,2 +1,5 @@ Usage: - runner token create + runner token create [flags] + +Global Flags: + --json Return output back in JSON format diff --git a/cmd/runner/testdata/runner/token/delete-expected-usage.txt b/cmd/runner/testdata/runner/token/delete-expected-usage.txt index 77d8fd43e..b0050ef20 100644 --- a/cmd/runner/testdata/runner/token/delete-expected-usage.txt +++ b/cmd/runner/testdata/runner/token/delete-expected-usage.txt @@ -1,5 +1,8 @@ Usage: - runner token delete + runner token delete [flags] Aliases: delete, rm + +Global Flags: + --json Return output back in JSON format diff --git a/cmd/runner/testdata/runner/token/list-expected-usage.txt b/cmd/runner/testdata/runner/token/list-expected-usage.txt index f86d7d0f4..a80ac45e5 100644 --- a/cmd/runner/testdata/runner/token/list-expected-usage.txt +++ b/cmd/runner/testdata/runner/token/list-expected-usage.txt @@ -1,5 +1,8 @@ Usage: - runner token list + runner token list [flags] Aliases: list, ls + +Global Flags: + --json Return output back in JSON format diff --git a/cmd/runner/token.go b/cmd/runner/token.go index 32cb11741..dfa3d6cc8 100644 --- a/cmd/runner/token.go +++ b/cmd/runner/token.go @@ -1,6 +1,7 @@ package runner import ( + "encoding/json" "time" "github.com/CircleCI-Public/circleci-cli/cmd/validator" @@ -15,6 +16,8 @@ func newTokenCommand(o *runnerOpts, preRunE validator.Validator) *cobra.Command Short: "Operate on runner tokens", } + jsonFormat := false + telemetryWrappedPreRunE := func(cmd *cobra.Command, args []string) error { telemetryClient, ok := telemetry.FromContext(cmd.Context()) if ok { @@ -64,15 +67,30 @@ func newTokenCommand(o *runnerOpts, preRunE validator.Validator) *cobra.Command return err } - table := tablewriter.NewWriter(cmd.OutOrStdout()) - defer table.Render() - table.SetHeader([]string{"ID", "Nickname", "Created At"}) - for _, token := range tokens { - table.Append([]string{token.ID, token.Nickname, token.CreatedAt.Format(time.RFC3339)}) + if jsonFormat { + // return JSON formatted for output + jsonTokens, err := json.Marshal(tokens) + if err != nil { + return err + } + jsonWriter := cmd.OutOrStdout() + if _, err := jsonWriter.Write(jsonTokens); err != nil { + return err + } + } else { + table := tablewriter.NewWriter(cmd.OutOrStdout()) + defer table.Render() + table.SetHeader([]string{"ID", "Nickname", "Created At"}) + for _, token := range tokens { + table.Append([]string{token.ID, token.Nickname, token.CreatedAt.Format(time.RFC3339)}) + } } return nil }, }) + cmd.PersistentFlags().BoolVar(&jsonFormat, "json", false, + "Return output back in JSON format") + return cmd }