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 6f0be8430..c8fd82bc1 100644
--- a/README.md
+++ b/README.md
@@ -183,4 +183,8 @@ Please see the [documentation](https://circleci-public.github.io/circleci-cli) o
| 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
|
\ No newline at end of file
+| 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
+
+There is some telemetry in the CLI. Be assured that we tried to keep it as little as possible. The first time you will run a command with the CLI you will be asked for your approval for the telemetry events. If your STDIN is not a TTY the telemetry will be automatically disabled, making it easier to use the CLI in scripts. Would you decide to change your mind about the telemetry, you can run the commands `circleci telemetry enable` and `circleci telemetry disable`. Setting the env variable `CIRCLECI_CLI_TELEMETRY_OPTOUT` will also disable the telemetry.
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 0c51b486d..a98be1a94 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"
@@ -1945,3 +1947,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 6c85de763..56a321dc5 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),
@@ -60,13 +66,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),
@@ -104,6 +117,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..7559d1eb9
--- /dev/null
+++ b/cmd/create_telemetry.go
@@ -0,0 +1,204 @@
+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()))
+ 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 available, send telemetry event, disable telemetry and return
+ if !isStdinATTY {
+ 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..19c65ac70
--- /dev/null
+++ b/cmd/create_telemetry_test.go
@@ -0,0 +1,237 @@
+package cmd
+
+import (
+ "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"
+)
+
+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
+}
+
+func TestLoadTelemetrySettings(t *testing.T) {
+ // 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
+ 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,
+ telemetryEvents: []telemetry.Event{},
+ },
+ },
+ {
+ name: "Does not change telemetry settings if stdin is not open",
+ 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,
+ }},
+ },
+ },
+ },
+ }
+
+ 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 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 })()
+
+ // 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
+ assert.DeepEqual(t, telemetryClient.events, tt.want.telemetryEvents)
+
+ // 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)
+ })
+ }
+}
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 a7037c7d8..d7fdd5394 100644
--- a/cmd/orb.go
+++ b/cmd/orb.go
@@ -25,6 +25,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"
@@ -388,6 +389,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 7b4e917ba..a33ef59ff 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 693bfb0c3..bd4a2260b 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 39b414649..ac7fab59c 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()
@@ -1382,10 +1382,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..8e920e5f5 100644
--- a/settings/settings.go
+++ b/settings/settings.go
@@ -17,25 +17,36 @@ 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:"-"`
+ 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 +61,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 +99,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 +209,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,
+ }
+}