Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
62 changes: 43 additions & 19 deletions internal/envutil/envutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand Down
111 changes: 111 additions & 0 deletions internal/envutil/envutil_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package envutil

import (
"os"
"path/filepath"
"testing"
)

Expand Down Expand Up @@ -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)
}
}
}
})
}
}
Loading