diff --git a/.circleci/config.yml b/.circleci/config.yml
index edd87a19d..8a1b5456c 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -137,6 +137,10 @@ jobs:
working_directory: integration_tests
environment:
TESTING: "true"
+ - run:
+ name: "Make sure simple command do not cause any timeout"
+ command: circleci version
+
test:
executor: go
steps:
@@ -378,12 +382,14 @@ workflows:
- devex-release
- deploy:
requires:
+ - cucumber
- test
- test_mac
- coverage
- lint
- deploy-test
- shellcheck/check
+ - vulnerability-scan
filters:
branches:
only: main
diff --git a/Makefile b/Makefile
index 604d50148..a51a58c4a 100644
--- a/Makefile
+++ b/Makefile
@@ -4,7 +4,7 @@ GOOS=$(shell go env GOOS)
GOARCH=$(shell go env GOARCH)
build: always
- go build -o build/$(GOOS)/$(GOARCH)/circleci
+ go build -o build/$(GOOS)/$(GOARCH)/circleci -ldflags='-X github.com/CircleCI-Public/circleci-cli/telemetry.SegmentEndpoint=https://api.segment.io'
build-all: build/linux/amd64/circleci build/darwin/amd64/circleci
diff --git a/README.md b/README.md
index a955cff70..b57b73873 100644
--- a/README.md
+++ b/README.md
@@ -180,6 +180,20 @@ Please see the [documentation](https://circleci-public.github.io/circleci-cli) o
## Server compatibility
-There are some difference of behavior depending on the version you use:
- - config validation will use the GraphQL API until **Server v4.0.5, v4.1.3, v4.2.0**. The above versions will use the new route `compile-config-with-defaults`
- - `circleci orb validate` will only allow you to validate orbs using other private orbs with the option `--org-slug` from version **Server v4.2.0**
+| Functionality | Impacted commands | Change description | Compatibility with Server |
+| --- | --- | --- | --- |
+| Config compilation and validation |
- `circleci config validate`
- `circleci config process`
- `circleci local execute`
| The config validation has been moved from the GraphQL API to a specific API endpoint | - **Server v4.0.5, v4.1.3, v4.2.0 and above**: Commands use the new specific endpoint
- **Previous version**: Commands use the GraphQL API
|
+| Orb compilation and validation of orb using private orbs | - `circleci orb process`
- `circleci orb validate`
| To support the validation of orbs requesting private orbs (see [issue](https://github.com/CircleCI-Public/circleci-cli/issues/751)). A field `ownerId` has been added to the GraphQL orb validation endpoint. Thus allowing the `Impacted commands` to use the `--org-id` parameter to enable the orb compilation / validation | - **Server v4.2.0 and above**: The field is accessible so you can use the parameter
- **Previous versions**: The field does not exist making the functionality unavailable
|
+
+## Telemetry
+
+The CircleCI CLI includes a telemetry feature that collects basic errors and feature usage data in order to help us improve the experience for everyone.
+
+Telemetry works on an opt-in basis: when running a command for the first time, you will be asked for consent to enable telemetry. For non-TTY STDIN, telemetry is disabled by default, ensuring that scripts that use the CLI run smoothly.
+
+You can disable or enable telemetry anytime in one of the following ways:
+
+* Run the commands `circleci telemetry enable` or `circleci telemetry disable`
+
+* Set the `CIRCLECI_CLI_TELEMETRY_OPTOUT` environment variable to `1` or `true` to disable it
+
diff --git a/Taskfile.yml b/Taskfile.yml
index ad2d19df9..3cb8bfffc 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -47,14 +47,16 @@ tasks:
build:
desc: Build main
cmds:
- - go build -v -o build/darwin/amd64/circleci .
-
+ # LDFlags sets the segment endpoint to an empty string thus letting the analytics library set the default endpoint on its own
+ # Not setting the `SegmentEndpoint` variable would let the value in the code ie "http://localhost"
+ - go build -v -o build/$(go env GOOS)/$(go env GOARCH)/circleci -ldflags='-X github.com/CircleCI-Public/circleci-cli/telemetry.SegmentEndpoint=https://api.segment.io' .
+
build-linux:
desc: Build main
cmds:
- go build -v -o build/linux/amd64/circleci .
-
+
cover:
desc: tests and generates a cover profile
cmds:
- - TESTING=true go test -race -coverprofile=coverage.txt ./...
\ No newline at end of file
+ - TESTING=true go test -race -coverprofile=coverage.txt ./...
diff --git a/api/api.go b/api/api.go
index 2dbaca0bd..846626465 100644
--- a/api/api.go
+++ b/api/api.go
@@ -6,11 +6,13 @@ import (
"io"
"log"
"net/http"
+ "net/url"
"os"
"sort"
"strings"
"github.com/CircleCI-Public/circleci-cli/api/graphql"
+ "github.com/CircleCI-Public/circleci-cli/api/rest"
"github.com/CircleCI-Public/circleci-cli/references"
"github.com/CircleCI-Public/circleci-cli/settings"
"github.com/Masterminds/semver"
@@ -1818,3 +1820,20 @@ func FollowProject(config settings.Config, vcs string, owner string, projectName
return fr, nil
}
+
+type Me struct {
+ ID string `json:"id"`
+ Login string `json:"login"`
+ Name string `json:"name"`
+}
+
+func GetMe(client *rest.Client) (Me, error) {
+ req, err := client.NewRequest("GET", &url.URL{Path: "me"}, nil)
+ if err != nil {
+ return Me{}, errors.Wrap(err, "Unable to get user info")
+ }
+
+ var me Me
+ _, err = client.DoRequest(req, &me)
+ return me, err
+}
diff --git a/clitest/clitest.go b/clitest/clitest.go
index 6a0010d5c..24b0b25bc 100644
--- a/clitest/clitest.go
+++ b/clitest/clitest.go
@@ -11,9 +11,11 @@ import (
"runtime"
"github.com/CircleCI-Public/circleci-cli/api/graphql"
+ "github.com/CircleCI-Public/circleci-cli/settings"
"github.com/onsi/gomega/gexec"
"github.com/onsi/gomega/ghttp"
"github.com/onsi/gomega/types"
+ "gopkg.in/yaml.v3"
"github.com/onsi/gomega"
)
@@ -30,10 +32,12 @@ func ShouldFail() types.GomegaMatcher {
// TempSettings contains useful settings for testing the CLI
type TempSettings struct {
- Home string
- TestServer *ghttp.Server
- Config *TmpFile
- Update *TmpFile
+ Home string
+ TestServer *ghttp.Server
+ Config *TmpFile
+ Update *TmpFile
+ Telemetry *TmpFile
+ TelemetryDestPath string
}
// Close should be called in an AfterEach and cleans up the temp directory and server process
@@ -68,6 +72,16 @@ func WithTempSettings() *TempSettings {
gomega.Expect(os.Mkdir(settingsPath, 0700)).To(gomega.Succeed())
tempSettings.Config = OpenTmpFile(settingsPath, "cli.yml")
+ tempSettings.Telemetry = OpenTmpFile(settingsPath, "telemetry.yml")
+ content, err := yaml.Marshal(settings.TelemetrySettings{
+ IsEnabled: false,
+ HasAnsweredPrompt: true,
+ })
+ gomega.Expect(err).ToNot(gomega.HaveOccurred())
+ _, err = tempSettings.Telemetry.File.Write(content)
+ gomega.Expect(err).ToNot(gomega.HaveOccurred())
+ tempSettings.TelemetryDestPath = filepath.Join(tempSettings.Home, "telemetry-content")
+
tempSettings.Update = OpenTmpFile(settingsPath, "update_check.yml")
tempSettings.TestServer = ghttp.NewServer()
diff --git a/clitest/telemetry.go b/clitest/telemetry.go
new file mode 100644
index 000000000..fc21f85ce
--- /dev/null
+++ b/clitest/telemetry.go
@@ -0,0 +1,19 @@
+package clitest
+
+import (
+ "encoding/json"
+ "os"
+
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
+ "github.com/onsi/gomega"
+)
+
+func CompareTelemetryEvent(settings *TempSettings, expected []telemetry.Event) {
+ content, err := os.ReadFile(settings.TelemetryDestPath)
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+
+ result := []telemetry.Event{}
+ err = json.Unmarshal(content, &result)
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(result).To(gomega.Equal(expected))
+}
diff --git a/cmd/build.go b/cmd/build.go
index c32ed969c..df4d110c4 100644
--- a/cmd/build.go
+++ b/cmd/build.go
@@ -3,6 +3,7 @@ package cmd
import (
"github.com/CircleCI-Public/circleci-cli/local"
"github.com/CircleCI-Public/circleci-cli/settings"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
"github.com/spf13/cobra"
)
@@ -16,7 +17,14 @@ func newLocalExecuteCommand(config *settings.Config) *cobra.Command {
return nil
},
RunE: func(cmd *cobra.Command, _ []string) error {
- return local.Execute(cmd.Flags(), config, args)
+ err := local.Execute(cmd.Flags(), config, args)
+
+ telemetryClient, ok := telemetry.FromContext(cmd.Context())
+ if ok {
+ _ = telemetryClient.Track(telemetry.CreateLocalExecuteEvent(telemetry.GetCommandInformation(cmd, true)))
+ }
+
+ return err
},
Args: cobra.MinimumNArgs(1),
}
diff --git a/cmd/cmd_suite_test.go b/cmd/cmd_suite_test.go
index 5f126d986..5e54304ca 100644
--- a/cmd/cmd_suite_test.go
+++ b/cmd/cmd_suite_test.go
@@ -5,6 +5,7 @@ import (
"os"
"os/exec"
"testing"
+ "time"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
@@ -14,6 +15,8 @@ import (
var pathCLI string
var _ = BeforeSuite(func() {
+ SetDefaultEventuallyTimeout(time.Second * 30)
+
var err error
pathCLI, err = gexec.Build("github.com/CircleCI-Public/circleci-cli")
Ω(err).ShouldNot(HaveOccurred())
diff --git a/cmd/completion.go b/cmd/completion.go
index bc0ff6d53..edeaa58d4 100644
--- a/cmd/completion.go
+++ b/cmd/completion.go
@@ -3,13 +3,21 @@ package cmd
import (
"os"
+ "github.com/CircleCI-Public/circleci-cli/settings"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
"github.com/spf13/cobra"
)
-func newCompletionCommand() *cobra.Command {
+func newCompletionCommand(config *settings.Config) *cobra.Command {
completionCmd := &cobra.Command{
Use: "completion",
Short: "Generate shell completion scripts",
+ PersistentPreRun: func(cmd *cobra.Command, _ []string) {
+ telemetryClient, ok := telemetry.FromContext(cmd.Context())
+ if ok {
+ _ = telemetryClient.Track(telemetry.CreateCompletionCommand(telemetry.GetCommandInformation(cmd, true)))
+ }
+ },
Run: func(cmd *cobra.Command, _ []string) {
err := cmd.Help()
if err != nil {
diff --git a/cmd/config.go b/cmd/config.go
index 690fc78d1..8fdba6194 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -8,14 +8,14 @@ import (
"github.com/CircleCI-Public/circleci-config/labeling"
"github.com/CircleCI-Public/circleci-config/labeling/codebase"
- "github.com/pkg/errors"
- "github.com/spf13/cobra"
- "gopkg.in/yaml.v3"
-
"github.com/CircleCI-Public/circleci-cli/config"
"github.com/CircleCI-Public/circleci-cli/filetree"
"github.com/CircleCI-Public/circleci-cli/proxy"
"github.com/CircleCI-Public/circleci-cli/settings"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
+ "github.com/pkg/errors"
+ "github.com/spf13/cobra"
+ "gopkg.in/yaml.v3"
)
// Path to the config.yml file to operate on.
@@ -37,8 +37,14 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command {
packCommand := &cobra.Command{
Use: "pack ",
Short: "Pack up your CircleCI configuration into a single file.",
- RunE: func(_ *cobra.Command, args []string) error {
- return packConfig(args)
+ RunE: func(cmd *cobra.Command, args []string) error {
+ err := packConfig(args)
+
+ telemetryClient, ok := telemetry.FromContext(cmd.Context())
+ if ok {
+ _ = telemetryClient.Track(telemetry.CreateConfigEvent(telemetry.GetCommandInformation(cmd, true), err))
+ }
+ return err
},
Args: cobra.ExactArgs(1),
Annotations: make(map[string]string),
@@ -64,13 +70,20 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command {
if len(args) == 1 {
path = args[0]
}
- return compiler.ValidateConfig(config.ValidateConfigOpts{
+
+ err = compiler.ValidateConfig(config.ValidateConfigOpts{
ConfigPath: path,
OrgID: orgID,
OrgSlug: orgSlug,
IgnoreDeprecatedImages: ignoreDeprecatedImages,
VerboseOutput: verboseOutput,
})
+ telemetryClient, ok := telemetry.FromContext(cmd.Context())
+ if ok {
+ _ = telemetryClient.Track(telemetry.CreateConfigEvent(telemetry.GetCommandInformation(cmd, true), err))
+ }
+
+ return err
},
Args: cobra.MaximumNArgs(1),
Annotations: make(map[string]string),
@@ -112,6 +125,10 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command {
PipelineParamsFilePath: pipelineParamsFilePath,
VerboseOutput: verboseOutput,
})
+ telemetryClient, ok := telemetry.FromContext(cmd.Context())
+ if ok {
+ _ = telemetryClient.Track(telemetry.CreateConfigEvent(telemetry.GetCommandInformation(cmd, true), err))
+ }
if err != nil {
return err
}
diff --git a/cmd/config_test.go b/cmd/config_test.go
index 9168b4146..86c5e8f6d 100644
--- a/cmd/config_test.go
+++ b/cmd/config_test.go
@@ -7,6 +7,7 @@ import (
"path/filepath"
"github.com/CircleCI-Public/circleci-cli/clitest"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gexec"
@@ -29,12 +30,41 @@ var _ = Describe("Config", func() {
tempSettings.Close()
})
+ Describe("telemetry", func() {
+ BeforeEach(func() {
+ tempSettings = clitest.WithTempSettings()
+ command = commandWithHome(pathCLI, tempSettings.Home,
+ "config", "pack",
+ "--skip-update-check",
+ filepath.Join("testdata", "hugo-pack", ".circleci"),
+ )
+ command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath))
+ })
+
+ AfterEach(func() {
+ tempSettings.Close()
+ })
+
+ It("should send telemetry event", func() {
+ session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
+ Expect(err).ShouldNot(HaveOccurred())
+
+ Eventually(session).Should(gexec.Exit(0))
+ clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{
+ telemetry.CreateConfigEvent(telemetry.CommandInfo{
+ Name: "pack",
+ LocalArgs: map[string]string{"help": "false"},
+ }, nil),
+ })
+ })
+ })
+
Describe("a .circleci folder with config.yml and local orbs folder containing the hugo orb", func() {
BeforeEach(func() {
command = exec.Command(pathCLI,
"config", "pack",
"--skip-update-check",
- "testdata/hugo-pack/.circleci")
+ filepath.Join("testdata", "hugo-pack", ".circleci"))
results = golden.Get(GinkgoT(), filepath.FromSlash("hugo-pack/result.yml"))
})
diff --git a/cmd/create_telemetry.go b/cmd/create_telemetry.go
new file mode 100644
index 000000000..645dfe675
--- /dev/null
+++ b/cmd/create_telemetry.go
@@ -0,0 +1,206 @@
+package cmd
+
+import (
+ "fmt"
+ "os"
+ "runtime"
+
+ "github.com/CircleCI-Public/circleci-cli/api"
+ "github.com/CircleCI-Public/circleci-cli/api/rest"
+ "github.com/CircleCI-Public/circleci-cli/prompt"
+ "github.com/CircleCI-Public/circleci-cli/settings"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
+ "github.com/CircleCI-Public/circleci-cli/version"
+ "github.com/google/uuid"
+ "github.com/spf13/cobra"
+ "github.com/spf13/pflag"
+ "golang.org/x/term"
+)
+
+var (
+ CreateUUID = func() string { return uuid.New().String() }
+ isStdinATTY = term.IsTerminal(int(os.Stdin.Fd()))
+ isStdoutATTY = term.IsTerminal(int(os.Stdout.Fd()))
+ anonymousUser = telemetry.User{
+ UniqueID: "cli-anonymous-telemetry",
+ }
+)
+
+type telemetryUI interface {
+ AskUserToApproveTelemetry(message string) bool
+}
+
+type telemetryInteractiveUI struct{}
+
+func (telemetryInteractiveUI) AskUserToApproveTelemetry(message string) bool {
+ return prompt.AskUserToConfirmWithDefault(message, true)
+}
+
+type TelemetryAPIClient interface {
+ GetMyUserId() (string, error)
+}
+
+func CreateAPIClient(config *settings.Config) TelemetryAPIClient {
+ return telemetryCircleCIAPI{
+ cli: rest.NewFromConfig(config.Host, config),
+ }
+}
+
+type telemetryCircleCIAPI struct {
+ cli *rest.Client
+}
+
+func (client telemetryCircleCIAPI) GetMyUserId() (string, error) {
+ me, err := api.GetMe(client.cli)
+ if err != nil {
+ return "", err
+ }
+ return me.ID, nil
+}
+
+type nullTelemetryAPIClient struct{}
+
+func (client nullTelemetryAPIClient) GetMyUserId() (string, error) {
+ panic("Should not be called")
+}
+
+// Make sure the user gave their approval for the telemetry and
+func CreateTelemetry(config *settings.Config) telemetry.Client {
+ mockTelemetry := config.MockTelemetry
+ if mockTelemetry == "" {
+ mockTelemetry = os.Getenv("MOCK_TELEMETRY")
+ }
+ if mockTelemetry != "" {
+ return telemetry.CreateFileTelemetry(mockTelemetry)
+ }
+
+ if config.IsTelemetryDisabled || len(os.Getenv("CIRCLECI_CLI_TELEMETRY_OPTOUT")) != 0 {
+ return telemetry.CreateClient(telemetry.User{}, false)
+ }
+
+ var apiClient TelemetryAPIClient = nullTelemetryAPIClient{}
+ if config.HTTPClient != nil {
+ apiClient = telemetryCircleCIAPI{
+ cli: rest.NewFromConfig(config.Host, config),
+ }
+ }
+ ui := telemetryInteractiveUI{}
+
+ telemetrySettings := settings.TelemetrySettings{}
+ user := telemetry.User{
+ IsSelfHosted: config.Host != defaultHost,
+ OS: runtime.GOOS,
+ Version: version.Version,
+ TeamName: "devex",
+ }
+
+ loadTelemetrySettings(&telemetrySettings, &user, apiClient, ui)
+ client := telemetry.CreateClient(user, telemetrySettings.IsEnabled)
+
+ return client
+}
+
+func loadTelemetrySettings(settings *settings.TelemetrySettings, user *telemetry.User, apiClient TelemetryAPIClient, ui telemetryUI) {
+ err := settings.Load()
+ if err != nil && !os.IsNotExist(err) {
+ fmt.Printf("Error loading telemetry configuration: %s\n", err)
+ }
+
+ user.UniqueID = settings.UniqueID
+ user.UserID = settings.UserID
+
+ // If we already have telemetry information or that telemetry is explicitly disabled, skip
+ if settings.HasAnsweredPrompt {
+ // If we have no user id, we try requesting the user id again
+ if settings.UserID == "" && settings.IsEnabled {
+ myID, err := apiClient.GetMyUserId()
+ if err == nil {
+ settings.UserID = myID
+ user.UserID = myID
+ if err := settings.Write(); err != nil {
+ fmt.Printf("Error writing telemetry settings to disk: %s\n", err)
+ }
+ }
+ }
+
+ return
+ }
+
+ // If stdin is not a TTY, stdout is not a TTY or we are in a CI environment, send telemetry event, disable telemetry and return
+ isDisabledByDefault := !isStdinATTY || !isStdoutATTY || os.Getenv("CI") == trueString
+ if isDisabledByDefault {
+ settings.IsEnabled = false
+ err := telemetry.SendTelemetryApproval(anonymousUser, telemetry.NoStdin)
+ if err != nil {
+ fmt.Printf("Error while sending telemetry approval %s\n", err)
+ }
+ return
+ }
+
+ // Else ask user for telemetry approval
+ fmt.Println("CircleCI would like to collect CLI usage data for product improvement purposes.")
+ fmt.Println("")
+ fmt.Println("Participation is voluntary, and your choice can be changed at any time through the command `cli telemetry enable` and `cli telemetry disable`.")
+ fmt.Println("For more information, please see our privacy policy at https://circleci.com/legal/privacy/.")
+ fmt.Println("")
+ settings.IsEnabled = ui.AskUserToApproveTelemetry("Enable telemetry?")
+ settings.HasAnsweredPrompt = true
+
+ // Make sure we have user info and set them
+ if settings.IsEnabled {
+ if settings.UniqueID == "" {
+ settings.UniqueID = CreateUUID()
+ }
+ user.UniqueID = settings.UniqueID
+
+ if settings.UserID == "" {
+ myID, err := apiClient.GetMyUserId()
+ if err == nil {
+ settings.UserID = myID
+ }
+ }
+ user.UserID = settings.UserID
+ } else {
+ *user = anonymousUser
+ }
+
+ // Send telemetry approval event
+ approval := telemetry.Enabled
+ if !settings.IsEnabled {
+ approval = telemetry.Disabled
+ }
+
+ if err := telemetry.SendTelemetryApproval(*user, approval); err != nil {
+ fmt.Printf("Unable to send approval telemetry event: %s\n", err)
+ }
+
+ // Write telemetry
+ if err := settings.Write(); err != nil {
+ fmt.Printf("Error writing telemetry settings to disk: %s\n", err)
+ }
+}
+
+// Utility function used when creating telemetry events.
+// It takes a cobra Command and creates a telemetry.CommandInfo of it
+// If getParent is true, puts both the command's args in `LocalArgs` and the parent's args
+// Else only put the command's args
+// Note: child flags overwrite parent flags with same name
+func GetCommandInformation(cmd *cobra.Command, getParent bool) telemetry.CommandInfo {
+ localArgs := map[string]string{}
+
+ parent := cmd.Parent()
+ if getParent && parent != nil {
+ parent.LocalFlags().VisitAll(func(flag *pflag.Flag) {
+ localArgs[flag.Name] = flag.Value.String()
+ })
+ }
+
+ cmd.LocalFlags().VisitAll(func(flag *pflag.Flag) {
+ localArgs[flag.Name] = flag.Value.String()
+ })
+
+ return telemetry.CommandInfo{
+ Name: cmd.Name(),
+ LocalArgs: localArgs,
+ }
+}
diff --git a/cmd/create_telemetry_test.go b/cmd/create_telemetry_test.go
new file mode 100644
index 000000000..92b5b9e9f
--- /dev/null
+++ b/cmd/create_telemetry_test.go
@@ -0,0 +1,321 @@
+package cmd
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/CircleCI-Public/circleci-cli/settings"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
+ "github.com/spf13/afero"
+ "gotest.tools/v3/assert"
+)
+
+func TestLoadTelemetrySettings(t *testing.T) {
+ // Delete env.CI variable
+ oldEnvCIValue := os.Getenv("CI")
+ os.Setenv("CI", "")
+ defer os.Setenv("CI", oldEnvCIValue)
+
+ // Mock HTTP
+ userId := "id"
+ uniqueId := "unique-id"
+
+ // Mock create UUID
+ oldUUIDCreate := CreateUUID
+ CreateUUID = func() string { return uniqueId }
+ defer (func() { CreateUUID = oldUUIDCreate })()
+
+ // Create test cases
+ type args struct {
+ closeStdin bool
+ closeStdout bool
+ env map[string]string
+ promptApproval bool
+ settings settings.TelemetrySettings
+ }
+ type want struct {
+ settings settings.TelemetrySettings
+ fileNotCreated bool
+ telemetryEvents []telemetry.Event
+ }
+ type testCase struct {
+ name string
+ args args
+ want want
+ }
+
+ testCases := []testCase{
+ {
+ name: "Prompt approval should be saved in settings",
+ args: args{
+ promptApproval: true,
+ settings: settings.TelemetrySettings{},
+ },
+ want: want{
+ settings: settings.TelemetrySettings{
+ IsEnabled: true,
+ HasAnsweredPrompt: true,
+ UserID: userId,
+ UniqueID: uniqueId,
+ },
+ telemetryEvents: []telemetry.Event{
+ {Object: "cli-telemetry", Action: "enabled",
+ Properties: map[string]interface{}{
+ "UUID": uniqueId,
+ "user_id": userId,
+ "is_self_hosted": false,
+ }},
+ },
+ },
+ },
+ {
+ name: "Prompt disapproval should be saved in settings",
+ args: args{
+ promptApproval: false,
+ settings: settings.TelemetrySettings{},
+ },
+ want: want{
+ settings: settings.TelemetrySettings{
+ IsEnabled: false,
+ HasAnsweredPrompt: true,
+ },
+ telemetryEvents: []telemetry.Event{
+ {Object: "cli-telemetry", Action: "disabled", Properties: map[string]interface{}{
+ "UUID": "cli-anonymous-telemetry",
+ "is_self_hosted": false,
+ }},
+ },
+ },
+ },
+ {
+ name: "Does not recreate a unique ID if there is one",
+ args: args{
+ promptApproval: true,
+ settings: settings.TelemetrySettings{
+ UniqueID: "other-id",
+ },
+ },
+ want: want{
+ settings: settings.TelemetrySettings{
+ IsEnabled: true,
+ HasAnsweredPrompt: true,
+ UserID: userId,
+ UniqueID: "other-id",
+ },
+ telemetryEvents: []telemetry.Event{
+ {Object: "cli-telemetry", Action: "enabled", Properties: map[string]interface{}{
+ "UUID": "other-id",
+ "user_id": userId,
+ "is_self_hosted": false,
+ }},
+ },
+ },
+ },
+ {
+ name: "Does not change telemetry settings if user already answered prompt",
+ args: args{
+ settings: settings.TelemetrySettings{
+ HasAnsweredPrompt: true,
+ },
+ },
+ want: want{
+ settings: settings.TelemetrySettings{
+ HasAnsweredPrompt: true,
+ },
+ fileNotCreated: true,
+ },
+ },
+ {
+ name: "Does not change telemetry settings if stdin is not a TTY",
+ args: args{closeStdin: true},
+ want: want{
+ fileNotCreated: true,
+ telemetryEvents: []telemetry.Event{
+ {Object: "cli-telemetry", Action: "disabled_default", Properties: map[string]interface{}{
+ "UUID": "cli-anonymous-telemetry",
+ "is_self_hosted": false,
+ }},
+ },
+ },
+ },
+ {
+ name: "Does not change telemetry settings if stdout is not a TTY",
+ args: args{closeStdout: true},
+ want: want{
+ fileNotCreated: true,
+ telemetryEvents: []telemetry.Event{
+ {Object: "cli-telemetry", Action: "disabled_default", Properties: map[string]interface{}{
+ "UUID": "cli-anonymous-telemetry",
+ "is_self_hosted": false,
+ }},
+ },
+ },
+ },
+ {
+ name: "Does not change telemetry settings if env.CI == true",
+ args: args{env: map[string]string{"CI": "true"}},
+ want: want{
+ fileNotCreated: true,
+ telemetryEvents: []telemetry.Event{
+ {Object: "cli-telemetry", Action: "disabled_default", Properties: map[string]interface{}{
+ "UUID": "cli-anonymous-telemetry",
+ "is_self_hosted": false,
+ }},
+ },
+ },
+ },
+ {
+ name: "Should try loading user id if user already answered prompt but has no user id",
+ args: args{
+ settings: settings.TelemetrySettings{
+ HasAnsweredPrompt: true,
+ IsEnabled: true,
+ },
+ },
+ want: want{
+ settings: settings.TelemetrySettings{
+ HasAnsweredPrompt: true,
+ IsEnabled: true,
+ UserID: userId,
+ },
+ },
+ },
+ {
+ name: "Should consider has_answered_prompt before disabled-default scenarios",
+ args: args{
+ env: map[string]string{"CI": "true"},
+ settings: settings.TelemetrySettings{
+ HasAnsweredPrompt: true,
+ IsEnabled: true,
+ },
+ },
+ want: want{
+ settings: settings.TelemetrySettings{
+ HasAnsweredPrompt: true,
+ IsEnabled: true,
+ UserID: userId,
+ },
+ },
+ },
+ }
+
+ for _, tt := range testCases {
+ t.Run(tt.name, func(t *testing.T) {
+ // Mock FS
+ oldFS := settings.FS.Fs
+ settings.FS.Fs = afero.NewMemMapFs()
+ defer (func() { settings.FS.Fs = oldFS })()
+
+ // Mock stdin
+ oldIsStdinOpen := isStdinATTY
+ isStdinATTY = !tt.args.closeStdin
+ defer (func() { isStdinATTY = oldIsStdinOpen })()
+
+ // Mock stdout
+ oldIsStdoutOpen := isStdoutATTY
+ isStdoutATTY = !tt.args.closeStdout
+ defer (func() { isStdoutATTY = oldIsStdoutOpen })()
+
+ // Mock telemetry
+ telemetryClient := testTelemetry{events: make([]telemetry.Event, 0)}
+ oldCreateActiveTelemetry := telemetry.CreateActiveTelemetry
+ telemetry.CreateActiveTelemetry = func(user telemetry.User) telemetry.Client {
+ telemetryClient.User = user
+ return &telemetryClient
+ }
+ defer (func() { telemetry.CreateActiveTelemetry = oldCreateActiveTelemetry })()
+
+ // Mock env
+ if tt.args.env != nil {
+ for k, v := range tt.args.env {
+ oldEnvValue := os.Getenv(k)
+ os.Setenv(k, v)
+ defer os.Setenv(k, oldEnvValue)
+ }
+ }
+
+ // Run tested function
+ loadTelemetrySettings(&tt.args.settings, &telemetry.User{}, telemetryTestAPIClient{userId, nil}, telemetryTestUI{tt.args.promptApproval})
+ assert.DeepEqual(t, &tt.args.settings, &tt.want.settings)
+
+ // Verify good telemetry events were sent
+ expectedEvents := tt.want.telemetryEvents
+ if expectedEvents == nil {
+ expectedEvents = []telemetry.Event{}
+ }
+ assert.DeepEqual(t, telemetryClient.events, expectedEvents)
+
+ // Verify if settings file exist
+ exist, err := settings.FS.Exists(filepath.Join(settings.SettingsPath(), "telemetry.yml"))
+ assert.NilError(t, err)
+ assert.Equal(t, exist, !tt.want.fileNotCreated)
+ if tt.want.fileNotCreated {
+ return
+ }
+
+ // Verify settings file content
+ loaded := settings.TelemetrySettings{}
+ err = loaded.Load()
+ assert.NilError(t, err)
+ assert.DeepEqual(t, &loaded, &tt.want.settings)
+ })
+ }
+}
+
+type testTelemetry struct {
+ events []telemetry.Event
+ User telemetry.User
+}
+
+func (cli *testTelemetry) Close() error { return nil }
+
+func (cli *testTelemetry) Track(event telemetry.Event) error {
+ newEvent := event
+ properties := map[string]interface{}{}
+ if cli.User.UniqueID != "" {
+ properties["UUID"] = cli.User.UniqueID
+ }
+
+ if cli.User.UserID != "" {
+ properties["user_id"] = cli.User.UserID
+ }
+
+ properties["is_self_hosted"] = cli.User.IsSelfHosted
+
+ if cli.User.OS != "" {
+ properties["os"] = cli.User.OS
+ }
+
+ if cli.User.Version != "" {
+ properties["cli_version"] = cli.User.Version
+ }
+
+ if cli.User.TeamName != "" {
+ properties["team_name"] = cli.User.TeamName
+ }
+
+ if len(properties) > 0 {
+ newEvent.Properties = properties
+ }
+
+ cli.events = append(cli.events, newEvent)
+ return nil
+}
+
+type telemetryTestUI struct {
+ Approved bool
+}
+
+func (ui telemetryTestUI) AskUserToApproveTelemetry(message string) bool {
+ return ui.Approved
+}
+
+type telemetryTestAPIClient struct {
+ id string
+ err error
+}
+
+func (me telemetryTestAPIClient) GetMyUserId() (string, error) {
+ return me.id, me.err
+}
diff --git a/cmd/diagnostic.go b/cmd/diagnostic.go
index 071ceb257..b8053d63f 100644
--- a/cmd/diagnostic.go
+++ b/cmd/diagnostic.go
@@ -7,6 +7,7 @@ import (
"github.com/CircleCI-Public/circleci-cli/api"
"github.com/CircleCI-Public/circleci-cli/api/graphql"
"github.com/CircleCI-Public/circleci-cli/settings"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
"github.com/spf13/cobra"
)
@@ -28,8 +29,15 @@ func newDiagnosticCommand(config *settings.Config) *cobra.Command {
opts.args = args
opts.cl = graphql.NewClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug)
},
- RunE: func(_ *cobra.Command, _ []string) error {
- return diagnostic(opts)
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ err := diagnostic(opts)
+
+ telemetryClient, ok := telemetry.FromContext(cmd.Context())
+ if ok {
+ _ = telemetryClient.Track(telemetry.CreateDiagnosticEvent(err))
+ }
+
+ return err
},
}
diff --git a/cmd/diagnostic_test.go b/cmd/diagnostic_test.go
index f3d48e5d5..21071d3e3 100644
--- a/cmd/diagnostic_test.go
+++ b/cmd/diagnostic_test.go
@@ -8,6 +8,7 @@ import (
"github.com/CircleCI-Public/circleci-cli/api/graphql"
"github.com/CircleCI-Public/circleci-cli/clitest"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
@@ -79,6 +80,24 @@ var _ = Describe("Diagnostic", func() {
tempSettings.Close()
})
+ Describe("telemetry", func() {
+ It("should send telemetry event", func() {
+ command = commandWithHome(pathCLI, tempSettings.Home,
+ "diagnostic",
+ "--skip-update-check",
+ "--host", tempSettings.TestServer.URL())
+ command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath))
+ tempSettings.Config.Write([]byte(`token: mytoken`))
+ session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
+ Expect(err).ShouldNot(HaveOccurred())
+
+ Eventually(session).Should(gexec.Exit(0))
+ clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{
+ telemetry.CreateDiagnosticEvent(nil),
+ })
+ })
+ })
+
Describe("existing config file", func() {
Describe("token set in config file", func() {
BeforeEach(func() {
diff --git a/cmd/follow.go b/cmd/follow.go
index 31eb3bbae..9512423ee 100644
--- a/cmd/follow.go
+++ b/cmd/follow.go
@@ -6,6 +6,7 @@ import (
"github.com/CircleCI-Public/circleci-cli/api"
"github.com/CircleCI-Public/circleci-cli/git"
"github.com/CircleCI-Public/circleci-cli/settings"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@@ -53,8 +54,15 @@ func followProjectCommand(config *settings.Config) *cobra.Command {
followCommand := &cobra.Command{
Use: "follow",
Short: "Attempt to follow the project for the current git repository.",
- RunE: func(_ *cobra.Command, _ []string) error {
- return followProject(opts)
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ err := followProject(opts)
+
+ telemetryClient, ok := telemetry.FromContext(cmd.Context())
+ if ok {
+ _ = telemetryClient.Track(telemetry.CreateFollowEvent(err))
+ }
+
+ return err
},
}
return followCommand
diff --git a/cmd/info/info.go b/cmd/info/info.go
index 1ed3028fe..fce0bad59 100644
--- a/cmd/info/info.go
+++ b/cmd/info/info.go
@@ -4,6 +4,7 @@ import (
"github.com/CircleCI-Public/circleci-cli/api/info"
"github.com/CircleCI-Public/circleci-cli/cmd/validator"
"github.com/CircleCI-Public/circleci-cli/settings"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
@@ -41,7 +42,14 @@ func orgInfoCommand(client info.InfoClient, opts infoOptions) *cobra.Command {
Long: `View your Organizations' names and ids.`,
PreRunE: opts.validator,
RunE: func(cmd *cobra.Command, _ []string) error {
- return getOrgInformation(cmd, client)
+ err := getOrgInformation(cmd, client)
+
+ telemetryClient, ok := telemetry.FromContext(cmd.Context())
+ if ok {
+ _ = telemetryClient.Track(telemetry.CreateInfoEvent(telemetry.GetCommandInformation(cmd, true), err))
+ }
+
+ return err
},
Annotations: make(map[string]string),
Example: `circleci info org`,
diff --git a/cmd/info/info_test.go b/cmd/info/info_test.go
index 19bda1417..7b49c8784 100644
--- a/cmd/info/info_test.go
+++ b/cmd/info/info_test.go
@@ -2,6 +2,7 @@ package info
import (
"bytes"
+ "context"
"fmt"
"net/http"
"net/http/httptest"
@@ -9,6 +10,7 @@ import (
"github.com/CircleCI-Public/circleci-cli/cmd/validator"
"github.com/CircleCI-Public/circleci-cli/settings"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
)
@@ -106,6 +108,55 @@ func TestFailedValidator(t *testing.T) {
assert.Error(t, err, errorMessage)
}
+type testTelemetryClient struct {
+ events []telemetry.Event
+}
+
+func (cli *testTelemetryClient) Track(event telemetry.Event) error {
+ cli.events = append(cli.events, event)
+ return nil
+}
+
+func (cli *testTelemetryClient) Close() error { return nil }
+
+func TestTelemetry(t *testing.T) {
+ telemetryClient := testTelemetryClient{make([]telemetry.Event, 0)}
+ // Test server
+ var serverHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`[{"id":"id", "name":"name"}]`))
+ }
+ server := httptest.NewServer(serverHandler)
+ defer server.Close()
+
+ // Test command
+ config := &settings.Config{
+ Token: "testtoken",
+ HTTPClient: http.DefaultClient,
+ Host: server.URL,
+ }
+ cmd := NewInfoCommand(config, nil)
+ cmd.SetArgs([]string{"org"})
+ ctx := cmd.Context()
+ if ctx == nil {
+ ctx = context.Background()
+ }
+ cmd.SetContext(telemetry.NewContext(ctx, &telemetryClient))
+
+ // Execute
+ err := cmd.Execute()
+
+ assert.NilError(t, err)
+
+ // Read the telemetry events and compare them
+ assert.DeepEqual(t, telemetryClient.events, []telemetry.Event{
+ telemetry.CreateInfoEvent(telemetry.CommandInfo{
+ Name: "org",
+ LocalArgs: map[string]string{"help": "false"},
+ }, nil),
+ })
+}
+
func defaultValidator(cmd *cobra.Command, args []string) error {
return nil
}
@@ -115,9 +166,10 @@ func scaffoldCMD(
validator validator.Validator,
) (*cobra.Command, *bytes.Buffer, *bytes.Buffer) {
config := &settings.Config{
- Token: "testtoken",
- HTTPClient: http.DefaultClient,
- Host: baseURL,
+ Token: "testtoken",
+ HTTPClient: http.DefaultClient,
+ Host: baseURL,
+ IsTelemetryDisabled: true,
}
cmd := NewInfoCommand(config, validator)
diff --git a/cmd/namespace.go b/cmd/namespace.go
index 0e5fb8cc5..37cf6cbab 100644
--- a/cmd/namespace.go
+++ b/cmd/namespace.go
@@ -8,6 +8,7 @@ import (
"github.com/CircleCI-Public/circleci-cli/api/graphql"
"github.com/CircleCI-Public/circleci-cli/prompt"
"github.com/CircleCI-Public/circleci-cli/settings"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
"github.com/google/uuid"
"github.com/spf13/cobra"
)
@@ -74,7 +75,14 @@ Please note that at this time all namespaces created in the registry are world-r
}
}
- return createNamespace(cmd, opts)
+ err := createNamespace(cmd, opts)
+
+ telemetryClient, ok := telemetry.FromContext(cmd.Context())
+ if ok {
+ _ = telemetryClient.Track(telemetry.CreateNamespaceEvent(telemetry.GetCommandInformation(cmd, true)))
+ }
+
+ return err
},
Args: cobra.RangeArgs(1, 3),
Annotations: make(map[string]string),
diff --git a/cmd/namespace_test.go b/cmd/namespace_test.go
index 189e4901e..216817c96 100644
--- a/cmd/namespace_test.go
+++ b/cmd/namespace_test.go
@@ -6,6 +6,7 @@ import (
"os/exec"
"github.com/CircleCI-Public/circleci-cli/clitest"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
@@ -27,6 +28,42 @@ var _ = Describe("Namespace integration tests", func() {
tempSettings.Close()
})
+ Describe("telemetry", func() {
+ It("sends expected event", func() {
+ command = exec.Command(pathCLI,
+ "namespace", "create",
+ "--skip-update-check",
+ "--token", token,
+ "--host", tempSettings.TestServer.URL(),
+ "--integration-testing",
+ "foo-ns",
+ "--org-id", `"bb604b45-b6b0-4b81-ad80-796f15eddf87"`,
+ )
+ command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath))
+
+ tempSettings.TestServer.AppendHandlers(func(res http.ResponseWriter, req *http.Request) {
+ res.WriteHeader(http.StatusOK)
+ _, _ = res.Write([]byte(`{"data":{"organization":{"name":"test-org","id":"bb604b45-b6b0-4b81-ad80-796f15eddf87"}}}`))
+ })
+
+ session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
+ Expect(err).ShouldNot(HaveOccurred())
+ Eventually(session).Should(gexec.Exit(0))
+
+ clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{
+ telemetry.CreateNamespaceEvent(telemetry.CommandInfo{
+ Name: "create",
+ LocalArgs: map[string]string{
+ "help": "false",
+ "integration-testing": "true",
+ "no-prompt": "false",
+ "org-id": "\"bb604b45-b6b0-4b81-ad80-796f15eddf87\"",
+ },
+ }),
+ })
+ })
+ })
+
Context("create, with interactive prompts", func() {
Describe("registering a namespace with orgID", func() {
BeforeEach(func() {
diff --git a/cmd/open.go b/cmd/open.go
index 6f1772b1d..2d9183550 100644
--- a/cmd/open.go
+++ b/cmd/open.go
@@ -6,6 +6,8 @@ import (
"strings"
"github.com/CircleCI-Public/circleci-cli/git"
+ "github.com/CircleCI-Public/circleci-cli/settings"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
"github.com/pkg/browser"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@@ -13,7 +15,7 @@ import (
// errorMessage string containing the error message displayed in both the open command and the follow command
var errorMessage = `
-This command is intended to be run from a git repository with a remote named 'origin' that is hosted on Github or Bitbucket only.
+This command is intended to be run from a git repository with a remote named 'origin' that is hosted on Github or Bitbucket only.
We are not currently supporting any other hosts.`
// projectUrl uses the provided values to create the url to open
@@ -39,12 +41,19 @@ func openProjectInBrowser() error {
}
// newOpenCommand creates the cli command open
-func newOpenCommand() *cobra.Command {
+func newOpenCommand(config *settings.Config) *cobra.Command {
openCommand := &cobra.Command{
Use: "open",
Short: "Open the current project in the browser.",
- RunE: func(_ *cobra.Command, _ []string) error {
- return openProjectInBrowser()
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ err := openProjectInBrowser()
+
+ telemetryClient, ok := telemetry.FromContext(cmd.Context())
+ if ok {
+ _ = telemetryClient.Track(telemetry.CreateOpenEvent(err))
+ }
+
+ return err
},
}
return openCommand
diff --git a/cmd/orb.go b/cmd/orb.go
index d7a011eb3..8a82fe60a 100644
--- a/cmd/orb.go
+++ b/cmd/orb.go
@@ -26,6 +26,7 @@ import (
"github.com/CircleCI-Public/circleci-cli/prompt"
"github.com/CircleCI-Public/circleci-cli/references"
"github.com/CircleCI-Public/circleci-cli/settings"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
"github.com/CircleCI-Public/circleci-cli/version"
"github.com/fatih/color"
"github.com/pkg/errors"
@@ -389,6 +390,11 @@ Please note that at this time all orbs created in the registry are world-readabl
opts.args = args
opts.cl = graphql.NewClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug)
+ telemetryClient, ok := telemetry.FromContext(cmd.Context())
+ if ok {
+ _ = telemetryClient.Track(telemetry.CreateOrbEvent(telemetry.GetCommandInformation(cmd, true)))
+ }
+
// PersistentPreRunE overwrites the inherited persistent hook from rootCmd
// So we explicitly call it here to retain that behavior.
// As of writing this comment, that is only for daily update checks.
diff --git a/cmd/orb_test.go b/cmd/orb_test.go
index 4804d1d6a..9d26392d7 100644
--- a/cmd/orb_test.go
+++ b/cmd/orb_test.go
@@ -16,6 +16,7 @@ import (
"github.com/CircleCI-Public/circleci-cli/api"
"github.com/CircleCI-Public/circleci-cli/api/graphql"
"github.com/CircleCI-Public/circleci-cli/clitest"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
@@ -23,6 +24,57 @@ import (
"github.com/onsi/gomega/gexec"
)
+var _ = Describe("Orb telemetry", func() {
+ var (
+ command *exec.Cmd
+ orb *clitest.TmpFile
+ tempSettings *clitest.TempSettings
+ )
+
+ BeforeEach(func() {
+ tempSettings = clitest.WithTempSettings()
+ orb = clitest.OpenTmpFile(tempSettings.Home, "orb.yml")
+ command = exec.Command(pathCLI,
+ "orb", "validate", orb.Path,
+ "--skip-update-check",
+ "--token", "token",
+ "--host", tempSettings.TestServer.URL(),
+ )
+ command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath))
+ })
+
+ AfterEach(func() {
+ orb.Close()
+ tempSettings.Close()
+ })
+
+ It("works", func() {
+ orb.Write([]byte(`{}`))
+
+ mockOrbIntrospection(true, "", tempSettings)
+
+ tempSettings.TestServer.AppendHandlers(func(res http.ResponseWriter, req *http.Request) {
+ res.WriteHeader(http.StatusOK)
+ _, _ = res.Write([]byte(`{"orbConfig": {"sourceYaml": "{}", "valid": true, "errors": []} }`))
+ })
+
+ session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
+ Expect(err).ShouldNot(HaveOccurred())
+ Eventually(session).Should(gexec.Exit(0))
+
+ clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{
+ telemetry.CreateOrbEvent(telemetry.CommandInfo{
+ Name: "validate",
+ LocalArgs: map[string]string{
+ "org-slug": "",
+ "help": "false",
+ "org-id": "",
+ },
+ }),
+ })
+ })
+})
+
var _ = Describe("Orb integration tests", func() {
Describe("Orb help text", func() {
It("shows a link to the docs", func() {
diff --git a/cmd/policy/policy.go b/cmd/policy/policy.go
index 6ccab181c..19147fbc3 100644
--- a/cmd/policy/policy.go
+++ b/cmd/policy/policy.go
@@ -26,15 +26,27 @@ import (
"github.com/CircleCI-Public/circleci-cli/api/rest"
"github.com/CircleCI-Public/circleci-cli/cmd/validator"
"github.com/CircleCI-Public/circleci-cli/config"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
+
"github.com/CircleCI-Public/circleci-cli/settings"
)
// NewCommand creates the root policy command with all policy subcommands attached.
func NewCommand(globalConfig *settings.Config, preRunE validator.Validator) *cobra.Command {
cmd := &cobra.Command{
- Use: "policy",
- PersistentPreRunE: preRunE,
- Short: "Manage security policies",
+ Use: "policy",
+ PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+ telemetryClient, ok := telemetry.FromContext(cmd.Context())
+ if ok {
+ _ = telemetryClient.Track(telemetry.CreatePolicyEvent(telemetry.GetCommandInformation(cmd, true)))
+ }
+
+ if preRunE != nil {
+ return preRunE(cmd, args)
+ }
+ return nil
+ },
+ Short: "Manage security policies",
Long: `Policies ensures security of build configs via security policy management framework.
This group of commands allows the management of polices to be verified against build configs.`,
}
diff --git a/cmd/policy/policy_test.go b/cmd/policy/policy_test.go
index 227a6a66d..d4c503afc 100644
--- a/cmd/policy/policy_test.go
+++ b/cmd/policy/policy_test.go
@@ -57,7 +57,7 @@ func TestPushPolicyWithPrompt(t *testing.T) {
}))
defer svr.Close()
- config := &settings.Config{Token: "testtoken", HTTPClient: http.DefaultClient}
+ config := &settings.Config{Token: "testtoken", HTTPClient: http.DefaultClient, IsTelemetryDisabled: true}
cmd := NewCommand(config, nil)
buffer := makeSafeBuffer()
@@ -1389,10 +1389,11 @@ func TestTestRunner(t *testing.T) {
func makeCMD(circleHost string, token string) (*cobra.Command, *bytes.Buffer, *bytes.Buffer) {
config := &settings.Config{
- Host: circleHost,
- Token: token,
- RestEndpoint: "/api/v2",
- HTTPClient: http.DefaultClient,
+ Host: circleHost,
+ Token: token,
+ RestEndpoint: "/api/v2",
+ HTTPClient: http.DefaultClient,
+ IsTelemetryDisabled: true,
}
cmd := NewCommand(config, nil)
diff --git a/cmd/root.go b/cmd/root.go
index 942278c69..05e028a99 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -1,6 +1,7 @@
package cmd
import (
+ "context"
"fmt"
"log"
"os"
@@ -14,6 +15,7 @@ import (
"github.com/CircleCI-Public/circleci-cli/data"
"github.com/CircleCI-Public/circleci-cli/md_docs"
"github.com/CircleCI-Public/circleci-cli/settings"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
"github.com/CircleCI-Public/circleci-cli/version"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
@@ -36,16 +38,23 @@ var rootOptions *settings.Config
// rootTokenFromFlag stores the value passed in through the flag --token
var rootTokenFromFlag string
-// Execute adds all child commands to rootCmd and
-// sets flags appropriately. This function is called
-// by main.main(). It only needs to happen once to
-// the rootCmd.
-func Execute() {
+// Execute adds all child commands to rootCmd, sets the flags appropriately
+// and put the telemetry client in the command context. This function is
+// called by main.main(). It only needs to happen once to the rootCmd
+func Execute() error {
header.SetCommandStr(CommandStr())
command := MakeCommands()
- if err := command.Execute(); err != nil {
- os.Exit(-1)
+
+ telemetryClient := CreateTelemetry(rootOptions)
+ defer telemetryClient.Close()
+
+ cmdContext := command.Context()
+ if cmdContext == nil {
+ cmdContext = context.Background()
}
+ command.SetContext(telemetry.NewContext(cmdContext, telemetryClient))
+
+ return command.Execute()
}
// Returns a string (e.g. "circleci context list") indicating what
@@ -123,7 +132,7 @@ func MakeCommands() *cobra.Command {
Use: "circleci",
Long: longHelp,
Short: rootHelpShort(rootOptions),
- PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
+ PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
return rootCmdPreRun(rootOptions)
},
}
@@ -144,7 +153,7 @@ func MakeCommands() *cobra.Command {
return validateToken(rootOptions)
}
- rootCmd.AddCommand(newOpenCommand())
+ rootCmd.AddCommand(newOpenCommand(rootOptions))
rootCmd.AddCommand(newTestsCommand())
rootCmd.AddCommand(newContextCommand(rootOptions))
rootCmd.AddCommand(project.NewProjectCommand(rootOptions, validator))
@@ -173,8 +182,9 @@ func MakeCommands() *cobra.Command {
rootCmd.AddCommand(newStepCommand(rootOptions))
rootCmd.AddCommand(newSwitchCommand(rootOptions))
rootCmd.AddCommand(newAdminCommand(rootOptions))
- rootCmd.AddCommand(newCompletionCommand())
+ rootCmd.AddCommand(newCompletionCommand(rootOptions))
rootCmd.AddCommand(newEnvCmd())
+ rootCmd.AddCommand(newTelemetryCommand(rootOptions))
flags := rootCmd.PersistentFlags()
@@ -184,8 +194,9 @@ func MakeCommands() *cobra.Command {
flags.StringVar(&rootOptions.Endpoint, "endpoint", rootOptions.Endpoint, "URI to your CircleCI GraphQL API endpoint")
flags.StringVar(&rootOptions.GitHubAPI, "github-api", "https://api.github.com/", "Change the default endpoint to GitHub API for retrieving updates")
flags.BoolVar(&rootOptions.SkipUpdateCheck, "skip-update-check", skipUpdateByDefault(), "Skip the check for updates check run before every command.")
+ flags.StringVar(&rootOptions.MockTelemetry, "mock-telemetry", "", "The path where telemetry must be written")
- hidden := []string{"github-api", "debug", "endpoint"}
+ hidden := []string{"github-api", "debug", "endpoint", "mock-telemetry"}
for _, f := range hidden {
if err := flags.MarkHidden(f); err != nil {
@@ -227,6 +238,7 @@ func rootCmdPreRun(rootOptions *settings.Config) error {
fmt.Printf("Error checking for updates: %s\n", err)
fmt.Printf("Please contact support.\n\n")
}
+
return nil
}
diff --git a/cmd/root_test.go b/cmd/root_test.go
index c23982b18..d05a85419 100644
--- a/cmd/root_test.go
+++ b/cmd/root_test.go
@@ -16,7 +16,7 @@ var _ = Describe("Root", func() {
Describe("subcommands", func() {
It("can create commands", func() {
commands := cmd.MakeCommands()
- Expect(len(commands.Commands())).To(Equal(24))
+ Expect(len(commands.Commands())).To(Equal(25))
})
})
@@ -52,8 +52,7 @@ var _ = Describe("Root", func() {
Eventually(session.Err.Contents()).Should(BeEmpty())
- Eventually(session.Out).Should(gbytes.Say("update This command is unavailable on your platform"))
-
+ Eventually(session.Out).Should(gbytes.Say("update\\s+This command is unavailable on your platform"))
Eventually(session).Should(gexec.Exit(0))
})
@@ -92,8 +91,7 @@ var _ = Describe("Root", func() {
Eventually(session.Err.Contents()).Should(BeEmpty())
- Eventually(session.Out).Should(gbytes.Say("update Update the tool to the latest version"))
-
+ Eventually(session.Out).Should(gbytes.Say("update\\s+Update the tool to the latest version"))
Eventually(session).Should(gexec.Exit(0))
})
})
diff --git a/cmd/runner/instance.go b/cmd/runner/instance.go
index 28f260937..a4da71338 100644
--- a/cmd/runner/instance.go
+++ b/cmd/runner/instance.go
@@ -9,6 +9,7 @@ import (
"github.com/CircleCI-Public/circleci-cli/api/runner"
"github.com/CircleCI-Public/circleci-cli/cmd/validator"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
)
func newRunnerInstanceCommand(o *runnerOpts, preRunE validator.Validator) *cobra.Command {
@@ -25,7 +26,17 @@ func newRunnerInstanceCommand(o *runnerOpts, preRunE validator.Validator) *cobra
Aliases: []string{"ls"},
Args: cobra.ExactArgs(1),
PreRunE: preRunE,
- RunE: func(_ *cobra.Command, args []string) error {
+ RunE: func(cmd *cobra.Command, args []string) error {
+ var err error
+
+ telemetryClient, ok := telemetry.FromContext(cmd.Context())
+ if ok {
+ // We defer the call to be sure the `err` has been filled
+ defer (func() {
+ _ = telemetryClient.Track(telemetry.CreateRunnerInstanceEvent(telemetry.GetCommandInformation(cmd, true), err))
+ })()
+ }
+
runners, err := o.r.GetRunnerInstances(args[0])
if err != nil {
return err
diff --git a/cmd/runner/resource_class.go b/cmd/runner/resource_class.go
index 1a727737e..4b68b37ae 100644
--- a/cmd/runner/resource_class.go
+++ b/cmd/runner/resource_class.go
@@ -8,12 +8,19 @@ import (
"github.com/CircleCI-Public/circleci-cli/api/runner"
"github.com/CircleCI-Public/circleci-cli/cmd/validator"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
)
func newResourceClassCommand(o *runnerOpts, preRunE validator.Validator) *cobra.Command {
cmd := &cobra.Command{
Use: "resource-class",
Short: "Operate on runner resource-classes",
+ PersistentPreRun: func(cmd *cobra.Command, _ []string) {
+ telemetryClient, ok := telemetry.FromContext(cmd.Context())
+ if ok {
+ _ = telemetryClient.Track(telemetry.CreateRunnerResourceClassEvent(telemetry.GetCommandInformation(cmd, true)))
+ }
+ },
}
genToken := false
diff --git a/cmd/runner/telemetry_test.go b/cmd/runner/telemetry_test.go
new file mode 100644
index 000000000..dcd12874c
--- /dev/null
+++ b/cmd/runner/telemetry_test.go
@@ -0,0 +1,62 @@
+package runner
+
+import (
+ "bytes"
+ "context"
+ "testing"
+
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
+ "gotest.tools/v3/assert"
+)
+
+type testTelemetryClient struct {
+ events []telemetry.Event
+}
+
+func (c *testTelemetryClient) Track(event telemetry.Event) error {
+ c.events = append(c.events, event)
+ return nil
+}
+
+func (c *testTelemetryClient) Close() error { return nil }
+
+func Test_RunnerTelemetry(t *testing.T) {
+ t.Run("resource-class", func(t *testing.T) {
+ telemetryClient := &testTelemetryClient{make([]telemetry.Event, 0)}
+ runner := runnerMock{}
+ cmd := newResourceClassCommand(&runnerOpts{r: &runner}, nil)
+ stdout := new(bytes.Buffer)
+ stderr := new(bytes.Buffer)
+ cmd.SetOut(stdout)
+ cmd.SetErr(stderr)
+ ctx := cmd.Context()
+ if ctx == nil {
+ ctx = context.Background()
+ }
+ cmd.SetContext(telemetry.NewContext(ctx, telemetryClient))
+
+ defer runner.reset()
+ defer stdout.Reset()
+ defer stderr.Reset()
+
+ cmd.SetArgs([]string{
+ "create",
+ "my-namespace/my-other-resource-class",
+ "my-description",
+ "--generate-token",
+ })
+
+ err := cmd.Execute()
+ assert.NilError(t, err)
+
+ assert.DeepEqual(t, telemetryClient.events, []telemetry.Event{
+ telemetry.CreateRunnerResourceClassEvent(telemetry.CommandInfo{
+ Name: "create",
+ LocalArgs: map[string]string{
+ "generate-token": "true",
+ "help": "false",
+ },
+ }),
+ })
+ })
+}
diff --git a/cmd/runner/token.go b/cmd/runner/token.go
index 4771b37e9..60edcbf6e 100644
--- a/cmd/runner/token.go
+++ b/cmd/runner/token.go
@@ -4,6 +4,7 @@ import (
"time"
"github.com/CircleCI-Public/circleci-cli/cmd/validator"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
)
@@ -12,6 +13,12 @@ func newTokenCommand(o *runnerOpts, preRunE validator.Validator) *cobra.Command
cmd := &cobra.Command{
Use: "token",
Short: "Operate on runner tokens",
+ PersistentPreRun: func(cmd *cobra.Command, _ []string) {
+ telemetryClient, ok := telemetry.FromContext(cmd.Context())
+ if ok {
+ _ = telemetryClient.Track(telemetry.CreateRunnerResourceClassEvent(telemetry.GetCommandInformation(cmd, true)))
+ }
+ },
}
cmd.AddCommand(&cobra.Command{
diff --git a/cmd/setup.go b/cmd/setup.go
index 6a2d068b5..efc30288a 100644
--- a/cmd/setup.go
+++ b/cmd/setup.go
@@ -7,6 +7,7 @@ import (
"github.com/CircleCI-Public/circleci-cli/api/graphql"
"github.com/CircleCI-Public/circleci-cli/prompt"
"github.com/CircleCI-Public/circleci-cli/settings"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@@ -123,7 +124,13 @@ func newSetupCommand(config *settings.Config) *cobra.Command {
opts.args = args
opts.cl = graphql.NewClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug)
},
- RunE: func(_ *cobra.Command, _ []string) error {
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ telemetryClient, ok := telemetry.FromContext(cmd.Context())
+ if ok {
+ // We defer the call to make sure the `opts.cfg.Host` has been filled
+ defer (func() { _ = telemetryClient.Track(telemetry.CreateSetupEvent(opts.cfg.Host != defaultHost)) })()
+ }
+
if opts.integrationTesting {
opts.tty = setupTestUI{
host: "boondoggle",
diff --git a/cmd/setup_test.go b/cmd/setup_test.go
index 591a6197d..8ae09473c 100644
--- a/cmd/setup_test.go
+++ b/cmd/setup_test.go
@@ -9,12 +9,44 @@ import (
"runtime"
"github.com/CircleCI-Public/circleci-cli/clitest"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/onsi/gomega/gexec"
)
+var _ = Describe("Setup telemetry", func() {
+ var (
+ command *exec.Cmd
+ tempSettings *clitest.TempSettings
+ )
+
+ BeforeEach(func() {
+ tempSettings = clitest.WithTempSettings()
+ command = commandWithHome(pathCLI, tempSettings.Home,
+ "setup",
+ "--integration-testing",
+ "--skip-update-check",
+ )
+ command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath))
+ })
+
+ AfterEach(func() {
+ tempSettings.Close()
+ })
+
+ It("should send telemetry event", func() {
+ session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
+ Expect(err).ShouldNot(HaveOccurred())
+
+ Eventually(session).Should(gexec.Exit(0))
+ clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{
+ telemetry.CreateSetupEvent(true),
+ })
+ })
+})
+
var _ = Describe("Setup with prompts", func() {
var (
command *exec.Cmd
diff --git a/cmd/telemetry.go b/cmd/telemetry.go
new file mode 100644
index 000000000..85e231f74
--- /dev/null
+++ b/cmd/telemetry.go
@@ -0,0 +1,79 @@
+package cmd
+
+import (
+ "os"
+
+ "github.com/CircleCI-Public/circleci-cli/settings"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
+ "github.com/pkg/errors"
+ "github.com/spf13/cobra"
+)
+
+func newTelemetryCommand(config *settings.Config) *cobra.Command {
+ apiClient := CreateAPIClient(config)
+
+ telemetryEnable := &cobra.Command{
+ Use: "enable",
+ Short: "Allow telemetry events to be sent to CircleCI servers",
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ err := setIsTelemetryActive(apiClient, true)
+ if telemetryClient, ok := telemetry.FromContext(cmd.Context()); ok {
+ _ = telemetryClient.Track(telemetry.CreateChangeTelemetryStatusEvent("enabled", "telemetry-command", err))
+ }
+ return err
+ },
+ Args: cobra.ExactArgs(0),
+ }
+
+ telemetryDisable := &cobra.Command{
+ Use: "disable",
+ Short: "Make sure no telemetry events is sent to CircleCI servers",
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ err := setIsTelemetryActive(apiClient, false)
+ if telemetryClient, ok := telemetry.FromContext(cmd.Context()); ok {
+ _ = telemetryClient.Track(telemetry.CreateChangeTelemetryStatusEvent("disabled", "telemetry-command", err))
+ }
+ return err
+ },
+ Args: cobra.ExactArgs(0),
+ }
+
+ telemetryCommand := &cobra.Command{
+ Use: "telemetry",
+ Short: "Configure telemetry preferences",
+ Long: `Configure telemetry preferences.
+
+Note: If you have not configured your telemetry preferences and call the CLI with a closed stdin, telemetry will be disabled`,
+ }
+
+ telemetryCommand.AddCommand(telemetryEnable)
+ telemetryCommand.AddCommand(telemetryDisable)
+
+ return telemetryCommand
+}
+
+func setIsTelemetryActive(apiClient TelemetryAPIClient, isActive bool) error {
+ settings := settings.TelemetrySettings{}
+ if err := settings.Load(); err != nil && !os.IsNotExist(err) {
+ return errors.Wrap(err, "Loading telemetry configuration")
+ }
+
+ settings.HasAnsweredPrompt = true
+ settings.IsEnabled = isActive
+
+ if settings.UniqueID == "" {
+ settings.UniqueID = CreateUUID()
+ }
+
+ if settings.UserID == "" {
+ if myID, err := apiClient.GetMyUserId(); err == nil {
+ settings.UserID = myID
+ }
+ }
+
+ if err := settings.Write(); err != nil {
+ return errors.Wrap(err, "Writing telemetry configuration")
+ }
+
+ return nil
+}
diff --git a/cmd/telemetry_test.go b/cmd/telemetry_test.go
new file mode 100644
index 000000000..fb8d334db
--- /dev/null
+++ b/cmd/telemetry_test.go
@@ -0,0 +1,58 @@
+package cmd_test
+
+import (
+ "fmt"
+ "os/exec"
+
+ "github.com/CircleCI-Public/circleci-cli/clitest"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
+ "github.com/onsi/gomega/gexec"
+
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("Telemetry events on telemetry commands", func() {
+ var (
+ tempSettings *clitest.TempSettings
+ command *exec.Cmd
+ )
+
+ BeforeEach(func() {
+ tempSettings = clitest.WithTempSettings()
+ })
+
+ AfterEach(func() {
+ tempSettings.Close()
+ })
+
+ Describe("telemetry enable", func() {
+ It("should send an event", func() {
+ command = exec.Command(pathCLI, "telemetry", "enable")
+ command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath))
+
+ session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
+ Expect(err).ShouldNot(HaveOccurred())
+ Eventually(session).Should(gexec.Exit(0))
+
+ clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{
+ telemetry.CreateChangeTelemetryStatusEvent("enabled", "telemetry-command", nil),
+ })
+ })
+ })
+
+ Describe("telemetry disable", func() {
+ It("should send an event", func() {
+ command = exec.Command(pathCLI, "telemetry", "disable")
+ command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath))
+
+ session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
+ Expect(err).ShouldNot(HaveOccurred())
+ Eventually(session).Should(gexec.Exit(0))
+
+ clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{
+ telemetry.CreateChangeTelemetryStatusEvent("disabled", "telemetry-command", nil),
+ })
+ })
+ })
+})
diff --git a/cmd/telemetry_unit_test.go b/cmd/telemetry_unit_test.go
new file mode 100644
index 000000000..468e1ce9d
--- /dev/null
+++ b/cmd/telemetry_unit_test.go
@@ -0,0 +1,143 @@
+package cmd
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/CircleCI-Public/circleci-cli/settings"
+ "github.com/spf13/afero"
+ "gotest.tools/v3/assert"
+)
+
+func TestSetIsTelemetryActive(t *testing.T) {
+ type args struct {
+ apiClient TelemetryAPIClient
+ isActive bool
+ settings *settings.TelemetrySettings
+ }
+ type want struct {
+ settings *settings.TelemetrySettings
+ }
+
+ type testCase struct {
+ name string
+ args args
+ want want
+ }
+
+ userId := "user-id"
+ uniqueId := "unique-id"
+
+ testCases := []testCase{
+ {
+ name: "Enabling telemetry with settings should just update the is active field",
+ args: args{
+ apiClient: telemetryTestAPIClient{},
+ isActive: true,
+ settings: &settings.TelemetrySettings{
+ IsEnabled: false,
+ HasAnsweredPrompt: true,
+ UniqueID: uniqueId,
+ UserID: userId,
+ },
+ },
+ want: want{
+ settings: &settings.TelemetrySettings{
+ IsEnabled: true,
+ HasAnsweredPrompt: true,
+ UniqueID: uniqueId,
+ UserID: userId,
+ },
+ },
+ },
+ {
+ name: "Enabling telemetry without settings should fill the settings fields",
+ args: args{
+ apiClient: telemetryTestAPIClient{id: userId, err: nil},
+ isActive: true,
+ settings: nil,
+ },
+ want: want{
+ settings: &settings.TelemetrySettings{
+ IsEnabled: true,
+ HasAnsweredPrompt: true,
+ UniqueID: uniqueId,
+ UserID: userId,
+ },
+ },
+ },
+ {
+ name: "Disabling telemetry with settings should just update the is active field",
+ args: args{
+ apiClient: telemetryTestAPIClient{},
+ isActive: false,
+ settings: &settings.TelemetrySettings{
+ IsEnabled: true,
+ HasAnsweredPrompt: true,
+ UniqueID: uniqueId,
+ UserID: userId,
+ },
+ },
+ want: want{
+ settings: &settings.TelemetrySettings{
+ IsEnabled: false,
+ HasAnsweredPrompt: true,
+ UniqueID: uniqueId,
+ UserID: userId,
+ },
+ },
+ },
+ {
+ name: "Enabling telemetry without settings should fill the settings fields",
+ args: args{
+ apiClient: telemetryTestAPIClient{id: userId, err: nil},
+ isActive: false,
+ settings: nil,
+ },
+ want: want{
+ settings: &settings.TelemetrySettings{
+ IsEnabled: false,
+ HasAnsweredPrompt: true,
+ UniqueID: uniqueId,
+ UserID: userId,
+ },
+ },
+ },
+ }
+
+ // Mock create UUID
+ oldUUIDCreate := CreateUUID
+ CreateUUID = func() string { return uniqueId }
+ defer (func() { CreateUUID = oldUUIDCreate })()
+
+ for _, tt := range testCases {
+ t.Run(tt.name, func(t *testing.T) {
+ // Mock FS
+ oldFS := settings.FS.Fs
+ settings.FS.Fs = afero.NewMemMapFs()
+ defer (func() { settings.FS.Fs = oldFS })()
+
+ if tt.args.settings != nil {
+ err := tt.args.settings.Write()
+ assert.NilError(t, err)
+ }
+
+ err := setIsTelemetryActive(tt.args.apiClient, tt.args.isActive)
+ assert.NilError(t, err)
+
+ exist, err := settings.FS.Exists(filepath.Join(settings.SettingsPath(), "telemetry.yml"))
+ assert.NilError(t, err)
+ if tt.want.settings == nil {
+ assert.Equal(t, exist, false)
+ } else {
+ assert.Equal(t, exist, true)
+
+ loadedSettings := &settings.TelemetrySettings{}
+ err := loadedSettings.Load()
+ assert.NilError(t, err)
+
+ assert.DeepEqual(t, tt.want.settings, loadedSettings)
+ }
+ })
+ }
+}
diff --git a/cmd/update.go b/cmd/update.go
index c43bf33d4..5ef2bcc44 100644
--- a/cmd/update.go
+++ b/cmd/update.go
@@ -5,6 +5,7 @@ import (
"time"
"github.com/CircleCI-Public/circleci-cli/settings"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
"github.com/CircleCI-Public/circleci-cli/update"
"github.com/CircleCI-Public/circleci-cli/version"
@@ -28,8 +29,13 @@ func newUpdateCommand(config *settings.Config) *cobra.Command {
update := &cobra.Command{
Use: "update",
Short: "Update the tool to the latest version",
- PersistentPreRun: func(_ *cobra.Command, _ []string) {
+ PersistentPreRun: func(cmd *cobra.Command, _ []string) {
opts.cfg.SkipUpdateCheck = true
+
+ telemetryClient, ok := telemetry.FromContext(cmd.Context())
+ if ok {
+ _ = telemetryClient.Track(telemetry.CreateUpdateEvent(telemetry.GetCommandInformation(cmd, cmd.Name() != "update")))
+ }
},
PreRun: func(cmd *cobra.Command, args []string) {
opts.args = args
@@ -43,9 +49,6 @@ func newUpdateCommand(config *settings.Config) *cobra.Command {
Use: "check",
Hidden: true,
Short: "Check if there are any updates available",
- PersistentPreRun: func(_ *cobra.Command, _ []string) {
- opts.cfg.SkipUpdateCheck = true
- },
PreRun: func(cmd *cobra.Command, args []string) {
opts.args = args
opts.dryRun = true
@@ -59,9 +62,6 @@ func newUpdateCommand(config *settings.Config) *cobra.Command {
Use: "install",
Hidden: true,
Short: "Update the tool to the latest version",
- PersistentPreRun: func(_ *cobra.Command, _ []string) {
- opts.cfg.SkipUpdateCheck = true
- },
PreRun: func(cmd *cobra.Command, args []string) {
opts.args = args
},
@@ -74,9 +74,6 @@ func newUpdateCommand(config *settings.Config) *cobra.Command {
Use: "build-agent",
Hidden: true,
Short: "This command has no effect, and is kept for backwards compatibility",
- PersistentPreRun: func(_ *cobra.Command, _ []string) {
- opts.cfg.SkipUpdateCheck = true
- },
PreRun: func(cmd *cobra.Command, args []string) {
opts.args = args
},
diff --git a/cmd/update_test.go b/cmd/update_test.go
index 431987119..6ed7aa5dd 100644
--- a/cmd/update_test.go
+++ b/cmd/update_test.go
@@ -1,11 +1,13 @@
package cmd_test
import (
+ "fmt"
"net/http"
"os/exec"
"path/filepath"
"github.com/CircleCI-Public/circleci-cli/clitest"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
@@ -70,6 +72,69 @@ var _ = Describe("Update", func() {
tempSettings.Close()
})
+ Describe("telemetry", func() {
+ It("should send telemetry event when calling parent command", func() {
+ updateCLI, err := gexec.Build("github.com/CircleCI-Public/circleci-cli")
+ Expect(err).ShouldNot(HaveOccurred())
+
+ command = exec.Command(updateCLI,
+ "update",
+ "--github-api", tempSettings.TestServer.URL(),
+ )
+ command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath))
+
+ assetBytes := golden.Get(GinkgoT(), filepath.FromSlash("update/foo.zip"))
+ assetResponse := string(assetBytes)
+
+ tempSettings.TestServer.AppendHandlers(
+ ghttp.CombineHandlers(
+ ghttp.VerifyRequest(http.MethodGet, "/repos/CircleCI-Public/circleci-cli/releases"),
+ ghttp.RespondWith(http.StatusOK, response),
+ ),
+ ghttp.CombineHandlers(
+ ghttp.VerifyRequest(http.MethodGet, "/repos/CircleCI-Public/circleci-cli/releases/assets/1"),
+ ghttp.RespondWith(http.StatusOK, assetResponse),
+ ),
+ )
+
+ session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
+ Expect(err).ShouldNot(HaveOccurred())
+
+ Eventually(session).Should(gexec.Exit(0))
+ clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{
+ telemetry.CreateUpdateEvent(telemetry.CommandInfo{
+ Name: "update",
+ LocalArgs: map[string]string{
+ "check": "false",
+ "help": "false",
+ },
+ }),
+ })
+ })
+
+ It("should send telemetry event when calling child command", func() {
+ command = exec.Command(pathCLI,
+ "update",
+ "check",
+ "--github-api", tempSettings.TestServer.URL(),
+ )
+ command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath))
+ session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
+ Expect(err).ShouldNot(HaveOccurred())
+
+ Eventually(session).Should(gexec.Exit(0))
+ clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{
+ telemetry.CreateUpdateEvent(telemetry.CommandInfo{
+ Name: "check",
+ LocalArgs: map[string]string{
+ "check": "false",
+ "help": "false",
+ },
+ }),
+ })
+ })
+ })
+
Describe("update --check", func() {
BeforeEach(func() {
command = exec.Command(pathCLI,
diff --git a/cmd/version.go b/cmd/version.go
index 756e5e6ce..e2cd566ce 100644
--- a/cmd/version.go
+++ b/cmd/version.go
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/CircleCI-Public/circleci-cli/settings"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
"github.com/CircleCI-Public/circleci-cli/version"
"github.com/spf13/cobra"
)
@@ -24,11 +25,18 @@ func newVersionCommand(config *settings.Config) *cobra.Command {
PersistentPreRun: func(_ *cobra.Command, _ []string) {
opts.cfg.SkipUpdateCheck = true
},
- PreRun: func(cmd *cobra.Command, args []string) {
+ PreRun: func(_ *cobra.Command, args []string) {
opts.args = args
},
- Run: func(_ *cobra.Command, _ []string) {
- fmt.Printf("%s+%s (%s)\n", version.Version, version.Commit, version.PackageManager())
+ Run: func(cmd *cobra.Command, _ []string) {
+ version := fmt.Sprintf("%s+%s (%s)", version.Version, version.Commit, version.PackageManager())
+
+ telemetryClient, ok := telemetry.FromContext(cmd.Context())
+ if ok {
+ _ = telemetryClient.Track(telemetry.CreateVersionEvent(version))
+ }
+
+ fmt.Printf("%s\n", version)
},
}
}
diff --git a/cmd/version_test.go b/cmd/version_test.go
new file mode 100644
index 000000000..e64cf051e
--- /dev/null
+++ b/cmd/version_test.go
@@ -0,0 +1,39 @@
+package cmd_test
+
+import (
+ "fmt"
+ "os/exec"
+
+ "github.com/CircleCI-Public/circleci-cli/clitest"
+ "github.com/CircleCI-Public/circleci-cli/telemetry"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ "github.com/onsi/gomega/gexec"
+)
+
+var _ = Describe("Version telemetry", func() {
+ var (
+ command *exec.Cmd
+ tempSettings *clitest.TempSettings
+ )
+
+ BeforeEach(func() {
+ tempSettings = clitest.WithTempSettings()
+ command = commandWithHome(pathCLI, tempSettings.Home, "version")
+ command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath))
+ })
+
+ AfterEach(func() {
+ tempSettings.Close()
+ })
+
+ It("should send a telemetry event", func() {
+ session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
+ Expect(err).ShouldNot(HaveOccurred())
+
+ Eventually(session).Should(gexec.Exit(0))
+ clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{
+ telemetry.CreateVersionEvent("0.0.0-dev+dirty-local-tree (source)"),
+ })
+ })
+})
diff --git a/go.mod b/go.mod
index f305f8565..710386a9d 100644
--- a/go.mod
+++ b/go.mod
@@ -37,6 +37,8 @@ require (
github.com/charmbracelet/lipgloss v0.5.0
github.com/erikgeiser/promptkit v0.7.0
github.com/hexops/gotextdiff v1.0.3
+ github.com/segmentio/analytics-go v3.1.0+incompatible
+ github.com/spf13/afero v1.9.5
github.com/stretchr/testify v1.8.3
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
golang.org/x/term v0.10.0
@@ -50,6 +52,7 @@ require (
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/beorn7/perks v1.0.1 // indirect
+ github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/charmbracelet/bubbles v0.11.0 // indirect
github.com/charmbracelet/bubbletea v0.21.0 // indirect
@@ -94,6 +97,7 @@ require (
github.com/prometheus/procfs v0.11.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
+ github.com/segmentio/backo-go v1.0.1 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.1.1 // indirect
@@ -101,6 +105,7 @@ require (
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
+ github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
github.com/yashtewari/glob-intersection v0.2.0 // indirect
go.opentelemetry.io/otel v1.16.0 // indirect
go.opentelemetry.io/otel/metric v1.16.0 // indirect
diff --git a/go.sum b/go.sum
index 7ab560a5e..4d5d49640 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,45 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
+cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
+cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/AlecAivazis/survey/v2 v2.1.1 h1:LEMbHE0pLj75faaVEKClEX1TM4AJmmnOh9eimREzLWI=
github.com/AlecAivazis/survey/v2 v2.1.1/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/CircleCI-Public/circle-policy-agent v0.0.683 h1:EzZaLy9mUGl4dwDNWceBHeDb3X0KAAjV4eFOk3C7lts=
github.com/CircleCI-Public/circle-policy-agent v0.0.683/go.mod h1:72U4Q4OtvAGRGGo/GqlCCO0tARg1cSG9xwxWyz3ktQI=
github.com/CircleCI-Public/circleci-config v0.0.0-20230629192034-c469d9e9936b h1:emg7uU3bRjVMlwSpOATBiybaBPXNWUIiFE/qbQQXZtE=
@@ -32,11 +72,14 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY=
github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA=
github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -47,9 +90,16 @@ github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@@ -65,6 +115,12 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikgeiser/promptkit v0.7.0 h1:Yi28iN6JRs8/0x+wjQRPfWb+vWz1pFmZ5fu2uoFipD8=
github.com/erikgeiser/promptkit v0.7.0/go.mod h1:Jj9bhN+N8RbMjB1jthkr9A4ydmczZ1WZJ8xTXnP12dg=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@@ -87,6 +143,9 @@ github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw4
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
github.com/go-git/go-git/v5 v5.7.0 h1:t9AudWVLmqzlo+4bqdf7GY+46SUuRsx59SboFxkq2aE=
github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -98,26 +157,51 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg78
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.1.1 h1:jxpi2eWoU84wbX9iIEyAeeoac3FLuifZpY9tcNUD9kw=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -125,17 +209,39 @@ github.com/google/go-github v15.0.0+incompatible h1:jlPg2Cpsxb/FyEV/MFiIE9tW/2RA
github.com/google/go-github v15.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0=
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY=
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
@@ -144,11 +250,15 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -220,10 +330,12 @@ github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
@@ -237,10 +349,15 @@ github.com/rhysd/go-github-selfupdate v0.0.0-20180520142321-41c1bbb0804a/go.mod
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
+github.com/segmentio/analytics-go v3.1.0+incompatible h1:IyiOfUgQFVHvsykKKbdI7ZsH374uv3/DfZUo9+G0Z80=
+github.com/segmentio/analytics-go v3.1.0+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48=
+github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N++y4=
+github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@@ -248,6 +365,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.1.1 h1:MTk78x9FPgDFVFkDLTrsnnfCJl7g1C/nnKvePgrIngE=
github.com/skeema/knownhosts v1.1.1/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo=
+github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
+github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
@@ -273,10 +392,21 @@ github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMc
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
+github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
+github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg=
github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 h1:pginetY7+onl4qN1vl0xW/V/v6OBZ0vVdH+esuJgvmM=
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
@@ -292,26 +422,87 @@ go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZE
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -320,31 +511,73 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -368,46 +601,180 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
+google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
+google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20230724170836-66ad5b6ff146 h1:0yJBPCSj/Hy/vQsNSrYtRLuJSNKoDzDXMu1q1ePGdus=
google.golang.org/genproto v0.0.0-20230724170836-66ad5b6ff146/go.mod h1:0ggbjUrZYpy1q+ANUS30SEoGZ53cdfwtbuG7Ptgy108=
google.golang.org/genproto/googleapis/api v0.0.0-20230724170836-66ad5b6ff146 h1:P60zJj7Yxq1VhZIxpRO7A5lDFyy07D6Dqa+HCixuFBM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230724170836-66ad5b6ff146 h1:0PjALPu/U/4OVXKQM2P8b8NJGd4V+xbZSP+uuBJpGm0=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
+google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
@@ -423,3 +790,13 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/main.go b/main.go
index 8948a918d..9a733aa63 100644
--- a/main.go
+++ b/main.go
@@ -1,10 +1,14 @@
package main
import (
+ "os"
+
"github.com/CircleCI-Public/circleci-cli/cmd"
)
func main() {
// See cmd/root.go for Execute()
- cmd.Execute()
+ if err := cmd.Execute(); err != nil {
+ os.Exit(-1)
+ }
}
diff --git a/prompt/prompt.go b/prompt/prompt.go
index cb2bd52dd..c9613386c 100644
--- a/prompt/prompt.go
+++ b/prompt/prompt.go
@@ -36,3 +36,14 @@ func AskUserToConfirm(message string) bool {
result, err := input.RunPrompt()
return err == nil && result
}
+
+func AskUserToConfirmWithDefault(message string, defaultValue bool) bool {
+ def := confirmation.No
+ if defaultValue {
+ def = confirmation.Yes
+ }
+
+ input := confirmation.New(message, def)
+ result, err := input.RunPrompt()
+ return err == nil && result
+}
diff --git a/settings/settings.go b/settings/settings.go
index 36928fac5..7a9e7f0e4 100644
--- a/settings/settings.go
+++ b/settings/settings.go
@@ -17,25 +17,37 @@ import (
yaml "gopkg.in/yaml.v3"
"github.com/CircleCI-Public/circleci-cli/data"
+ "github.com/spf13/afero"
+)
+
+var (
+ FS afero.Afero = afero.Afero{
+ Fs: afero.NewOsFs(),
+ }
)
// Config is used to represent the current state of a CLI instance.
type Config struct {
- Host string `yaml:"host"`
- DlHost string `yaml:"-"`
- Endpoint string `yaml:"endpoint"`
- Token string `yaml:"token"`
- RestEndpoint string `yaml:"rest_endpoint"`
- TLSCert string `yaml:"tls_cert"`
- TLSInsecure bool `yaml:"tls_insecure"`
- HTTPClient *http.Client `yaml:"-"`
- Data *data.DataBag `yaml:"-"`
- Debug bool `yaml:"-"`
- Address string `yaml:"-"`
- FileUsed string `yaml:"-"`
- GitHubAPI string `yaml:"-"`
- SkipUpdateCheck bool `yaml:"-"`
- OrbPublishing OrbPublishingInfo `yaml:"orb_publishing"`
+ Host string `yaml:"host"`
+ DlHost string `yaml:"-"`
+ Endpoint string `yaml:"endpoint"`
+ Token string `yaml:"token"`
+ RestEndpoint string `yaml:"rest_endpoint"`
+ TLSCert string `yaml:"tls_cert"`
+ TLSInsecure bool `yaml:"tls_insecure"`
+ HTTPClient *http.Client `yaml:"-"`
+ Data *data.DataBag `yaml:"-"`
+ Debug bool `yaml:"-"`
+ Address string `yaml:"-"`
+ FileUsed string `yaml:"-"`
+ GitHubAPI string `yaml:"-"`
+ SkipUpdateCheck bool `yaml:"-"`
+ // Parameter used to disable telemetry from tests
+ IsTelemetryDisabled bool `yaml:"-"`
+ // If this value is defined, the telemetry will write all its events a file
+ // The value of this field is the path where the telemetry will be written
+ MockTelemetry string `yaml:"-"`
+ OrbPublishing OrbPublishingInfo `yaml:"orb_publishing"`
}
type OrbPublishingInfo struct {
@@ -50,6 +62,14 @@ type UpdateCheck struct {
FileUsed string `yaml:"-"`
}
+// TelemetrySettings is used to represent telemetry related settings
+type TelemetrySettings struct {
+ IsEnabled bool `yaml:"is_enabled"`
+ HasAnsweredPrompt bool `yaml:"has_answered_prompt"`
+ UniqueID string `yaml:"unique_id"`
+ UserID string `yaml:"user_id"`
+}
+
// Load will read the update check settings from the user's disk and then deserialize it into the current instance.
func (upd *UpdateCheck) Load() error {
path := filepath.Join(SettingsPath(), updateCheckFilename())
@@ -80,6 +100,35 @@ func (upd *UpdateCheck) WriteToDisk() error {
return err
}
+// Load will read the telemetry settings from the user's disk and then deserialize it into the current instance.
+func (tel *TelemetrySettings) Load() error {
+ path := filepath.Join(SettingsPath(), telemetryFilename())
+
+ if err := ensureSettingsFileExists(path); err != nil {
+ return err
+ }
+
+ content, err := FS.ReadFile(path) // #nosec
+ if err != nil {
+ return err
+ }
+
+ err = yaml.Unmarshal(content, &tel)
+ return err
+}
+
+// WriteToDisk will write the telemetry settings to disk by serializing the YAML
+func (tel *TelemetrySettings) Write() error {
+ enc, err := yaml.Marshal(&tel)
+ if err != nil {
+ return err
+ }
+
+ path := filepath.Join(SettingsPath(), telemetryFilename())
+ err = FS.WriteFile(path, enc, 0600)
+ return err
+}
+
// Load will read the config from the user's disk and then evaluate possible configuration from the environment.
func (cfg *Config) Load() error {
if err := cfg.LoadFromDisk(); err != nil {
@@ -161,6 +210,11 @@ func configFilename() string {
return "cli.yml"
}
+// telemetryFilename returns the name of the cli telemetry file
+func telemetryFilename() string {
+ return "telemetry.yml"
+}
+
// settingsPath returns the path of the CLI settings directory
func SettingsPath() string {
// TODO: Make this configurable
diff --git a/telemetry/context.go b/telemetry/context.go
new file mode 100644
index 000000000..4e245f101
--- /dev/null
+++ b/telemetry/context.go
@@ -0,0 +1,16 @@
+package telemetry
+
+import "context"
+
+type contextKey string
+
+const telemetryClientContextKey contextKey = "telemetryClientContextKey"
+
+func NewContext(ctx context.Context, client Client) context.Context {
+ return context.WithValue(ctx, telemetryClientContextKey, client)
+}
+
+func FromContext(ctx context.Context) (Client, bool) {
+ client, ok := ctx.Value(telemetryClientContextKey).(Client)
+ return client, ok
+}
diff --git a/telemetry/events.go b/telemetry/events.go
new file mode 100644
index 000000000..dfdc61330
--- /dev/null
+++ b/telemetry/events.go
@@ -0,0 +1,149 @@
+package telemetry
+
+import "fmt"
+
+// This file contains all the telemetry event constructors
+// All the events are referenced in the following file:
+// https://circleci.atlassian.net/wiki/spaces/DE/pages/6760694125/CLI+segment+event+tracking
+// If you want to add an event, first make sure it appears in this file
+
+type CommandInfo struct {
+ Name string
+ LocalArgs map[string]string
+}
+
+func createEventFromCommandInfo(name string, cmdInfo CommandInfo) Event {
+ properties := map[string]interface{}{}
+ for key, value := range cmdInfo.LocalArgs {
+ properties[fmt.Sprintf("cmd.flag.%s", key)] = value
+ }
+ properties["has_been_executed"] = false
+
+ return Event{
+ Object: fmt.Sprintf("cli-%s", name),
+ Action: cmdInfo.Name,
+ Properties: properties,
+ }
+}
+
+func errorToProperties(err error) map[string]interface{} {
+ properties := map[string]interface{}{
+ "has_been_executed": true,
+ }
+
+ if err != nil {
+ properties["error"] = err.Error()
+ }
+ return properties
+}
+
+func CreateSetupEvent(isServerCustomer bool) Event {
+ return Event{
+ Object: "cli-setup",
+ Action: "setup",
+ Properties: map[string]interface{}{
+ "is_server_customer": isServerCustomer,
+ "has_been_executed": true,
+ },
+ }
+}
+
+func CreateVersionEvent(version string) Event {
+ return Event{
+ Object: "cli-version",
+ Action: "version",
+ Properties: map[string]interface{}{
+ "version": version,
+ "has_been_executed": true,
+ },
+ }
+}
+
+func CreateUpdateEvent(cmdInfo CommandInfo) Event {
+ return createEventFromCommandInfo("update", cmdInfo)
+}
+
+func CreateDiagnosticEvent(err error) Event {
+ return Event{
+ Object: "cli-diagnostic", Action: "diagnostic", Properties: errorToProperties(err),
+ }
+}
+
+func CreateFollowEvent(err error) Event {
+ return Event{
+ Object: "cli-follow", Action: "follow", Properties: errorToProperties(err),
+ }
+}
+
+func CreateOpenEvent(err error) Event {
+ return Event{Object: "cli-open", Action: "open", Properties: errorToProperties(err)}
+}
+
+func CreateCompletionCommand(cmdInfo CommandInfo) Event {
+ return createEventFromCommandInfo("completion", cmdInfo)
+}
+
+func CreateConfigEvent(cmdInfo CommandInfo, err error) Event {
+ event := createEventFromCommandInfo("config", cmdInfo)
+ if err != nil {
+ event.Properties["error"] = err.Error()
+ event.Properties["has_been_executed"] = true
+ }
+ return event
+}
+
+func CreateLocalExecuteEvent(cmdInfo CommandInfo) Event {
+ return createEventFromCommandInfo("local", cmdInfo)
+}
+
+func CreateNamespaceEvent(cmdInfo CommandInfo) Event {
+ return createEventFromCommandInfo("namespace", cmdInfo)
+}
+
+func CreateOrbEvent(cmdInfo CommandInfo) Event {
+ return createEventFromCommandInfo("orb", cmdInfo)
+}
+
+func CreatePolicyEvent(cmdInfo CommandInfo) Event {
+ return createEventFromCommandInfo("policy", cmdInfo)
+}
+
+func CreateRunnerInstanceEvent(cmdInfo CommandInfo, err error) Event {
+ event := createEventFromCommandInfo("runner-instance", cmdInfo)
+ if err != nil {
+ event.Properties["error"] = err.Error()
+ event.Properties["has_been_executed"] = true
+ }
+ return event
+}
+
+func CreateRunnerResourceClassEvent(cmdInfo CommandInfo) Event {
+ return createEventFromCommandInfo("runner-resource-class", cmdInfo)
+}
+
+func CreateRunnerToken(cmdInfo CommandInfo) Event {
+ return createEventFromCommandInfo("runner-resource-class", cmdInfo)
+}
+
+func CreateInfoEvent(cmdInfo CommandInfo, err error) Event {
+ event := createEventFromCommandInfo("info", cmdInfo)
+ if err != nil {
+ event.Properties["error"] = err.Error()
+ event.Properties["has_been_executed"] = true
+ }
+ return event
+}
+
+func CreateChangeTelemetryStatusEvent(action string, origin string, err error) Event {
+ event := Event{
+ Object: "cli-telemetry",
+ Action: action,
+ Properties: map[string]interface{}{
+ "origin": origin,
+ },
+ }
+ if err != nil {
+ event.Properties["error"] = err.Error()
+ }
+ return event
+}
diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go
new file mode 100644
index 000000000..928e59dd8
--- /dev/null
+++ b/telemetry/telemetry.go
@@ -0,0 +1,197 @@
+package telemetry
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/segmentio/analytics-go"
+)
+
+var (
+ // Overwrite this function for tests
+ CreateActiveTelemetry = newSegmentClient
+
+ SegmentEndpoint = "http://localhost:33457"
+ segmentKey = "AbgkrgN4cbRhAVEwlzMkHbwvrXnxHh35"
+)
+
+type Approval string
+
+const (
+ Enabled Approval = "enabled"
+ Disabled Approval = "disabled"
+ NoStdin Approval = "disabled_default"
+)
+
+type Client interface {
+ io.Closer
+ // Send a telemetry event. This method is not to be called directly. Use config.Track instead
+ Track(event Event) error
+}
+
+// A segment event to be sent to the telemetry
+// Important: this is not meant to be constructed directly apart in tests
+// If you want to create a new event, add its constructor in ./events.go
+type Event struct {
+ Object string `json:"object"`
+ Action string `json:"action"`
+ Properties map[string]interface{} `json:"properties"`
+}
+
+type User struct {
+ UniqueID string
+ UserID string
+ IsSelfHosted bool
+ OS string
+ Version string
+ TeamName string
+}
+
+// Create a telemetry client to be used to send telemetry events
+func CreateClient(user User, enabled bool) Client {
+ if !enabled {
+ return nullClient{}
+ }
+
+ return CreateActiveTelemetry(user)
+}
+
+// Sends the user's approval event
+func SendTelemetryApproval(user User, approval Approval) error {
+ client := CreateActiveTelemetry(user)
+ defer client.Close()
+
+ return client.Track(CreateChangeTelemetryStatusEvent(string(approval), "first-time-prompt", nil))
+}
+
+// Null client
+// Used when telemetry is disabled
+
+func CreateNullClient() Client {
+ return nullClient{}
+}
+
+type nullClient struct{}
+
+func (cli nullClient) Close() error { return nil }
+
+func (cli nullClient) Track(_ Event) error { return nil }
+
+// Segment client
+// Used when telemetry is enabled
+
+// Nil segment logger
+type nilSegmentEmptyLogger struct{}
+
+func (nilSegmentEmptyLogger) Logf(format string, args ...interface{}) {}
+func (nilSegmentEmptyLogger) Errorf(format string, args ...interface{}) {}
+
+type segmentClient struct {
+ analyticsClient analytics.Client
+ user User
+}
+
+func newSegmentClient(user User) Client {
+ cli, err := analytics.NewWithConfig(segmentKey, analytics.Config{
+ Endpoint: SegmentEndpoint,
+ Logger: nilSegmentEmptyLogger{},
+ })
+
+ if err != nil {
+ return CreateNullClient()
+ }
+
+ if len(user.UniqueID) == 0 {
+ return CreateNullClient()
+ }
+
+ err = cli.Enqueue(
+ analytics.Identify{
+ UserId: user.UniqueID,
+ Traits: analytics.NewTraits().Set("os", user.OS),
+ },
+ )
+ if err != nil {
+ fmt.Printf("Error while identifying with telemetry: %s\n", err)
+ }
+
+ return &segmentClient{cli, user}
+}
+
+func (segment *segmentClient) Track(event Event) error {
+ if event.Properties == nil {
+ event.Properties = make(map[string]interface{})
+ }
+ if event.Action != "" {
+ event.Properties["action"] = event.Action
+ }
+
+ if segment.user.UniqueID != "" {
+ event.Properties["anonymous_id"] = segment.user.UniqueID
+ }
+
+ if segment.user.UserID != "" {
+ event.Properties["cci_user_id"] = segment.user.UserID
+ }
+
+ event.Properties["is_self_hosted"] = segment.user.IsSelfHosted
+
+ if segment.user.OS != "" {
+ event.Properties["os"] = segment.user.OS
+ }
+
+ if segment.user.Version != "" {
+ event.Properties["cli_version"] = segment.user.Version
+ }
+
+ if segment.user.TeamName != "" {
+ event.Properties["team_name"] = segment.user.TeamName
+ }
+
+ return segment.analyticsClient.Enqueue(analytics.Track{
+ UserId: segment.user.UniqueID,
+ Event: event.Object,
+ Properties: event.Properties,
+ })
+}
+
+func (segment *segmentClient) Close() error {
+ return segment.analyticsClient.Close()
+}
+
+// File telemetry
+// Used for E2E tests
+
+type fileTelemetry struct {
+ filePath string
+ events []Event
+}
+
+func CreateFileTelemetry(filePath string) Client {
+ return &fileTelemetry{filePath, make([]Event, 0)}
+}
+
+func (cli *fileTelemetry) Track(event Event) error {
+ cli.events = append(cli.events, event)
+ return nil
+}
+
+func (cli *fileTelemetry) Close() error {
+ file, err := os.OpenFile(cli.filePath, os.O_CREATE|os.O_WRONLY, 0666)
+ if err != nil {
+ return err
+ }
+
+ content, err := json.Marshal(&cli.events)
+ if err != nil {
+ return err
+ }
+
+ if _, err = file.Write(content); err != nil {
+ return err
+ }
+
+ return file.Close()
+}
diff --git a/telemetry/utils.go b/telemetry/utils.go
new file mode 100644
index 000000000..2201887ad
--- /dev/null
+++ b/telemetry/utils.go
@@ -0,0 +1,31 @@
+package telemetry
+
+import (
+ "github.com/spf13/cobra"
+ "github.com/spf13/pflag"
+)
+
+// Utility function used when creating telemetry events.
+// It takes a cobra Command and creates a telemetry.CommandInfo of it
+// If getParent is true, puts both the command's args in `LocalArgs` and the parent's args
+// Else only put the command's args
+// Note: child flags overwrite parent flags with same name
+func GetCommandInformation(cmd *cobra.Command, getParent bool) CommandInfo {
+ localArgs := map[string]string{}
+
+ parent := cmd.Parent()
+ if getParent && parent != nil {
+ parent.LocalFlags().VisitAll(func(flag *pflag.Flag) {
+ localArgs[flag.Name] = flag.Value.String()
+ })
+ }
+
+ cmd.LocalFlags().VisitAll(func(flag *pflag.Flag) {
+ localArgs[flag.Name] = flag.Value.String()
+ })
+
+ return CommandInfo{
+ Name: cmd.Name(),
+ LocalArgs: localArgs,
+ }
+}