From 96c445ed22ce0848a882f8276da7d45f7569198e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 06:28:22 +0000 Subject: [PATCH 1/2] Initial plan From 938031c34258c436d5f0476195e97b23261a2b43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 06:37:51 +0000 Subject: [PATCH 2/2] fix: replace sync.Once reset with mutex-guarded struct to eliminate data race Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/repo.go | 51 ++++++++++++------- .../repository_features_validation.go | 46 +++++++++++------ 2 files changed, 63 insertions(+), 34 deletions(-) diff --git a/pkg/cli/repo.go b/pkg/cli/repo.go index 71e9779653b..82e81153a56 100644 --- a/pkg/cli/repo.go +++ b/pkg/cli/repo.go @@ -13,19 +13,27 @@ import ( var repoLog = logger.New("cli:repo") +// repoSlugCacheState holds the cached repository slug and protects it with a mutex. +// Using a mutex-guarded struct instead of sync.Once avoids the data race that arises +// when resetting sync.Once via struct assignment (= sync.Once{}) after first use. +type repoSlugCacheState struct { + mu sync.Mutex + result string + err error + done bool +} + // Global cache for current repository info -var ( - getCurrentRepoSlugOnce sync.Once - currentRepoSlugResult string - currentRepoSlugError error -) +var currentRepoSlugCache repoSlugCacheState -// ClearCurrentRepoSlugCache clears the current repository slug cache -// This is useful for testing or when repository context might have changed +// ClearCurrentRepoSlugCache clears the current repository slug cache. +// This is useful for testing or when repository context might have changed. func ClearCurrentRepoSlugCache() { - getCurrentRepoSlugOnce = sync.Once{} - currentRepoSlugResult = "" - currentRepoSlugError = nil + currentRepoSlugCache.mu.Lock() + defer currentRepoSlugCache.mu.Unlock() + currentRepoSlugCache.result = "" + currentRepoSlugCache.err = nil + currentRepoSlugCache.done = false } // getCurrentRepoSlugUncached gets the current repository slug (owner/repo) using gh CLI (uncached) @@ -91,19 +99,24 @@ func getCurrentRepoSlugUncached() (string, error) { return repoPath, nil } -// GetCurrentRepoSlug gets the current repository slug with caching using sync.Once -// This is the recommended function to use for repository access across the codebase +// GetCurrentRepoSlug gets the current repository slug with caching. +// This is the recommended function to use for repository access across the codebase. func GetCurrentRepoSlug() (string, error) { - getCurrentRepoSlugOnce.Do(func() { - currentRepoSlugResult, currentRepoSlugError = getCurrentRepoSlugUncached() - }) + currentRepoSlugCache.mu.Lock() + if !currentRepoSlugCache.done { + currentRepoSlugCache.result, currentRepoSlugCache.err = getCurrentRepoSlugUncached() + currentRepoSlugCache.done = true + } + result := currentRepoSlugCache.result + err := currentRepoSlugCache.err + currentRepoSlugCache.mu.Unlock() - if currentRepoSlugError != nil { - return "", currentRepoSlugError + if err != nil { + return "", err } - repoLog.Printf("Using cached repository slug: %s", currentRepoSlugResult) - return currentRepoSlugResult, nil + repoLog.Printf("Using cached repository slug: %s", result) + return result, nil } // SplitRepoSlug wraps repoutil.SplitRepoSlug for backward compatibility. diff --git a/pkg/workflow/repository_features_validation.go b/pkg/workflow/repository_features_validation.go index dc0030111d9..8798e2cc2cf 100644 --- a/pkg/workflow/repository_features_validation.go +++ b/pkg/workflow/repository_features_validation.go @@ -60,17 +60,26 @@ type RepositoryFeatures struct { HasIssues bool } +// currentRepositoryCacheState holds the cached current repository and protects it +// with a mutex. Using a mutex-guarded struct instead of sync.Once avoids the data +// race that arises when resetting sync.Once via struct assignment (= sync.Once{}) +// after first use. +type currentRepositoryCacheState struct { + mu sync.Mutex + result string + err error + done bool +} + // Global cache for repository features and current repository info var ( repositoryFeaturesCache = sync.Map{} // sync.Map is thread-safe and efficient for read-heavy workloads repositoryFeaturesLoggedCache = sync.Map{} // Tracks which repositories have had their success messages logged - getCurrentRepositoryOnce sync.Once - currentRepositoryResult string - currentRepositoryError error + currentRepositoryCache currentRepositoryCacheState ) -// ClearRepositoryFeaturesCache clears the repository features cache -// This is useful for testing or when repository settings might have changed +// ClearRepositoryFeaturesCache clears the repository features cache. +// This is useful for testing or when repository settings might have changed. func ClearRepositoryFeaturesCache() { // Clear the features cache repositoryFeaturesCache.Range(func(key, value any) bool { @@ -85,9 +94,11 @@ func ClearRepositoryFeaturesCache() { }) // Reset the current repository cache - getCurrentRepositoryOnce = sync.Once{} - currentRepositoryResult = "" - currentRepositoryError = nil + currentRepositoryCache.mu.Lock() + currentRepositoryCache.result = "" + currentRepositoryCache.err = nil + currentRepositoryCache.done = false + currentRepositoryCache.mu.Unlock() repositoryFeaturesLog.Print("Repository features and current repository caches cleared") } @@ -181,16 +192,21 @@ func (c *Compiler) validateRepositoryFeatures(workflowData *WorkflowData) error // getCurrentRepository gets the current repository from git context (with caching) func getCurrentRepository() (string, error) { - getCurrentRepositoryOnce.Do(func() { - currentRepositoryResult, currentRepositoryError = getCurrentRepositoryUncached() - }) + currentRepositoryCache.mu.Lock() + if !currentRepositoryCache.done { + currentRepositoryCache.result, currentRepositoryCache.err = getCurrentRepositoryUncached() + currentRepositoryCache.done = true + } + result := currentRepositoryCache.result + err := currentRepositoryCache.err + currentRepositoryCache.mu.Unlock() - if currentRepositoryError != nil { - return "", currentRepositoryError + if err != nil { + return "", err } - repositoryFeaturesLog.Printf("Using cached current repository: %s", currentRepositoryResult) - return currentRepositoryResult, nil + repositoryFeaturesLog.Printf("Using cached current repository: %s", result) + return result, nil } // getCurrentRepositoryUncached fetches the current repository from gh CLI (no caching)