Skip to content

Commit 8918c66

Browse files
authored
skip saving version when running in ci and added ci in report url (#439)
1 parent e50c0a4 commit 8918c66

2 files changed

Lines changed: 118 additions & 8 deletions

File tree

versioncheck/versioncheck.go

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ const (
2020
DisableEnv = "POUTINE_DISABLE_VERSION_CHECK"
2121
// URLEnv overrides the compiled-in endpoint, primarily for staging.
2222
URLEnv = "POUTINE_VERSION_CHECK_URL"
23+
// CIEnv signals an ephemeral CI run. When truthy, the check skips
24+
// reading and writing the user-level state file and tags the report
25+
// with ci=true.
26+
CIEnv = "CI"
2327
)
2428

2529
const (
@@ -44,6 +48,7 @@ type options struct {
4448
Version string
4549
URL string
4650
Disabled bool
51+
CI bool
4752
Client *http.Client
4853
Now func() time.Time
4954
SaveConfig func(*Config) error
@@ -60,12 +65,23 @@ func Run(ctx context.Context, version string, disabled bool) *Result {
6065
return nil
6166
}
6267

63-
cfg, _ := LoadConfig()
64-
if cfg == nil {
65-
cfg = &Config{}
68+
ci := isCIEnv(os.Getenv(CIEnv))
69+
var cfg *Config
70+
saveConfig := SaveConfig
71+
if ci {
72+
cfg = &Config{
73+
InstanceID: uuid.NewString(),
74+
StartCount: 1,
75+
}
76+
saveConfig = func(*Config) error { return nil }
77+
} else {
78+
cfg, _ = LoadConfig()
79+
if cfg == nil {
80+
cfg = &Config{}
81+
}
82+
recordStart(cfg, uuid.NewString)
83+
_ = SaveConfig(cfg)
6684
}
67-
recordStart(cfg, uuid.NewString)
68-
_ = SaveConfig(cfg)
6985

7086
timeoutCtx, cancel := context.WithTimeout(ctx, checkTimeout)
7187
defer cancel()
@@ -74,9 +90,10 @@ func Run(ctx context.Context, version string, disabled bool) *Result {
7490
Config: cfg,
7591
Version: version,
7692
URL: VersionCheckURL,
93+
CI: ci,
7794
Client: &http.Client{Timeout: checkTimeout},
7895
Now: time.Now,
79-
SaveConfig: SaveConfig,
96+
SaveConfig: saveConfig,
8097
Env: os.Getenv,
8198
NewID: uuid.NewString,
8299
})
@@ -121,7 +138,7 @@ func run(ctx context.Context, opts options) (*Result, error) {
121138
}
122139
instanceID := ensureInstanceID(opts.Config, opts.NewID)
123140
startsSinceLastCheck := startsSinceLastReport(opts.Config)
124-
requestURL := buildRequestURL(u, version, instanceID, opts.Config.StartCount, startsSinceLastCheck)
141+
requestURL := buildRequestURL(u, version, instanceID, opts.Config.StartCount, startsSinceLastCheck, opts.CI)
125142

126143
client := opts.Client
127144
if client == nil {
@@ -173,6 +190,15 @@ func isDisabledByEnv(value string) bool {
173190
}
174191
}
175192

193+
func isCIEnv(value string) bool {
194+
switch strings.ToLower(strings.TrimSpace(value)) {
195+
case "1", "true", "yes", "on":
196+
return true
197+
default:
198+
return false
199+
}
200+
}
201+
176202
func isDevVersion(version string) bool {
177203
version = strings.TrimSpace(version)
178204
return version == "" ||
@@ -230,7 +256,7 @@ func readResult(resp *http.Response) (*Result, error) {
230256
return &result, nil
231257
}
232258

233-
func buildRequestURL(u *url.URL, version, instanceID string, startCount, startsSinceLastCheck int) string {
259+
func buildRequestURL(u *url.URL, version, instanceID string, startCount, startsSinceLastCheck int, ci bool) string {
234260
q := u.Query()
235261
q.Set("project", "poutine")
236262
q.Set("component", "cli")
@@ -240,6 +266,9 @@ func buildRequestURL(u *url.URL, version, instanceID string, startCount, startsS
240266
q.Set("start_count", strconv.Itoa(startCount))
241267
q.Set("starts_since_last_check", strconv.Itoa(startsSinceLastCheck))
242268
}
269+
if ci {
270+
q.Set("ci", "true")
271+
}
243272
u.RawQuery = q.Encode()
244273
return u.String()
245274
}

versioncheck/versioncheck_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,87 @@ func TestLoadConfig_RoundTrip(t *testing.T) {
308308
assert.True(t, cfg.LastVersionCheckAt.Equal(loaded.LastVersionCheckAt))
309309
}
310310

311+
func TestRun_CIFlagSetInRequest(t *testing.T) {
312+
now := time.Date(2026, 5, 4, 12, 0, 0, 0, time.UTC)
313+
cfg := &Config{
314+
InstanceID: "2ed05245-10d7-4d21-a8e8-7c4e8a9851b4",
315+
StartCount: 1,
316+
}
317+
var gotReq *http.Request
318+
319+
result, err := run(context.Background(), options{
320+
Config: cfg,
321+
Version: "v0.18.0",
322+
URL: "https://updates.example/check",
323+
CI: true,
324+
Client: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
325+
gotReq = req
326+
return &http.Response{
327+
StatusCode: http.StatusNoContent,
328+
Body: io.NopCloser(strings.NewReader("")),
329+
}, nil
330+
})},
331+
Now: func() time.Time { return now },
332+
SaveConfig: func(*Config) error { return nil },
333+
Env: func(string) string { return "" },
334+
})
335+
336+
require.NoError(t, err)
337+
assert.Nil(t, result)
338+
require.NotNil(t, gotReq)
339+
assert.Equal(t, "true", gotReq.URL.Query().Get("ci"))
340+
}
341+
342+
func TestRun_NonCIOmitsCIParam(t *testing.T) {
343+
cfg := &Config{InstanceID: "2ed05245-10d7-4d21-a8e8-7c4e8a9851b4", StartCount: 1}
344+
var gotReq *http.Request
345+
346+
_, err := run(context.Background(), options{
347+
Config: cfg,
348+
Version: "v0.18.0",
349+
URL: "https://updates.example/check",
350+
Client: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
351+
gotReq = req
352+
return &http.Response{
353+
StatusCode: http.StatusNoContent,
354+
Body: io.NopCloser(strings.NewReader("")),
355+
}, nil
356+
})},
357+
SaveConfig: func(*Config) error { return nil },
358+
Env: func(string) string { return "" },
359+
})
360+
361+
require.NoError(t, err)
362+
require.NotNil(t, gotReq)
363+
_, hasCI := gotReq.URL.Query()["ci"]
364+
assert.False(t, hasCI, "ci param should be absent when not running in CI")
365+
}
366+
367+
func TestRun_CISkipsPersistentState(t *testing.T) {
368+
dir := t.TempDir()
369+
t.Setenv("POUTINE_CONFIG_DIR", dir)
370+
t.Setenv(DisableEnv, "")
371+
t.Setenv(CIEnv, "true")
372+
t.Setenv(URLEnv, "https://override.example/check")
373+
374+
// Stub the HTTP transport via the default client used by Run. Because Run
375+
// constructs its own client, we can't intercept the request here; what we
376+
// care about is that no state file is written on disk after the call.
377+
_ = Run(context.Background(), "v0.18.0", false)
378+
379+
_, err := os.Stat(filepath.Join(dir, "config.yaml"))
380+
assert.True(t, os.IsNotExist(err), "no state file should be written in CI mode")
381+
}
382+
383+
func TestIsCIEnv(t *testing.T) {
384+
for _, value := range []string{"1", "true", "TRUE", "yes", "on"} {
385+
assert.True(t, isCIEnv(value), value)
386+
}
387+
for _, value := range []string{"", "0", "false", "no", "off", "anything"} {
388+
assert.False(t, isCIEnv(value), value)
389+
}
390+
}
391+
311392
func TestRun_DisabledShortCircuitsBeforeAnyDiskWrite(t *testing.T) {
312393
dir := t.TempDir()
313394
t.Setenv("POUTINE_CONFIG_DIR", dir)

0 commit comments

Comments
 (0)