Skip to content
Open
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
79f7203
Initial plan
Copilot Mar 12, 2026
c2ec8e8
test: add comprehensive tests for flags, filesystem, http, and functi…
Copilot Mar 12, 2026
c95ed53
test: fix cross-platform path issues in tests per code review
Copilot Mar 12, 2026
138797d
fix: correct go-fumpt formatting and update NOTICE file URL path
Copilot Mar 12, 2026
c305c40
fix: revert incorrect NOTICE URL - /v2/LICENSE.txt is the correct path
Copilot Mar 13, 2026
cbc82f1
Merge branch 'main' into copilot/increase-test-coverage-high-impact
nitrocode Mar 13, 2026
919e954
Changes before error encountered
Copilot Mar 19, 2026
9d7bcf3
fix: address CodeRabbit feedback - strengthen tests and document cont…
Copilot Mar 19, 2026
af1b9a6
Merge branch 'main' into copilot/increase-test-coverage-high-impact
nitrocode Mar 19, 2026
cff8ea2
fix: address CodeRabbit feedback - strengthen tests and fix data races
Copilot Mar 20, 2026
84f0c95
fix: address code review feedback - use type assertions and nil guards
Copilot Mar 20, 2026
a059e07
fix: address CodeRabbit audit - injectable viper, ResetPathMatchCache…
Copilot Mar 20, 2026
de7c7cc
Merge branch 'main' into copilot/increase-test-coverage-high-impact
nitrocode Mar 22, 2026
8f91e21
fix: address CodeRabbit round 2 - isolated viper in tests, t.TempDir(…
Copilot Mar 22, 2026
1659bf2
fix: address CodeRabbit round 3 audit - nil guard, behavioral tests, …
Copilot Mar 22, 2026
04f8d19
fix: address CodeRabbit round 4 audit - PathMatch cleanup, test namin…
Copilot Mar 22, 2026
f2bf8e1
fix: address CodeRabbit round 5 - phantom-path caching bug, multi-tok…
Copilot Mar 22, 2026
0f23667
fix: address CodeRabbit round 6 - path cache reset, cache key normali…
Copilot Mar 22, 2026
525f416
fix: address CodeRabbit round 7 - non-nil contract, missing-dir subte…
Copilot Mar 22, 2026
8b9554c
fix: address CodeRabbit round 8 - comma-safe glob cache, reflect poin…
Copilot Mar 22, 2026
04a4a0a
fix: address CodeRabbit round 9 - nil-check regression, subdomain tes…
Copilot Mar 23, 2026
fe62aae
Merge branch 'main' into copilot/increase-test-coverage-high-impact
nitrocode Mar 23, 2026
4826a8e
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 23, 2026
3d15b91
feat: LRU cache for filesystem glob, GHES auth, Windows tests, doc.go…
Copilot Mar 23, 2026
7c2e8c6
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 23, 2026
f111de4
feat: add uploads.github.com, eviction counter, Client doc cross-links
Copilot Mar 23, 2026
c495d14
feat: env-configurable glob cache, host normalization, redirect safet…
Copilot Mar 23, 2026
aa5d84a
feat: expvar counters, Windows while-open test, test-race Makefile ta…
Copilot Mar 23, 2026
b50e896
Merge branch 'main' into copilot/increase-test-coverage-high-impact
nitrocode Mar 23, 2026
1053cdb
feat: add config clamping tests and all redirect status code tests (3…
Copilot Mar 23, 2026
b2e83cb
Merge branch 'main' into copilot/increase-test-coverage-high-impact
nitrocode Mar 24, 2026
8ca5417
feat: add WithGitHubHostMatcher precedence table test (11 subtests) a…
Copilot Mar 24, 2026
69b1d82
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 24, 2026
bc5657f
feat: clamp glob cache minimums (TTL≥1s, entries≥16) and strip defaul…
Copilot Mar 24, 2026
7704902
fix: apply CodeRabbit auto-fixes
coderabbitai[bot] Mar 24, 2026
b80a3a1
fix: restore trailing newlines and remove invalid Windows delete-whil…
Copilot Mar 25, 2026
35885a2
Merge branch 'main' into copilot/increase-test-coverage-high-impact
nitrocode Mar 27, 2026
4187e60
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 27, 2026
fa508cc
fix: log when glob cache env vars are clamped to minimums; exclude ta…
Copilot Mar 27, 2026
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
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Changelog

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://www.conventionalcommits.org/) for commit message format.

---

## [Unreleased]

### Changed

- **`pkg/filesystem.GetGlobMatches`**: always returns a non-nil `[]string{}` (never `nil`).
Callers must use `len(result) == 0` to detect no matches instead of `result == nil`.
The cache is now bounded and configurable via three environment variables:
- `ATMOS_FS_GLOB_CACHE_MAX_ENTRIES` (default `1024`, minimum `16`) — maximum number of cached glob patterns.
- `ATMOS_FS_GLOB_CACHE_TTL` (default `5m`, minimum `1s`) — time-to-live for each cache entry.
Values below the respective minimums are clamped up rather than rejected.
- `ATMOS_FS_GLOB_CACHE_EMPTY` (default `1`) — set to `0` to skip caching patterns that match no files.
Comment on lines +14 to +18
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify how non-positive cache settings behave.

applyGlobCacheConfig only clamps positive values below the floor. 0 and negative values currently fall back to defaults, so "values below the respective minimums are clamped up" reads broader than the shipped behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` around lines 14 - 18, Update the CHANGELOG text to accurately
reflect the runtime behavior implemented in applyGlobCacheConfig: state that
only positive values below the minimums are clamped up to the floor, whereas
zero or negative values for ATMOS_FS_GLOB_CACHE_MAX_ENTRIES,
ATMOS_FS_GLOB_CACHE_TTL, and ATMOS_FS_GLOB_CACHE_EMPTY cause the code to fall
back to the default values rather than being clamped; mention
applyGlobCacheConfig by name so readers and reviewers can find the exact logic.

- **`pkg/http.normalizeHost`**: now strips default ports (`:443`, `:80`) in addition to
lower-casing and removing trailing dots, so that `api.github.com:443` is treated
identically to `api.github.com` for allowlist matching.

### Added

- **`pkg/filesystem`**: expvar observability counters (`atmos_glob_cache_hits`,
`atmos_glob_cache_misses`, `atmos_glob_cache_evictions`, `atmos_glob_cache_len`) published
via `RegisterGlobCacheExpvars()` and accessible at `/debug/vars` when the HTTP debug
server is enabled.
- **`pkg/http`**: host-matcher three-level precedence documented and tested:
1. `WithGitHubHostMatcher` — custom predicate always wins.
2. `GITHUB_API_URL` — GHES hostname added to allowlist when set.
3. Built-in allowlist — `api.github.com`, `raw.githubusercontent.com`, `uploads.github.com`.
Authorization is only injected over HTTPS and stripped on cross-host redirects
(301 / 302 / 303 / 307 / 308) to prevent token leakage.

### Security

- Cross-host HTTP redirects (all five status codes: 301, 302, 303, 307, 308) no longer
forward the `Authorization` header to the redirect target, preventing accidental token
leakage via open redirects.
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,8 @@ Auto-enabled via `RootCmd.ExecuteC()`. Non-standard paths use `telemetry.Capture

**Prerequisites**: Go 1.26+, golangci-lint, Make. See `.cursor/rules/atmos-rules.mdc`.

> **Minimum Go version**: `go.mod` requires Go 1.26. Test helpers use `sync.Map.Clear` (added in Go 1.23) and range-over-int (Go 1.22). CI pins the Go version via `go-version-file: go.mod`. Local development with an older toolchain will fail to compile test-only files.

**Build**: CGO disabled, cross-platform, version via ldflags, output to `./build/`

### Compilation (MANDATORY)
Expand Down
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,11 @@ link-check:
@command -v lychee >/dev/null 2>&1 || { echo "Install lychee: brew install lychee"; exit 1; }
lychee --config lychee.toml --root-dir "$(CURDIR)" '**/*.md'

.PHONY: readme lint lintroller gomodcheck build version build-linux build-windows build-macos deps version-linux version-windows version-macos testacc testacc-cover testacc-coverage test-short test-short-cover generate-mocks link-check
# Run quick tests with race detector and shuffled order.
# This target is recommended for CI to catch data races and order-dependent failures.
# Usage: make test-race
test-race: deps
@echo "Running tests with -race -shuffle=on"
go test -race -shuffle=on $(TEST) $(TESTARGS) -timeout 10m

.PHONY: readme lint lintroller gomodcheck build version build-linux build-windows build-macos deps version-linux version-windows version-macos testacc testacc-cover testacc-coverage test-short test-short-cover test-race generate-mocks link-check
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add missing phony targets to clear checkmake minphony warnings.

Line 134 still omits all, clean, and test, which keeps static-analysis warnings active.

🔧 Proposed patch
+all: build
+
+test: testacc
+
+clean:
+	rm -rf build coverage coverage.out coverage.html
+
-.PHONY: readme lint lintroller gomodcheck build version build-linux build-windows build-macos deps version-linux version-windows version-macos testacc testacc-cover testacc-coverage test-short test-short-cover test-race generate-mocks link-check
+.PHONY: all clean test readme lint lintroller gomodcheck build version build-linux build-windows build-macos deps version-linux version-windows version-macos testacc testacc-cover testacc-coverage test-short test-short-cover test-race generate-mocks link-check
🧰 Tools
🪛 checkmake (0.2.2)

[warning] 134-134: Missing required phony target "all"

(minphony)


[warning] 134-134: Missing required phony target "clean"

(minphony)


[warning] 134-134: Missing required phony target "test"

(minphony)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` at line 134, The .PHONY declaration is missing common targets which
triggers checkmake 'minphony' warnings; update the .PHONY line that currently
lists readme, lint, lintroller, gomodcheck, build, version, build-linux,
build-windows, build-macos, deps, version-linux, version-windows, version-macos,
testacc, testacc-cover, testacc-coverage, test-short, test-short-cover,
test-race, generate-mocks, link-check to also include the targets all, clean,
and test so that the Makefile's .PHONY covers those standard phony targets and
clears the static-analysis warnings.

2 changes: 1 addition & 1 deletion go.mod

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion internal/exec/stack_processor_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -1102,7 +1102,10 @@ func processYAMLConfigFileWithContextInternal(
err,
)
return nil, nil, nil, nil, nil, nil, nil, nil, errors.New(errorMessage)
} else if importMatches == nil {
} else {
// err == nil but importMatches is an empty slice (not nil): pkg/filesystem.GetGlobMatches
// guarantees a non-nil result, so the old "else if importMatches == nil" check was dead
// code. We reach this branch when no files matched and the call returned ([]string{}, nil).
Comment on lines +1106 to +1108
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix incorrect package contract reference in the inline comment.

Line 1106 references pkg/filesystem.GetGlobMatches, but this code path calls u.GetGlobMatches (pkg/utils). The mismatch can misdirect future fixes.

📝 Proposed comment-only fix
-						// err == nil but importMatches is an empty slice (not nil): pkg/filesystem.GetGlobMatches
-						// guarantees a non-nil result, so the old "else if importMatches == nil" check was dead
-						// code. We reach this branch when no files matched and the call returned ([]string{}, nil).
+						// err == nil but importMatches is an empty slice (not nil): this branch guards
+						// against empty results explicitly, independent of the specific GetGlobMatches
+						// package contract.
Based on learnings: “GetGlobMatches has intentionally divergent semantics across packages — pkg/filesystem.GetGlobMatches always returns a non-nil slice, while pkg/utils.GetGlobMatches treats no matches as an error or nil depending on doublestar.Glob result.”
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// err == nil but importMatches is an empty slice (not nil): pkg/filesystem.GetGlobMatches
// guarantees a non-nil result, so the old "else if importMatches == nil" check was dead
// code. We reach this branch when no files matched and the call returned ([]string{}, nil).
// err == nil but importMatches is an empty slice (not nil): this branch guards
// against empty results explicitly, independent of the specific GetGlobMatches
// package contract.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/exec/stack_processor_utils.go` around lines 1106 - 1108, The inline
comment incorrectly references pkg/filesystem.GetGlobMatches; update the comment
near the call to u.GetGlobMatches to mention pkg/utils.GetGlobMatches (or
u.GetGlobMatches) and clarify its semantics (that u.GetGlobMatches may return
nil on no matches depending on doublestar.Glob), replacing the misleading
sentence about pkg/filesystem.GetGlobMatches and ensuring the note explains why
the old "else if importMatches == nil" check was dead in the context of
u.GetGlobMatches.

errorMessage := fmt.Sprintf("no matches found for the import '%s' in the file '%s'",
imp,
relativeFilePath,
Expand Down
32 changes: 32 additions & 0 deletions pkg/filesystem/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Package filesystem provides file-system utilities for the Atmos CLI, including
// atomic file writes (POSIX rename and a Windows-compatible remove-before-rename
// variant) and glob-pattern matching with a bounded, time-limited LRU cache.
//
// # GetGlobMatches contract
//
// [GetGlobMatches] always returns a non-nil []string. An empty result set is
// returned as []string{}, never nil. Callers must check len(result) == 0, not
// result == nil.
//
// # Cache configuration
//
// The glob LRU cache is configurable at startup via environment variables:
//
// - ATMOS_FS_GLOB_CACHE_MAX_ENTRIES – maximum number of cached patterns
// (default: 1024, minimum: 16; values below 16 are clamped up).
// - ATMOS_FS_GLOB_CACHE_TTL – TTL per entry as a Go duration string, e.g.
// "10m" (default: 5m, minimum: 1s; values below 1s are clamped up).
// - ATMOS_FS_GLOB_CACHE_EMPTY – set to "0" or "false" to disable caching
// of empty (no-match) results (default: "1" = enabled).
//
// # Observability
//
// Three atomic int64 counters track cache activity:
// - hits, misses, evictions (accessible via [GlobCacheHits], [GlobCacheMisses],
// [GlobCacheEvictions] in tests).
//
// Call [RegisterGlobCacheExpvars] once at startup to expose these counters via
// the expvar /debug/vars HTTP endpoint:
//
// filesystem.RegisterGlobCacheExpvars()
package filesystem
100 changes: 100 additions & 0 deletions pkg/filesystem/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package filesystem

import (
"path/filepath"
"sync"
"sync/atomic"
"time"
)

// ResetGlobMatchesCache clears the glob matches LRU cache and resets all counters.
// This is exported only for testing to avoid data races from direct struct assignment.
func ResetGlobMatchesCache() {
globMatchesLRUMu.Lock()
globMatchesLRU.Purge()
globMatchesLRUMu.Unlock()
atomic.StoreInt64(&globMatchesEvictions, 0)
atomic.StoreInt64(&globMatchesHits, 0)
atomic.StoreInt64(&globMatchesMisses, 0)
}

// ResetPathMatchCache clears the path match cache.
// This is exported only for testing to ensure consistent state between tests.
func ResetPathMatchCache() {
pathMatchCacheMu.Lock()
pathMatchCache = make(map[pathMatchKey]bool)
pathMatchCacheMu.Unlock()
}

// SetGlobCacheEntryExpired forcibly marks a cache entry as expired for testing TTL eviction.
// It re-adds the entry with an expiry in the past, simulating TTL expiry.
func SetGlobCacheEntryExpired(pattern string) {
normalizedPattern := filepath.ToSlash(pattern)
globMatchesLRUMu.Lock()
if entry, ok := globMatchesLRU.Get(normalizedPattern); ok {
entry.expiry = time.Time{} // zero time is in the past
globMatchesLRU.Add(normalizedPattern, entry)
}
globMatchesLRUMu.Unlock()
}

// GlobCacheLen returns the number of entries currently in the glob LRU cache.
func GlobCacheLen() int {
globMatchesLRUMu.RLock()
defer globMatchesLRUMu.RUnlock()
return globMatchesLRU.Len()
}

// GlobCacheEvictions returns the total number of LRU evictions since the last cache reset.
// This counter is incremented atomically by the LRU eviction callback.
func GlobCacheEvictions() int64 {
return atomic.LoadInt64(&globMatchesEvictions)
}

// GlobCacheHits returns the total number of cache hits since the last cache reset.
func GlobCacheHits() int64 {
return atomic.LoadInt64(&globMatchesHits)
}

// GlobCacheMisses returns the total number of cache misses since the last cache reset.
func GlobCacheMisses() int64 {
return atomic.LoadInt64(&globMatchesMisses)
}

// ApplyGlobCacheConfigForTest re-reads ATMOS_FS_GLOB_CACHE_* env vars and reinitializes
// the glob LRU cache. Tests should call this after setting env vars via t.Setenv.
// It also resets all counters so tests start from a clean baseline.
func ApplyGlobCacheConfigForTest() {
applyGlobCacheConfig()
atomic.StoreInt64(&globMatchesEvictions, 0)
atomic.StoreInt64(&globMatchesHits, 0)
atomic.StoreInt64(&globMatchesMisses, 0)
}

// GlobCacheEmptyEnabled returns the current empty-result caching setting.
func GlobCacheEmptyEnabled() bool {
globMatchesLRUMu.RLock()
defer globMatchesLRUMu.RUnlock()
return globCacheEmptyEnabled
}

// ResetGlobExpvarOnce resets the sync.Once guard so RegisterGlobCacheExpvars
// can be called again in the same test binary. Only for use in tests that
// need to verify expvar registration after a cache reset.
func ResetGlobExpvarOnce() {
globExpvarOnce = sync.Once{}
}
Comment on lines +81 to +86
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

❓ Verification inconclusive

Script executed:

#!/bin/bash
# Check if any other test or init code calls RegisterGlobCacheExpvars
rg -n "RegisterGlobCacheExpvars" --type go -g '!*_test.go'

Repository: cloudposse/atmos


Repository: cloudposse/atmos
Exit code: 0

stdout:

pkg/filesystem/doc.go:28:// Call [RegisterGlobCacheExpvars] once at startup to expose these counters via
pkg/filesystem/doc.go:31://	filesystem.RegisterGlobCacheExpvars()
pkg/filesystem/glob_metrics.go:11:// RegisterGlobCacheExpvars multiple times (e.g., in tests) are protected.
pkg/filesystem/glob_metrics.go:14:// RegisterGlobCacheExpvars publishes the glob-cache counters as expvar integers
pkg/filesystem/glob_metrics.go:21://	filesystem.RegisterGlobCacheExpvars()
pkg/filesystem/glob_metrics.go:28:func RegisterGlobCacheExpvars() {

Script executed:

cat -n pkg/filesystem/glob_metrics.go

Repository: cloudposse/atmos


Repository: cloudposse/atmos
Exit code: 0

stdout:

     1	package filesystem
     2	
     3	import (
     4		"expvar"
     5		"sync"
     6		"sync/atomic"
     7	)
     8	
     9	// globExpvarOnce ensures expvar variables are registered at most once.
    10	// expvar.Publish panics on duplicate registration, so callers that call
    11	// RegisterGlobCacheExpvars multiple times (e.g., in tests) are protected.
    12	var globExpvarOnce sync.Once
    13	
    14	// RegisterGlobCacheExpvars publishes the glob-cache counters as expvar integers
    15	// under the /debug/vars HTTP endpoint.  The function is no-op after the first call
    16	// (duplicate registration would panic).
    17	//
    18	// Call this once at program startup to expose cache performance metrics:
    19	//
    20	//	import _ "net/http/pprof" // enable /debug/vars
    21	//	filesystem.RegisterGlobCacheExpvars()
    22	//
    23	// The following variables are published:
    24	//   - atmos_glob_cache_hits      – number of cache hits since last reset
    25	//   - atmos_glob_cache_misses    – number of cache misses since last reset
    26	//   - atmos_glob_cache_evictions – number of LRU evictions since last reset
    27	//   - atmos_glob_cache_len       – current number of entries in the cache
    28	func RegisterGlobCacheExpvars() {
    29		globExpvarOnce.Do(func() {
    30			expvar.Publish("atmos_glob_cache_hits", expvar.Func(func() any {
    31				return atomic.LoadInt64(&globMatchesHits)
    32			}))
    33			expvar.Publish("atmos_glob_cache_misses", expvar.Func(func() any {
    34				return atomic.LoadInt64(&globMatchesMisses)
    35			}))
    36			expvar.Publish("atmos_glob_cache_evictions", expvar.Func(func() any {
    37				return atomic.LoadInt64(&globMatchesEvictions)
    38			}))
    39			expvar.Publish("atmos_glob_cache_len", expvar.Func(func() any {
    40				globMatchesLRUMu.RLock()
    41				defer globMatchesLRUMu.RUnlock()
    42				return globMatchesLRU.Len()
    43			}))
    44		})
    45	}

Script executed:

rg -n "RegisterGlobCacheExpvars" --type go pkg/filesystem -A 5 -B 2

Repository: cloudposse/atmos


Repository: cloudposse/atmos
Exit code: 0

stdout:

pkg/filesystem/glob_atomic_test.go-498-}
pkg/filesystem/glob_atomic_test.go-499-
pkg/filesystem/glob_atomic_test.go:500:// TestRegisterGlobCacheExpvars verifies that RegisterGlobCacheExpvars publishes
pkg/filesystem/glob_atomic_test.go-501-// counters that reflect actual cache activity.
pkg/filesystem/glob_atomic_test.go:502:func TestRegisterGlobCacheExpvars(t *testing.T) {
pkg/filesystem/glob_atomic_test.go-503-	// ApplyGlobCacheConfigForTest re-reads env vars and reinitializes the LRU.
pkg/filesystem/glob_atomic_test.go-504-	// This is essential when a prior test (e.g. TestGetGlobMatches_EnvTTL) left
pkg/filesystem/glob_atomic_test.go-505-	// the in-package globCacheTTL at 1ns due to cleanup ordering.
pkg/filesystem/glob_atomic_test.go-506-	ApplyGlobCacheConfigForTest()
pkg/filesystem/glob_atomic_test.go-507-	ResetGlobMatchesCache()
--
pkg/filesystem/glob_atomic_test.go-513-	})
pkg/filesystem/glob_atomic_test.go-514-
pkg/filesystem/glob_atomic_test.go:515:	RegisterGlobCacheExpvars()
pkg/filesystem/glob_atomic_test.go-516-
pkg/filesystem/glob_atomic_test.go-517-	tmpDir := t.TempDir()
pkg/filesystem/glob_atomic_test.go-518-	require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "ev.yaml"), []byte(""), 0o644))
pkg/filesystem/glob_atomic_test.go-519-	pattern := filepath.Join(tmpDir, "*.yaml")
pkg/filesystem/glob_atomic_test.go-520-
--
pkg/filesystem/export_test.go-79-}
pkg/filesystem/export_test.go-80-
pkg/filesystem/export_test.go:81:// ResetGlobExpvarOnce resets the sync.Once guard so RegisterGlobCacheExpvars
pkg/filesystem/export_test.go-82-// can be called again in the same test binary.  Only for use in tests that
pkg/filesystem/export_test.go-83-// need to verify expvar registration after a cache reset.
pkg/filesystem/export_test.go-84-func ResetGlobExpvarOnce() {
pkg/filesystem/export_test.go-85-	globExpvarOnce = sync.Once{}
pkg/filesystem/export_test.go-86-}
--
pkg/filesystem/doc.go-26-//     [GlobCacheEvictions] in tests).
pkg/filesystem/doc.go-27-//
pkg/filesystem/doc.go:28:// Call [RegisterGlobCacheExpvars] once at startup to expose these counters via
pkg/filesystem/doc.go-29-// the expvar /debug/vars HTTP endpoint:
pkg/filesystem/doc.go-30-//
pkg/filesystem/doc.go:31://	filesystem.RegisterGlobCacheExpvars()
pkg/filesystem/doc.go-32-package filesystem
--
pkg/filesystem/glob_metrics.go-9-// globExpvarOnce ensures expvar variables are registered at most once.
pkg/filesystem/glob_metrics.go-10-// expvar.Publish panics on duplicate registration, so callers that call
pkg/filesystem/glob_metrics.go:11:// RegisterGlobCacheExpvars multiple times (e.g., in tests) are protected.
pkg/filesystem/glob_metrics.go-12-var globExpvarOnce sync.Once
pkg/filesystem/glob_metrics.go-13-
pkg/filesystem/glob_metrics.go:14:// RegisterGlobCacheExpvars publishes the glob-cache counters as expvar integers
pkg/filesystem/glob_metrics.go-15-// under the /debug/vars HTTP endpoint.  The function is no-op after the first call
pkg/filesystem/glob_metrics.go-16-// (duplicate registration would panic).
pkg/filesystem/glob_metrics.go-17-//
pkg/filesystem/glob_metrics.go-18-// Call this once at program startup to expose cache performance metrics:
pkg/filesystem/glob_metrics.go-19-//
pkg/filesystem/glob_metrics.go-20-//	import _ "net/http/pprof" // enable /debug/vars
pkg/filesystem/glob_metrics.go:21://	filesystem.RegisterGlobCacheExpvars()
pkg/filesystem/glob_metrics.go-22-//
pkg/filesystem/glob_metrics.go-23-// The following variables are published:
pkg/filesystem/glob_metrics.go-24-//   - atmos_glob_cache_hits      – number of cache hits since last reset
pkg/filesystem/glob_metrics.go-25-//   - atmos_glob_cache_misses    – number of cache misses since last reset
pkg/filesystem/glob_metrics.go-26-//   - atmos_glob_cache_evictions – number of LRU evictions since last reset
pkg/filesystem/glob_metrics.go-27-//   - atmos_glob_cache_len       – current number of entries in the cache
pkg/filesystem/glob_metrics.go:28:func RegisterGlobCacheExpvars() {
pkg/filesystem/glob_metrics.go-29-	globExpvarOnce.Do(func() {
pkg/filesystem/glob_metrics.go-30-		expvar.Publish("atmos_glob_cache_hits", expvar.Func(func() any {
pkg/filesystem/glob_metrics.go-31-			return atomic.LoadInt64(&globMatchesHits)
pkg/filesystem/glob_metrics.go-32-		}))
pkg/filesystem/glob_metrics.go-33-		expvar.Publish("atmos_glob_cache_misses", expvar.Func(func() any {

Script executed:

sed -n '500,550p' pkg/filesystem/glob_atomic_test.go

Repository: cloudposse/atmos


Repository: cloudposse/atmos
Exit code: 0

stdout:

// TestRegisterGlobCacheExpvars verifies that RegisterGlobCacheExpvars publishes
// counters that reflect actual cache activity.
func TestRegisterGlobCacheExpvars(t *testing.T) {
	// ApplyGlobCacheConfigForTest re-reads env vars and reinitializes the LRU.
	// This is essential when a prior test (e.g. TestGetGlobMatches_EnvTTL) left
	// the in-package globCacheTTL at 1ns due to cleanup ordering.
	ApplyGlobCacheConfigForTest()
	ResetGlobMatchesCache()
	ResetGlobExpvarOnce()
	t.Cleanup(func() {
		ApplyGlobCacheConfigForTest()
		ResetGlobMatchesCache()
		ResetGlobExpvarOnce()
	})

	RegisterGlobCacheExpvars()

	tmpDir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "ev.yaml"), []byte(""), 0o644))
	pattern := filepath.Join(tmpDir, "*.yaml")

	// First call is a miss.
	_, err := GetGlobMatches(pattern)
	require.NoError(t, err)

	// Second call is a hit.
	_, err = GetGlobMatches(pattern)
	require.NoError(t, err)

	// Verify expvar values match atomic counters.
	hitsVar := expvar.Get("atmos_glob_cache_hits")
	require.NotNil(t, hitsVar, "atmos_glob_cache_hits expvar must be registered")
	assert.Equal(t, "1", hitsVar.String(), "hit counter must be 1 after second call")

	missesVar := expvar.Get("atmos_glob_cache_misses")
	require.NotNil(t, missesVar)
	assert.Equal(t, "1", missesVar.String(), "miss counter must be 1 after first call")

	lenVar := expvar.Get("atmos_glob_cache_len")
	require.NotNil(t, lenVar)
	assert.Equal(t, "1", lenVar.String(), "cache len must be 1 after one unique pattern")
}

Script executed:

rg -n "ResetGlobExpvarOnce" --type go

Repository: cloudposse/atmos


Repository: cloudposse/atmos
Exit code: 0

stdout:

pkg/filesystem/glob_atomic_test.go:508:	ResetGlobExpvarOnce()
pkg/filesystem/glob_atomic_test.go:512:		ResetGlobExpvarOnce()
pkg/filesystem/export_test.go:81:// ResetGlobExpvarOnce resets the sync.Once guard so RegisterGlobCacheExpvars
pkg/filesystem/export_test.go:84:func ResetGlobExpvarOnce() {

Script executed:

rg -n "RegisterGlobCacheExpvars" pkg/filesystem -B 3 -A 1 | grep -E "(func Test|RegisterGlobCacheExpvars)"

Repository: cloudposse/atmos


Repository: cloudposse/atmos
Exit code: 0

stdout:

pkg/filesystem/glob_metrics.go:11:// RegisterGlobCacheExpvars multiple times (e.g., in tests) are protected.
pkg/filesystem/glob_metrics.go:14:// RegisterGlobCacheExpvars publishes the glob-cache counters as expvar integers
pkg/filesystem/glob_metrics.go:21://	filesystem.RegisterGlobCacheExpvars()
pkg/filesystem/glob_metrics.go:28:func RegisterGlobCacheExpvars() {
pkg/filesystem/glob_atomic_test.go:500:// TestRegisterGlobCacheExpvars verifies that RegisterGlobCacheExpvars publishes
pkg/filesystem/glob_atomic_test.go:502:func TestRegisterGlobCacheExpvars(t *testing.T) {
pkg/filesystem/glob_atomic_test.go:515:	RegisterGlobCacheExpvars()
pkg/filesystem/export_test.go:81:// ResetGlobExpvarOnce resets the sync.Once guard so RegisterGlobCacheExpvars
pkg/filesystem/doc.go:28:// Call [RegisterGlobCacheExpvars] once at startup to expose these counters via
pkg/filesystem/doc.go:31://	filesystem.RegisterGlobCacheExpvars()

Resetting sync.Once for expvar.Publish is fragile and creates a latent panic risk.

ResetGlobExpvarOnce allows RegisterGlobCacheExpvars to execute its body again, but expvar.Publish panics on duplicate names and provides no unpublication mechanism. The current test avoids the panic only because it's the sole caller of RegisterGlobCacheExpvars, but the cleanup leaves globExpvarOnce reset—any subsequent test that calls it (or a second run with -shuffle=on) will panic.

Either guard the re-registration (e.g., check if already published) or accept that TestRegisterGlobCacheExpvars must run in isolation. The test-only reset helper is only safe if its caller is the only code path that uses RegisterGlobCacheExpvars, which isn't enforced.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/filesystem/export_test.go` around lines 81 - 86, ResetGlobExpvarOnce is
unsafe because expvar.Publish panics on duplicate names; instead of resetting
globExpvarOnce, modify RegisterGlobCacheExpvars to be idempotent by checking
expvar.Get("<name>") before calling expvar.Publish for each published variable
(skip publish if already present) and remove or stop using ResetGlobExpvarOnce
in tests; alternatively, if you must keep the helper, restrict its use to tests
that are guaranteed to be the only callers, but the recommended fix is to make
RegisterGlobCacheExpvars perform an expvar.Get guard around expvar.Publish so
repeated calls (or shuffled tests) won't panic.


// GlobCacheTTL returns the currently active cache TTL for test introspection.
func GlobCacheTTL() time.Duration {
globMatchesLRUMu.RLock()
defer globMatchesLRUMu.RUnlock()
return globCacheTTL
}

// GlobCacheMaxEntries returns the currently configured LRU capacity for test introspection.
func GlobCacheMaxEntries() int {
globMatchesLRUMu.RLock()
defer globMatchesLRUMu.RUnlock()
return globCacheMaxEntries
}
Loading
Loading