From 4bc9efc39aa160a3ae9ee023ecdb4de872396fe5 Mon Sep 17 00:00:00 2001 From: Pedro Enrique Date: Mon, 1 Sep 2025 17:24:08 +0200 Subject: [PATCH 1/2] fix: prevent local development env files from syncing to production Local development files (.env.development, .env.local, etc.) should never be synced to the production database. This was causing the CLI to prompt users to sync development-only environment variables to their cloud project. The fix adds a conditional check to only call HandleMissingProjectEnvs when not in local development mode (isLocalDev = false). Fixes issue where users were getting prompted to sync env vars from .env.development files to production. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-94a6a673-ef07-4dbd-95a8-8437d17bdd21 --- internal/envutil/envutil.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/envutil/envutil.go b/internal/envutil/envutil.go index 5a4776fe..703b8abf 100644 --- a/internal/envutil/envutil.go +++ b/internal/envutil/envutil.go @@ -66,7 +66,11 @@ func ProcessEnvFiles(ctx context.Context, logger logger.Logger, dir string, thep errsystem.WithContextMessage("Error parsing .env file")).ShowErrorAndExit() } - projectData = HandleMissingProjectEnvs(ctx, logger, le, projectData, theproject, apiUrl, token, force) + // Only sync env vars to production when not in local development mode + // Local development files (.env.development, .env.local, etc.) should never be synced to production + if !isLocalDev { + projectData = HandleMissingProjectEnvs(ctx, logger, le, projectData, theproject, apiUrl, token, force) + } envFile.Env = le return envFile, projectData } From 01fa60b35cc730dbc6bdc97990b9e5c2c1b3590d Mon Sep 17 00:00:00 2001 From: Pedro Enrique Date: Mon, 1 Sep 2025 17:27:29 +0200 Subject: [PATCH 2/2] refactor: extract ProcessEnvFiles into testable functions with unit tests - Extract DetermineEnvFilename() for env file selection logic - Extract ParseAndProcessEnvFile() for file parsing and template processing - Extract ShouldSyncToProduction() for production sync decision logic - Add comprehensive unit tests covering: - Environment file selection (production vs development) - File creation when .env.development doesn't exist - Production sync conditional logic based on isLocalDev flag - Fix bug where created .env.development file path wasn't returned correctly This refactor makes the code more maintainable and ensures the core logic for preventing local development env vars from syncing to production is well-tested. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-94a6a673-ef07-4dbd-95a8-8437d17bdd21 --- internal/envutil/envutil.go | 62 +++++++++++------ internal/envutil/envutil_test.go | 111 +++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 19 deletions(-) diff --git a/internal/envutil/envutil.go b/internal/envutil/envutil.go index 703b8abf..23c7bfd6 100644 --- a/internal/envutil/envutil.go +++ b/internal/envutil/envutil.go @@ -30,8 +30,8 @@ var IsAgentuityEnv = isAgentuityEnv var looksLikeSecret = regexp.MustCompile(`(?i)(^|_|-)(APIKEY|API_KEY|PRIVATE_KEY|KEY|SECRET|TOKEN|CREDENTIAL|CREDENTIALS|PASSWORD|sk_[a-zA-Z0-9_-]*|BEARER|AUTH|JWT|WEBHOOK)($|_|-)`) var isAgentuityEnv = regexp.MustCompile(`(?i)AGENTUITY_`) -// ProcessEnvFiles handles .env and template env processing -func ProcessEnvFiles(ctx context.Context, logger logger.Logger, dir string, theproject *project.Project, projectData *project.ProjectData, apiUrl, token string, force bool, isLocalDev bool) (*deployer.EnvFile, *project.ProjectData) { +// DetermineEnvFilename determines which env file to use based on isLocalDev flag +func DetermineEnvFilename(dir string, isLocalDev bool) (string, error) { envfilename := filepath.Join(dir, ".env") if isLocalDev { f := filepath.Join(dir, ".env.development") @@ -42,36 +42,60 @@ func ProcessEnvFiles(ctx context.Context, logger logger.Logger, dir string, thep // but since its gitignore it won't get checked in and might not exist when you clone a project of, err := os.Create(f) if err != nil { - errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage("Failed to create .env.development file")).ShowErrorAndExit() + return "", err } defer of.Close() of.WriteString("# This file is used to store development environment variables\n") + envfilename = f } } - var envFile *deployer.EnvFile - if (tui.HasTTY || force) && util.Exists(envfilename) { - // attempt to see if we have any template files - templateEnvs := ReadPossibleEnvTemplateFiles(dir) + return envfilename, nil +} - le, err := env.ParseEnvFileWithComments(envfilename) - if err != nil { - errsystem.New(errsystem.ErrParseEnvironmentFile, err, - errsystem.WithContextMessage("Error parsing .env file")).ShowErrorAndExit() - } - envFile = &deployer.EnvFile{Filepath: envfilename, Env: le} +// ParseAndProcessEnvFile parses an env file and processes template envs +func ParseAndProcessEnvFile(logger logger.Logger, dir, envfilename string, force bool) (*deployer.EnvFile, error) { + templateEnvs := ReadPossibleEnvTemplateFiles(dir) - le, err = HandleMissingTemplateEnvs(logger, dir, envfilename, le, templateEnvs, force) + le, err := env.ParseEnvFileWithComments(envfilename) + if err != nil { + return nil, err + } + envFile := &deployer.EnvFile{Filepath: envfilename, Env: le} + + le, err = HandleMissingTemplateEnvs(logger, dir, envfilename, le, templateEnvs, force) + if err != nil { + return nil, err + } + + envFile.Env = le + return envFile, nil +} + +// ShouldSyncToProduction determines if env vars should be synced to production based on mode +func ShouldSyncToProduction(isLocalDev bool) bool { + // Only sync env vars to production when not in local development mode + // Local development files (.env.development, .env.local, etc.) should never be synced to production + return !isLocalDev +} + +// ProcessEnvFiles handles .env and template env processing +func ProcessEnvFiles(ctx context.Context, logger logger.Logger, dir string, theproject *project.Project, projectData *project.ProjectData, apiUrl, token string, force bool, isLocalDev bool) (*deployer.EnvFile, *project.ProjectData) { + envfilename, err := DetermineEnvFilename(dir, isLocalDev) + if err != nil { + errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage("Failed to create .env.development file")).ShowErrorAndExit() + } + + var envFile *deployer.EnvFile + if (tui.HasTTY || force) && util.Exists(envfilename) { + envFile, err = ParseAndProcessEnvFile(logger, dir, envfilename, force) if err != nil { errsystem.New(errsystem.ErrParseEnvironmentFile, err, errsystem.WithContextMessage("Error parsing .env file")).ShowErrorAndExit() } - // Only sync env vars to production when not in local development mode - // Local development files (.env.development, .env.local, etc.) should never be synced to production - if !isLocalDev { - projectData = HandleMissingProjectEnvs(ctx, logger, le, projectData, theproject, apiUrl, token, force) + if ShouldSyncToProduction(isLocalDev) { + projectData = HandleMissingProjectEnvs(ctx, logger, envFile.Env, projectData, theproject, apiUrl, token, force) } - envFile.Env = le return envFile, projectData } return envFile, projectData diff --git a/internal/envutil/envutil_test.go b/internal/envutil/envutil_test.go index ab3c8bb6..b86fdc3d 100644 --- a/internal/envutil/envutil_test.go +++ b/internal/envutil/envutil_test.go @@ -1,6 +1,8 @@ package envutil import ( + "os" + "path/filepath" "testing" ) @@ -76,3 +78,112 @@ func TestIsAgentuityEnv(t *testing.T) { } } } + +func TestShouldSyncToProduction(t *testing.T) { + tests := []struct { + name string + isLocalDev bool + shouldSync bool + }{ + { + name: "production mode should sync", + isLocalDev: false, + shouldSync: true, + }, + { + name: "local development mode should not sync", + isLocalDev: true, + shouldSync: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ShouldSyncToProduction(tt.isLocalDev) + if result != tt.shouldSync { + t.Errorf("ShouldSyncToProduction(%v) = %v, want %v", tt.isLocalDev, result, tt.shouldSync) + } + }) + } +} + +func TestDetermineEnvFilename(t *testing.T) { + // Create a temporary directory for testing + tempDir := t.TempDir() + + tests := []struct { + name string + isLocalDev bool + existingFiles []string + expectedFile string + shouldCreateDev bool + }{ + { + name: "production mode uses .env", + isLocalDev: false, + existingFiles: []string{".env"}, + expectedFile: ".env", + }, + { + name: "local dev mode uses .env.development when it exists", + isLocalDev: true, + existingFiles: []string{".env", ".env.development"}, + expectedFile: ".env.development", + }, + { + name: "local dev mode creates .env.development when it doesn't exist", + isLocalDev: true, + existingFiles: []string{".env"}, + expectedFile: ".env.development", + shouldCreateDev: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a subdirectory for this test + testDir := filepath.Join(tempDir, tt.name) + err := os.MkdirAll(testDir, 0755) + if err != nil { + t.Fatalf("Failed to create test directory: %v", err) + } + + // Create existing files + for _, filename := range tt.existingFiles { + filePath := filepath.Join(testDir, filename) + err := os.WriteFile(filePath, []byte("TEST_VAR=test_value\n"), 0644) + if err != nil { + t.Fatalf("Failed to create test file %s: %v", filename, err) + } + } + + result, err := DetermineEnvFilename(testDir, tt.isLocalDev) + if err != nil { + t.Fatalf("DetermineEnvFilename() error = %v", err) + } + + expectedPath := filepath.Join(testDir, tt.expectedFile) + if result != expectedPath { + t.Errorf("DetermineEnvFilename() = %v, want %v", result, expectedPath) + } + + // Check if .env.development was created when expected + if tt.shouldCreateDev { + devFile := filepath.Join(testDir, ".env.development") + if _, err := os.Stat(devFile); os.IsNotExist(err) { + t.Errorf("Expected .env.development to be created, but it doesn't exist") + } else { + // Check content + content, err := os.ReadFile(devFile) + if err != nil { + t.Fatalf("Failed to read created .env.development: %v", err) + } + expectedContent := "# This file is used to store development environment variables\n" + if string(content) != expectedContent { + t.Errorf("Created .env.development content = %q, want %q", string(content), expectedContent) + } + } + } + }) + } +}