Include CI context in telemetry

This commit is contained in:
William Martin 2026-04-17 16:56:28 +02:00
parent 61a7865380
commit 78f1ad537c
5 changed files with 86 additions and 15 deletions

19
internal/ci/ci.go Normal file
View file

@ -0,0 +1,19 @@
// Package ci provides helpers for detecting CI/CD execution environments.
package ci
import "os"
// IsCI determines if the current execution context is within a known CI/CD system.
// This is based on https://github.com/watson/ci-info/blob/HEAD/index.js.
func IsCI() bool {
return os.Getenv("CI") != "" || // GitHub Actions, Travis CI, CircleCI, Cirrus CI, GitLab CI, AppVeyor, CodeShip, dsari
os.Getenv("BUILD_NUMBER") != "" || // Jenkins, TeamCity
os.Getenv("RUN_ID") != "" // TaskCluster, dsari
}
// IsGitHubActions determines if the current execution context is within GitHub Actions.
// GitHub Actions sets the GITHUB_ACTIONS environment variable to "true" for all steps.
// See https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables.
func IsGitHubActions() bool {
return os.Getenv("GITHUB_ACTIONS") == "true"
}

56
internal/ci/ci_test.go Normal file
View file

@ -0,0 +1,56 @@
package ci
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsCI(t *testing.T) {
tests := []struct {
name string
env map[string]string
want bool
}{
{name: "no CI env vars", env: map[string]string{}, want: false},
{name: "CI set", env: map[string]string{"CI": "true"}, want: true},
{name: "BUILD_NUMBER set", env: map[string]string{"BUILD_NUMBER": "42"}, want: true},
{name: "RUN_ID set", env: map[string]string{"RUN_ID": "abc"}, want: true},
{name: "CI empty string", env: map[string]string{"CI": ""}, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("CI", "")
t.Setenv("BUILD_NUMBER", "")
t.Setenv("RUN_ID", "")
for k, v := range tt.env {
t.Setenv(k, v)
}
assert.Equal(t, tt.want, IsCI())
})
}
}
func TestIsGitHubActions(t *testing.T) {
tests := []struct {
name string
value string
set bool
want bool
}{
{name: "unset", set: false, want: false},
{name: "true", value: "true", set: true, want: true},
{name: "false", value: "false", set: true, want: false},
{name: "empty", value: "", set: true, want: false},
{name: "other value", value: "yes", set: true, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("GITHUB_ACTIONS", "")
if tt.set {
t.Setenv("GITHUB_ACTIONS", tt.value)
}
assert.Equal(t, tt.want, IsGitHubActions())
})
}
}

View file

@ -19,6 +19,7 @@ import (
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/agents"
"github.com/cli/cli/v2/internal/build"
"github.com/cli/cli/v2/internal/ci"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/config/migration"
"github.com/cli/cli/v2/internal/gh"
@ -69,9 +70,11 @@ func Main() exitCode {
ghExecutablePath := executablePath("gh")
additionalCommonDimensions := ghtelemetry.Dimensions{
"version": strings.TrimPrefix(buildVersion, "v"),
"is_tty": strconv.FormatBool(ioStreams.IsStdoutTTY()),
"agent": string(agents.Detect()),
"version": strings.TrimPrefix(buildVersion, "v"),
"is_tty": strconv.FormatBool(ioStreams.IsStdoutTTY()),
"agent": string(agents.Detect()),
"ci": strconv.FormatBool(ci.IsCI()),
"github_actions": strconv.FormatBool(ci.IsGitHubActions()),
}
var telemetryService ghtelemetry.Service

View file

@ -13,6 +13,7 @@ import (
"strings"
"time"
"github.com/cli/cli/v2/internal/ci"
"github.com/cli/cli/v2/pkg/extensions"
"github.com/hashicorp/go-version"
"github.com/mattn/go-isatty"
@ -42,7 +43,7 @@ func ShouldCheckForExtensionUpdate() bool {
if os.Getenv("CODESPACES") != "" {
return false
}
return !IsCI() && IsTerminal(os.Stdout) && IsTerminal(os.Stderr)
return !ci.IsCI() && IsTerminal(os.Stdout) && IsTerminal(os.Stderr)
}
// CheckForExtensionUpdate checks whether an update exists for a specific extension based on extension type and recency of last check within past 24 hours.
@ -83,7 +84,7 @@ func ShouldCheckForUpdate() bool {
if os.Getenv("CODESPACES") != "" {
return false
}
return !IsCI() && IsTerminal(os.Stdout) && IsTerminal(os.Stderr)
return !ci.IsCI() && IsTerminal(os.Stdout) && IsTerminal(os.Stderr)
}
// CheckForUpdate checks whether an update exists for the GitHub CLI based on recency of last check within past 24 hours.
@ -182,11 +183,3 @@ func versionGreaterThan(v, w string) bool {
func IsTerminal(f *os.File) bool {
return isatty.IsTerminal(f.Fd()) || isatty.IsCygwinTerminal(f.Fd())
}
// IsCI determines if the current execution context is within a known CI/CD system.
// This is based on https://github.com/watson/ci-info/blob/HEAD/index.js.
func IsCI() bool {
return os.Getenv("CI") != "" || // GitHub Actions, Travis CI, CircleCI, Cirrus CI, GitLab CI, AppVeyor, CodeShip, dsari
os.Getenv("BUILD_NUMBER") != "" || // Jenkins, TeamCity
os.Getenv("RUN_ID") != "" // TaskCluster, dsari
}

View file

@ -18,10 +18,10 @@ import (
"strings"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/ci"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/internal/safepaths"
"github.com/cli/cli/v2/internal/update"
ghzip "github.com/cli/cli/v2/internal/zip"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -150,7 +150,7 @@ func runCopilot(opts *CopilotOptions) error {
fmt.Fprintf(opts.IO.ErrOut, "%s Copilot CLI was not installed", opts.IO.ColorScheme().WarningIcon())
return cmdutil.SilentError
}
} else if !update.IsCI() {
} else if !ci.IsCI() {
fmt.Fprintf(opts.IO.ErrOut, "%s Copilot CLI not installed", opts.IO.ColorScheme().WarningIcon())
return cmdutil.SilentError
}