diff --git a/internal/envutil/envutil.go b/internal/envutil/envutil.go index 920587a9..713c117e 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, envfilename) + if ShouldSyncToProduction(isLocalDev) { + projectData = HandleMissingProjectEnvs(ctx, logger, envFile.Env, projectData, theproject, apiUrl, token, force, envfilename) } - 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) + } + } + } + }) + } +}