Skip to content

Commit

Permalink
Merge pull request #999 from CircleCI-Public/develop
Browse files Browse the repository at this point in the history
Release
  • Loading branch information
JulesFaucherre authored Sep 5, 2023
2 parents b8e132d + 4be7e3a commit 663eda4
Show file tree
Hide file tree
Showing 52 changed files with 2,351 additions and 94 deletions.
6 changes: 6 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ jobs:
working_directory: integration_tests
environment:
TESTING: "true"
- run:
name: "Make sure simple command do not cause any timeout"
command: circleci version

test:
executor: go
steps:
Expand Down Expand Up @@ -378,12 +382,14 @@ workflows:
- devex-release
- deploy:
requires:
- cucumber
- test
- test_mac
- coverage
- lint
- deploy-test
- shellcheck/check
- vulnerability-scan
filters:
branches:
only: main
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,20 @@ Please see the [documentation](https://circleci-public.github.io/circleci-cli) o

## Server compatibility

There are some difference of behavior depending on the version you use:
- config validation will use the GraphQL API until **Server v4.0.5, v4.1.3, v4.2.0**. The above versions will use the new route `compile-config-with-defaults`
- `circleci orb validate` will only allow you to validate orbs using other private orbs with the option `--org-slug` from version **Server v4.2.0**
| Functionality | Impacted commands | Change description | Compatibility with Server |
| --- | --- | --- | --- |
| Config compilation and validation | <ul><li>`circleci config validate`</li><li>`circleci config process`</li><li>`circleci local execute`</li> | The config validation has been moved from the GraphQL API to a specific API endpoint | <ul><li>**Server v4.0.5, v4.1.3, v4.2.0 and above**: Commands use the new specific endpoint</li><li>**Previous version**: Commands use the GraphQL API</li></ul> |
| Orb compilation and validation of orb using private orbs | <ul><li>`circleci orb process`</li><li>`circleci orb validate`</li></ul> | 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 | <ul><li>**Server v4.2.0 and above**: The field is accessible so you can use the parameter</li><li>**Previous versions**: The field does not exist making the functionality unavailable</li></ul> |

## Telemetry

The CircleCI CLI includes a telemetry feature that collects basic errors and feature usage data in order to help us improve the experience for everyone.

Telemetry works on an opt-in basis: when running a command for the first time, you will be asked for consent to enable telemetry. For non-TTY STDIN, telemetry is disabled by default, ensuring that scripts that use the CLI run smoothly.

You can disable or enable telemetry anytime in one of the following ways:

* Run the commands `circleci telemetry enable` or `circleci telemetry disable`

* Set the `CIRCLECI_CLI_TELEMETRY_OPTOUT` environment variable to `1` or `true` to disable it

10 changes: 6 additions & 4 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ./...
- TESTING=true go test -race -coverprofile=coverage.txt ./...
19 changes: 19 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -1818,3 +1820,20 @@ func FollowProject(config settings.Config, vcs string, owner string, projectName

return fr, nil
}

type Me struct {
ID string `json:"id"`
Login string `json:"login"`
Name string `json:"name"`
}

func GetMe(client *rest.Client) (Me, error) {
req, err := client.NewRequest("GET", &url.URL{Path: "me"}, nil)
if err != nil {
return Me{}, errors.Wrap(err, "Unable to get user info")
}

var me Me
_, err = client.DoRequest(req, &me)
return me, err
}
7 changes: 4 additions & 3 deletions api/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import (
// An EnvironmentVariable has a Variable, a ContextID (its owner), and a
// CreatedAt date.
type EnvironmentVariable struct {
Variable string
ContextID string
CreatedAt time.Time
Variable string `json:"variable"`
ContextID string `json:"context_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// A Context is the owner of EnvironmentVariables.
Expand Down
22 changes: 18 additions & 4 deletions clitest/clitest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down
19 changes: 19 additions & 0 deletions clitest/telemetry.go
Original file line number Diff line number Diff line change
@@ -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))
}
10 changes: 9 additions & 1 deletion cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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),
}
Expand Down
3 changes: 3 additions & 0 deletions cmd/cmd_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"os/exec"
"testing"
"time"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
Expand All @@ -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())
Expand Down
10 changes: 9 additions & 1 deletion cmd/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
31 changes: 24 additions & 7 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -37,8 +37,14 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command {
packCommand := &cobra.Command{
Use: "pack <path>",
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),
Expand All @@ -64,13 +70,20 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command {
if len(args) == 1 {
path = args[0]
}
return compiler.ValidateConfig(config.ValidateConfigOpts{

err = compiler.ValidateConfig(config.ValidateConfigOpts{
ConfigPath: path,
OrgID: orgID,
OrgSlug: orgSlug,
IgnoreDeprecatedImages: ignoreDeprecatedImages,
VerboseOutput: verboseOutput,
})
telemetryClient, ok := telemetry.FromContext(cmd.Context())
if ok {
_ = telemetryClient.Track(telemetry.CreateConfigEvent(telemetry.GetCommandInformation(cmd, true), err))
}

return err
},
Args: cobra.MaximumNArgs(1),
Annotations: make(map[string]string),
Expand Down Expand Up @@ -112,6 +125,10 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command {
PipelineParamsFilePath: pipelineParamsFilePath,
VerboseOutput: verboseOutput,
})
telemetryClient, ok := telemetry.FromContext(cmd.Context())
if ok {
_ = telemetryClient.Track(telemetry.CreateConfigEvent(telemetry.GetCommandInformation(cmd, true), err))
}
if err != nil {
return err
}
Expand Down
32 changes: 31 additions & 1 deletion cmd/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"))
})

Expand Down
Loading

0 comments on commit 663eda4

Please sign in to comment.