Skip to content

Commit d6462fd

Browse files
committed
Skip constraint cache writes under --check
A --check dry run must not mutate disk, but FetchConstraints wrote the fetched artifact into the cache dir on every successful live fetch — so a dry run still populated the cache whenever it was writable, even though the pipeline skips every other write-side step under --check. Thread a writeCache flag through FetchConstraints; the pipeline passes !p.Check. An existing cache is still read for offline fallback, since reading is not a mutation. Assert both directly (FetchConstraints with writeCache=false leaves the cache dir empty) and end-to-end (the --check pipeline test now checks the cache dir stays empty too). Reported by codex review of the pipeline PR. Co-authored-by: Isaac
1 parent 6f31579 commit d6462fd

4 files changed

Lines changed: 47 additions & 14 deletions

File tree

libs/localenv/constraints.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,12 @@ func writeCacheAtomic(path string, data []byte) error {
137137
// baseURL points at the repo hosting the constraint artifacts (see
138138
// RepoConstraintBaseURL); it is empty when no source is configured, which is
139139
// reported below as a fetch-phase error.
140-
func FetchConstraints(ctx context.Context, baseURL, envKey, cacheDir string) (*Constraints, error) {
140+
//
141+
// writeCache controls whether a successful live fetch populates the on-disk
142+
// cache. Callers pass false for a dry run (--check), which must not mutate
143+
// disk; an existing cache is still read for offline fallback, since reading is
144+
// not a mutation.
145+
func FetchConstraints(ctx context.Context, baseURL, envKey, cacheDir string, writeCache bool) (*Constraints, error) {
141146
if baseURL == "" {
142147
// No constraint host is configured. This is reported at the fetch phase (as
143148
// E_FETCH) rather than aborting earlier, so the failure flows through the
@@ -158,9 +163,12 @@ func FetchConstraints(ctx context.Context, baseURL, envKey, cacheDir string) (*C
158163
return nil, fmt.Errorf("parse constraints for %s: %w", envKey, err)
159164
}
160165
// Write the cache copy (creating cacheDir if needed, atomically); non-fatal
161-
// so a read-only cacheDir doesn't break the command.
162-
if err := writeCacheAtomic(cachePath, data); err != nil {
163-
log.Debugf(ctx, "failed to write constraint cache %s: %v", filepath.ToSlash(cachePath), err)
166+
// so a read-only cacheDir doesn't break the command. Skipped under a dry
167+
// run so --check performs no disk writes at all.
168+
if writeCache {
169+
if err := writeCacheAtomic(cachePath, data); err != nil {
170+
log.Debugf(ctx, "failed to write constraint cache %s: %v", filepath.ToSlash(cachePath), err)
171+
}
164172
}
165173
return &Constraints{
166174
EnvKey: envKey,

libs/localenv/constraints_test.go

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func TestRepoConstraintBaseURL(t *testing.T) {
3030
func TestFetchConstraintsNoSourceConfigured(t *testing.T) {
3131
// An empty base URL means no constraint host is configured; it must classify as
3232
// E_FETCH (surfaced at the fetch phase) and name the env var to set.
33-
_, err := FetchConstraints(t.Context(), "", "serverless/serverless-v4", t.TempDir())
33+
_, err := FetchConstraints(t.Context(), "", "serverless/serverless-v4", t.TempDir(), true)
3434
var pe *PipelineError
3535
require.ErrorAs(t, err, &pe)
3636
assert.Equal(t, ErrFetch, pe.Code)
@@ -116,14 +116,31 @@ func TestFetchConstraintsCreatesCacheDir(t *testing.T) {
116116
}))
117117
defer srv.Close()
118118

119-
_, err := FetchConstraints(t.Context(), srv.URL, "serverless/serverless-v4", cacheDir)
119+
_, err := FetchConstraints(t.Context(), srv.URL, "serverless/serverless-v4", cacheDir, true)
120120
require.NoError(t, err)
121121
// The cache file was written into the freshly created directory.
122122
written, err := os.ReadFile(filepath.Join(cacheDir, cacheFileName("serverless/serverless-v4")))
123123
require.NoError(t, err)
124124
assert.Equal(t, sampleToml, string(written))
125125
}
126126

127+
func TestFetchConstraintsSkipsCacheWriteWhenDisabled(t *testing.T) {
128+
// With writeCache=false (the --check dry-run path), a successful live fetch
129+
// must not write anything to cacheDir.
130+
cacheDir := t.TempDir()
131+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
132+
_, _ = w.Write([]byte(sampleToml))
133+
}))
134+
defer srv.Close()
135+
136+
c, err := FetchConstraints(t.Context(), srv.URL, "serverless/serverless-v4", cacheDir, false)
137+
require.NoError(t, err)
138+
assert.False(t, c.FromCache)
139+
entries, err := os.ReadDir(cacheDir)
140+
require.NoError(t, err)
141+
assert.Empty(t, entries, "no cache file should be written under --check")
142+
}
143+
127144
func TestCacheFileNameInjective(t *testing.T) {
128145
// Distinct env keys that flatten to the same slug must not collide, so a
129146
// cache hit can never serve another environment's constraints.
@@ -140,7 +157,7 @@ func TestFetchConstraintsHTTP(t *testing.T) {
140157
}))
141158
defer srv.Close()
142159

143-
c, err := FetchConstraints(t.Context(), srv.URL, "serverless/serverless-v4", t.TempDir())
160+
c, err := FetchConstraints(t.Context(), srv.URL, "serverless/serverless-v4", t.TempDir(), true)
144161
require.NoError(t, err)
145162
assert.False(t, c.FromCache)
146163
assert.Equal(t, "databricks-connect~=17.2.0", c.DatabricksConnect)
@@ -155,7 +172,7 @@ func TestFetchConstraintsEnvKeyNotFound(t *testing.T) {
155172
}))
156173
defer srv.Close()
157174

