Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 37 additions & 8 deletions versioncheck/versioncheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ const (
DisableEnv = "POUTINE_DISABLE_VERSION_CHECK"
// URLEnv overrides the compiled-in endpoint, primarily for staging.
URLEnv = "POUTINE_VERSION_CHECK_URL"
// CIEnv signals an ephemeral CI run. When truthy, the check skips
// reading and writing the user-level state file and tags the report
// with ci=true.
CIEnv = "CI"
)

const (
Expand All @@ -44,6 +48,7 @@ type options struct {
Version string
URL string
Disabled bool
CI bool
Client *http.Client
Now func() time.Time
SaveConfig func(*Config) error
Expand All @@ -60,12 +65,23 @@ func Run(ctx context.Context, version string, disabled bool) *Result {
return nil
}

cfg, _ := LoadConfig()
if cfg == nil {
cfg = &Config{}
ci := isCIEnv(os.Getenv(CIEnv))
var cfg *Config
saveConfig := SaveConfig
if ci {
cfg = &Config{
InstanceID: uuid.NewString(),
StartCount: 1,
}
saveConfig = func(*Config) error { return nil }
} else {
cfg, _ = LoadConfig()
if cfg == nil {
cfg = &Config{}
}
recordStart(cfg, uuid.NewString)
_ = SaveConfig(cfg)
}
recordStart(cfg, uuid.NewString)
_ = SaveConfig(cfg)

timeoutCtx, cancel := context.WithTimeout(ctx, checkTimeout)
defer cancel()
Expand All @@ -74,9 +90,10 @@ func Run(ctx context.Context, version string, disabled bool) *Result {
Config: cfg,
Version: version,
URL: VersionCheckURL,
CI: ci,
Client: &http.Client{Timeout: checkTimeout},
Now: time.Now,
SaveConfig: SaveConfig,
SaveConfig: saveConfig,
Env: os.Getenv,
NewID: uuid.NewString,
})
Expand Down Expand Up @@ -121,7 +138,7 @@ func run(ctx context.Context, opts options) (*Result, error) {
}
instanceID := ensureInstanceID(opts.Config, opts.NewID)
startsSinceLastCheck := startsSinceLastReport(opts.Config)
requestURL := buildRequestURL(u, version, instanceID, opts.Config.StartCount, startsSinceLastCheck)
requestURL := buildRequestURL(u, version, instanceID, opts.Config.StartCount, startsSinceLastCheck, opts.CI)

client := opts.Client
if client == nil {
Expand Down Expand Up @@ -173,6 +190,15 @@ func isDisabledByEnv(value string) bool {
}
}

func isCIEnv(value string) bool {
switch strings.ToLower(strings.TrimSpace(value)) {
case "1", "true", "yes", "on":
return true
default:
return false
}
}

func isDevVersion(version string) bool {
version = strings.TrimSpace(version)
return version == "" ||
Expand Down Expand Up @@ -230,7 +256,7 @@ func readResult(resp *http.Response) (*Result, error) {
return &result, nil
}

func buildRequestURL(u *url.URL, version, instanceID string, startCount, startsSinceLastCheck int) string {
func buildRequestURL(u *url.URL, version, instanceID string, startCount, startsSinceLastCheck int, ci bool) string {
q := u.Query()
q.Set("project", "poutine")
q.Set("component", "cli")
Expand All @@ -240,6 +266,9 @@ func buildRequestURL(u *url.URL, version, instanceID string, startCount, startsS
q.Set("start_count", strconv.Itoa(startCount))
q.Set("starts_since_last_check", strconv.Itoa(startsSinceLastCheck))
}
if ci {
q.Set("ci", "true")
}
u.RawQuery = q.Encode()
return u.String()
}
81 changes: 81 additions & 0 deletions versioncheck/versioncheck_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,87 @@ func TestLoadConfig_RoundTrip(t *testing.T) {
assert.True(t, cfg.LastVersionCheckAt.Equal(loaded.LastVersionCheckAt))
}

func TestRun_CIFlagSetInRequest(t *testing.T) {
now := time.Date(2026, 5, 4, 12, 0, 0, 0, time.UTC)
cfg := &Config{
InstanceID: "2ed05245-10d7-4d21-a8e8-7c4e8a9851b4",
StartCount: 1,
}
var gotReq *http.Request

result, err := run(context.Background(), options{
Config: cfg,
Version: "v0.18.0",
URL: "https://updates.example/check",
CI: true,
Client: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
gotReq = req
return &http.Response{
StatusCode: http.StatusNoContent,
Body: io.NopCloser(strings.NewReader("")),
}, nil
})},
Now: func() time.Time { return now },
SaveConfig: func(*Config) error { return nil },
Env: func(string) string { return "" },
})

require.NoError(t, err)
assert.Nil(t, result)
require.NotNil(t, gotReq)
assert.Equal(t, "true", gotReq.URL.Query().Get("ci"))
}

func TestRun_NonCIOmitsCIParam(t *testing.T) {
cfg := &Config{InstanceID: "2ed05245-10d7-4d21-a8e8-7c4e8a9851b4", StartCount: 1}
var gotReq *http.Request

_, err := run(context.Background(), options{
Config: cfg,
Version: "v0.18.0",
URL: "https://updates.example/check",
Client: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
gotReq = req
return &http.Response{
StatusCode: http.StatusNoContent,
Body: io.NopCloser(strings.NewReader("")),
}, nil
})},
SaveConfig: func(*Config) error { return nil },
Env: func(string) string { return "" },
})

require.NoError(t, err)
require.NotNil(t, gotReq)
_, hasCI := gotReq.URL.Query()["ci"]
assert.False(t, hasCI, "ci param should be absent when not running in CI")
}

func TestRun_CISkipsPersistentState(t *testing.T) {
dir := t.TempDir()
t.Setenv("POUTINE_CONFIG_DIR", dir)
t.Setenv(DisableEnv, "")
t.Setenv(CIEnv, "true")
t.Setenv(URLEnv, "https://override.example/check")

// Stub the HTTP transport via the default client used by Run. Because Run
// constructs its own client, we can't intercept the request here; what we
// care about is that no state file is written on disk after the call.
_ = Run(context.Background(), "v0.18.0", false)

_, err := os.Stat(filepath.Join(dir, "config.yaml"))
assert.True(t, os.IsNotExist(err), "no state file should be written in CI mode")
}

func TestIsCIEnv(t *testing.T) {
for _, value := range []string{"1", "true", "TRUE", "yes", "on"} {
assert.True(t, isCIEnv(value), value)
}
for _, value := range []string{"", "0", "false", "no", "off", "anything"} {
assert.False(t, isCIEnv(value), value)
}
}

func TestRun_DisabledShortCircuitsBeforeAnyDiskWrite(t *testing.T) {
dir := t.TempDir()
t.Setenv("POUTINE_CONFIG_DIR", dir)
Expand Down
Loading