158-
_, err := FetchConstraints(t.Context(), srv.URL, "dbr/99.9.x-scala2.12", t.TempDir())
175+
_, err := FetchConstraints(t.Context(), srv.URL, "dbr/99.9.x-scala2.12", t.TempDir(), true)
159176
var pe *PipelineError
160177
require.ErrorAs(t, err, &pe)
161178
assert.Equal(t, ErrEnvUnsupported, pe.Code)
@@ -167,7 +184,7 @@ func TestFetchConstraintsTransportFailureNoCache(t *testing.T) {
167184
url := down.URL
168185
down.Close()
169186

170-
_, err := FetchConstraints(t.Context(), url, "serverless/serverless-v4", t.TempDir())
187+
_, err := FetchConstraints(t.Context(), url, "serverless/serverless-v4", t.TempDir(), true)
171188
var pe *PipelineError
172189
require.ErrorAs(t, err, &pe)
173190
assert.Equal(t, ErrFetch, pe.Code)
@@ -182,7 +199,7 @@ func TestFetchConstraintsRejectsOversizedBody(t *testing.T) {
182199
}))
183200
defer srv.Close()
184201

185-
_, err := FetchConstraints(t.Context(), srv.URL, "serverless/serverless-v4", t.TempDir())
202+
_, err := FetchConstraints(t.Context(), srv.URL, "serverless/serverless-v4", t.TempDir(), true)
186203
var pe *PipelineError
187204
require.ErrorAs(t, err, &pe)
188205
assert.Equal(t, ErrFetch, pe.Code)
@@ -194,12 +211,12 @@ func TestFetchConstraintsFallsBackToCache(t *testing.T) {
194211
good := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
195212
_, _ = w.Write([]byte(sampleToml))
196213
}))
197-
_, err := FetchConstraints(t.Context(), good.URL, "serverless/serverless-v4", cacheDir)
214+
_, err := FetchConstraints(t.Context(), good.URL, "serverless/serverless-v4", cacheDir, true)
198215
require.NoError(t, err)
199216
good.Close()
200217

201218
// Now the server is down; fetch must serve the cache.
202-
c, err := FetchConstraints(t.Context(), good.URL, "serverless/serverless-v4", cacheDir)
219+
c, err := FetchConstraints(t.Context(), good.URL, "serverless/serverless-v4", cacheDir, true)
203220
require.NoError(t, err)
204221
assert.True(t, c.FromCache)
205222
}

libs/localenv/pipeline.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,10 @@ func (p *Pipeline) resolve(ctx context.Context) (*TargetInfo, error) {
187187
}
188188

189189
// fetch fetches constraints for the resolved target and records the fetch phase.
190+
// Under --check the cache is not populated, so a dry run performs no disk writes
191+
// (an existing cache is still read for offline fallback).
190192
func (p *Pipeline) fetch(ctx context.Context, target *TargetInfo) (*Constraints, error) {
191-
c, err := FetchConstraints(ctx, p.ConstraintBaseURL, target.EnvKey, p.CacheDir)
193+
c, err := FetchConstraints(ctx, p.ConstraintBaseURL, target.EnvKey, p.CacheDir, !p.Check)
192194
if err != nil {
193195
// FetchConstraints classifies the cause: E_ENV_UNSUPPORTED for a missing
194196
// key (404) versus E_FETCH for transport failure with no cache. Both are

libs/localenv/pipeline_test.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,13 @@ func newTestServer(t *testing.T) *httptest.Server {
8181
func TestPipelineCheckMutatesNothing(t *testing.T) {
8282
dir := writeProject(t)
8383
before, _ := os.ReadFile(filepath.Join(dir, "pyproject.toml"))
84+
cacheDir := t.TempDir()
8485
srv := newTestServer(t)
8586
defer srv.Close()
8687

8788
p := &Pipeline{
8889
Mode: ModeDefault, Check: true, ProjectDir: dir,
89-
ConstraintBaseURL: srv.URL, CacheDir: t.TempDir(),
90+
ConstraintBaseURL: srv.URL, CacheDir: cacheDir,
9091
Flags: TargetFlags{Serverless: "v4"},
9192
Compute: stubCompute{}, PM: fakePM{py: "3.12", dbc: "17.2.0"},
9293
}
@@ -98,6 +99,11 @@ func TestPipelineCheckMutatesNothing(t *testing.T) {
9899
assert.Contains(t, res.Plan.Diff, "==3.12.*")
99100
after, _ := os.ReadFile(filepath.Join(dir, "pyproject.toml"))
100101
assert.Equal(t, string(before), string(after)) // unchanged
102+
103+
// --check must not populate the constraint cache either (no disk writes).
104+
entries, err := os.ReadDir(cacheDir)
105+
require.NoError(t, err)
106+
assert.Empty(t, entries)
101107
}
102108

103109
func TestPipelineCheckDoesNotProvision(t *testing.T) {

0 commit comments

Comments
 (0)