sprout-cli 0.4.0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sprout-cli
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: CLI tool to automate git worktree and Docker Compose development workflows
5
5
  Author: SecDevLab Inc.
6
6
  License: MIT
@@ -41,7 +41,7 @@ A CLI tool to automate git worktree and Docker Compose development workflows.
41
41
  ## Installation
42
42
 
43
43
  ```bash
44
- pip install sprout
44
+ pip install sprout-cli
45
45
  ```
46
46
 
47
47
  For development:
@@ -56,7 +56,9 @@ pip install -e ".[dev]"
56
56
 
57
57
  ## Quick Start
58
58
 
59
- 1. Create a `.env.example` template in your project root (and optionally in subdirectories):
59
+ **Note**: Sprout works in any git repository. `.env.example` files are optional - if you don't have them, sprout will simply create worktrees without `.env` generation.
60
+
61
+ 1. (Optional) Create a `.env.example` template in your project root (and optionally in subdirectories) for automatic `.env` generation:
60
62
  ```env
61
63
  # API Configuration
62
64
  API_KEY={{ API_KEY }}
@@ -66,6 +68,10 @@ API_PORT={{ auto_port() }}
66
68
  DB_HOST=localhost
67
69
  DB_PORT={{ auto_port() }}
68
70
 
71
+ # Branch-specific Configuration
72
+ SERVICE_NAME=myapp-{{ branch() }}
73
+ DEPLOYMENT_ENV={{ branch() }}
74
+
69
75
  # Example: Docker Compose variables (preserved as-is)
70
76
  # sprout will NOT process ${...} syntax - it's passed through unchanged
71
77
  # DB_NAME=${DB_NAME}
@@ -86,9 +92,13 @@ repo/
86
92
  cd $(sprout create feature-branch --path)
87
93
  ```
88
94
 
95
+ **What happens when you run `sprout create`:**
96
+ - If `.env.example` files exist: Sprout will generate corresponding `.env` files with populated variables and unique port assignments
97
+ - If no `.env.example` files exist: Sprout will show a warning and create the worktree without `.env` generation
98
+
89
99
  This single command:
90
100
  - Creates a new git worktree for `feature-branch`
91
- - Generates a `.env` file from your template
101
+ - Generates `.env` files from your templates (if `.env.example` files exist)
92
102
  - Outputs the path to the new environment
93
103
  - Changes to that directory when wrapped in `cd $(...)`
94
104
 
@@ -174,7 +184,7 @@ Show the version of sprout.
174
184
 
175
185
  ## Template Syntax
176
186
 
177
- sprout supports two types of placeholders in `.env.example`:
187
+ sprout supports three types of placeholders in `.env.example`:
178
188
 
179
189
  1. **Variable Placeholders**: `{{ VARIABLE_NAME }}`
180
190
  - **First**: Checks if the variable exists in your environment (e.g., `export API_KEY=xxx`)
@@ -187,7 +197,12 @@ sprout supports two types of placeholders in `.env.example`:
187
197
  - Checks system port availability
188
198
  - Ensures global uniqueness even in monorepo setups
189
199
 
190
- 3. **Docker Compose Syntax (Preserved)**: `${VARIABLE}`
200
+ 3. **Branch Name**: `{{ branch() }}`
201
+ - Replaced with the current branch/subtree name
202
+ - Useful for branch-specific configurations
203
+ - Example: `SERVICE_NAME=myapp-{{ branch() }}` becomes `SERVICE_NAME=myapp-feature-auth`
204
+
205
+ 4. **Docker Compose Syntax (Preserved)**: `${VARIABLE}`
191
206
  - NOT processed by sprout - passed through as-is
192
207
  - Useful for Docker Compose variable substitution
193
208
  - Example: `${DB_NAME:-default}` remains unchanged in generated `.env`
@@ -13,7 +13,7 @@ A CLI tool to automate git worktree and Docker Compose development workflows.
13
13
  ## Installation
14
14
 
15
15
  ```bash
16
- pip install sprout
16
+ pip install sprout-cli
17
17
  ```
18
18
 
19
19
  For development:
@@ -28,7 +28,9 @@ pip install -e ".[dev]"
28
28
 
29
29
  ## Quick Start
30
30
 
31
- 1. Create a `.env.example` template in your project root (and optionally in subdirectories):
31
+ **Note**: Sprout works in any git repository. `.env.example` files are optional - if you don't have them, sprout will simply create worktrees without `.env` generation.
32
+
33
+ 1. (Optional) Create a `.env.example` template in your project root (and optionally in subdirectories) for automatic `.env` generation:
32
34
  ```env
33
35
  # API Configuration
34
36
  API_KEY={{ API_KEY }}
@@ -38,6 +40,10 @@ API_PORT={{ auto_port() }}
38
40
  DB_HOST=localhost
39
41
  DB_PORT={{ auto_port() }}
40
42
 
43
+ # Branch-specific Configuration
44
+ SERVICE_NAME=myapp-{{ branch() }}
45
+ DEPLOYMENT_ENV={{ branch() }}
46
+
41
47
  # Example: Docker Compose variables (preserved as-is)
42
48
  # sprout will NOT process ${...} syntax - it's passed through unchanged
43
49
  # DB_NAME=${DB_NAME}
@@ -58,9 +64,13 @@ repo/
58
64
  cd $(sprout create feature-branch --path)
59
65
  ```
60
66
 
67
+ **What happens when you run `sprout create`:**
68
+ - If `.env.example` files exist: Sprout will generate corresponding `.env` files with populated variables and unique port assignments
69
+ - If no `.env.example` files exist: Sprout will show a warning and create the worktree without `.env` generation
70
+
61
71
  This single command:
62
72
  - Creates a new git worktree for `feature-branch`
63
- - Generates a `.env` file from your template
73
+ - Generates `.env` files from your templates (if `.env.example` files exist)
64
74
  - Outputs the path to the new environment
65
75
  - Changes to that directory when wrapped in `cd $(...)`
66
76
 
@@ -146,7 +156,7 @@ Show the version of sprout.
146
156
 
147
157
  ## Template Syntax
148
158
 
149
- sprout supports two types of placeholders in `.env.example`:
159
+ sprout supports three types of placeholders in `.env.example`:
150
160
 
151
161
  1. **Variable Placeholders**: `{{ VARIABLE_NAME }}`
152
162
  - **First**: Checks if the variable exists in your environment (e.g., `export API_KEY=xxx`)
@@ -159,7 +169,12 @@ sprout supports two types of placeholders in `.env.example`:
159
169
  - Checks system port availability
160
170
  - Ensures global uniqueness even in monorepo setups
161
171
 
162
- 3. **Docker Compose Syntax (Preserved)**: `${VARIABLE}`
172
+ 3. **Branch Name**: `{{ branch() }}`
173
+ - Replaced with the current branch/subtree name
174
+ - Useful for branch-specific configurations
175
+ - Example: `SERVICE_NAME=myapp-{{ branch() }}` becomes `SERVICE_NAME=myapp-feature-auth`
176
+
177
+ 4. **Docker Compose Syntax (Preserved)**: `${VARIABLE}`
163
178
  - NOT processed by sprout - passed through as-is
164
179
  - Useful for Docker Compose variable substitution
165
180
  - Example: `${DB_NAME:-default}` remains unchanged in generated `.env`
@@ -1,3 +1,3 @@
1
1
  """sprout - CLI tool to automate git worktree and Docker Compose development workflows."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.6.0"
@@ -47,11 +47,9 @@ def create_worktree(branch_name: BranchName, path_only: bool = False) -> Never:
47
47
 
48
48
  if not env_examples:
49
49
  if not path_only:
50
- console.print("[red]Error: No .env.example files found[/red]")
51
- console.print(f"Expected at least one .env.example file in: {git_root}")
52
- else:
53
- typer.echo(f"Error: No .env.example files found in {git_root}", err=True)
54
- raise typer.Exit(1)
50
+ console.print("[yellow]Warning: No .env.example files found[/yellow]")
51
+ console.print(f"Proceeding without .env generation in: {git_root}")
52
+ # Continue execution without exiting
55
53
 
56
54
  # Check if worktree already exists
57
55
  if worktree_exists(branch_name):
@@ -87,58 +85,62 @@ def create_worktree(branch_name: BranchName, path_only: bool = False) -> Never:
87
85
  typer.echo(f"Error creating worktree: {e}", err=True)
88
86
  raise typer.Exit(1) from e
89
87
 
90
- # Generate .env files
91
- if not path_only:
92
- console.print(f"Generating .env files from {len(env_examples)} template(s)...")
93
-
94
- # Get all currently used ports to avoid conflicts
95
- all_used_ports = get_used_ports()
96
- session_ports: set[int] = set()
97
-
98
- try:
99
- for env_example in env_examples:
100
- # Calculate relative path from git root
101
- relative_dir = env_example.parent.relative_to(git_root)
102
-
103
- # Create target directory in worktree if needed
104
- if relative_dir != Path("."):
105
- target_dir = worktree_path / relative_dir
106
- target_dir.mkdir(parents=True, exist_ok=True)
107
- env_file = target_dir / ".env"
108
- else:
109
- env_file = worktree_path / ".env"
110
-
111
- # Parse template with combined used ports
112
- env_content = parse_env_template(
113
- env_example, silent=path_only, used_ports=all_used_ports | session_ports
114
- )
115
-
116
- # Extract ports from generated content and add to session_ports
117
- port_matches = re.findall(r"=(\d{4,5})\b", env_content)
118
- for port_str in port_matches:
119
- port = int(port_str)
120
- if 1024 <= port <= 65535:
121
- session_ports.add(port)
122
-
123
- # Write the .env file
124
- env_file.write_text(env_content)
125
-
126
- except SproutError as e:
127
- if not path_only:
128
- console.print(f"[red]Error generating .env file: {e}[/red]")
129
- else:
130
- typer.echo(f"Error generating .env file: {e}", err=True)
131
- # Clean up worktree on failure
132
- run_command(["git", "worktree", "remove", str(worktree_path)], check=False)
133
- raise typer.Exit(1) from e
134
- except KeyboardInterrupt:
88
+ # Generate .env files only if .env.example files exist
89
+ if env_examples:
135
90
  if not path_only:
136
- console.print("\n[yellow]Cancelled by user[/yellow]")
137
- else:
138
- typer.echo("Cancelled by user", err=True)
139
- # Clean up worktree on cancellation
140
- run_command(["git", "worktree", "remove", str(worktree_path)], check=False)
141
- raise typer.Exit(130) from None
91
+ console.print(f"Generating .env files from {len(env_examples)} template(s)...")
92
+
93
+ # Get all currently used ports to avoid conflicts
94
+ all_used_ports = get_used_ports()
95
+ session_ports: set[int] = set()
96
+
97
+ try:
98
+ for env_example in env_examples:
99
+ # Calculate relative path from git root
100
+ relative_dir = env_example.parent.relative_to(git_root)
101
+
102
+ # Create target directory in worktree if needed
103
+ if relative_dir != Path("."):
104
+ target_dir = worktree_path / relative_dir
105
+ target_dir.mkdir(parents=True, exist_ok=True)
106
+ env_file = target_dir / ".env"
107
+ else:
108
+ env_file = worktree_path / ".env"
109
+
110
+ # Parse template with combined used ports
111
+ env_content = parse_env_template(
112
+ env_example,
113
+ silent=path_only,
114
+ used_ports=all_used_ports | session_ports,
115
+ branch_name=branch_name,
116
+ )
117
+
118
+ # Extract ports from generated content and add to session_ports
119
+ port_matches = re.findall(r"=(\d{4,5})\b", env_content)
120
+ for port_str in port_matches:
121
+ port = int(port_str)
122
+ if 1024 <= port <= 65535:
123
+ session_ports.add(port)
124
+
125
+ # Write the .env file
126
+ env_file.write_text(env_content)
127
+
128
+ except SproutError as e:
129
+ if not path_only:
130
+ console.print(f"[red]Error generating .env file: {e}[/red]")
131
+ else:
132
+ typer.echo(f"Error generating .env file: {e}", err=True)
133
+ # Clean up worktree on failure
134
+ run_command(["git", "worktree", "remove", str(worktree_path)], check=False)
135
+ raise typer.Exit(1) from e
136
+ except KeyboardInterrupt:
137
+ if not path_only:
138
+ console.print("\n[yellow]Cancelled by user[/yellow]")
139
+ else:
140
+ typer.echo("Cancelled by user", err=True)
141
+ # Clean up worktree on cancellation
142
+ run_command(["git", "worktree", "remove", str(worktree_path)], check=False)
143
+ raise typer.Exit(130) from None
142
144
 
143
145
  # Success message or path output
144
146
  if path_only:
@@ -146,8 +148,17 @@ def create_worktree(branch_name: BranchName, path_only: bool = False) -> Never:
146
148
  print(str(worktree_path))
147
149
  else:
148
150
  console.print(f"\n[green]✅ Workspace '{branch_name}' created successfully![/green]\n")
151
+ if env_examples:
152
+ console.print(f"Generated .env files from {len(env_examples)} template(s)")
153
+ else:
154
+ console.print("No .env files generated (no .env.example templates found)")
149
155
  console.print("Navigate to your new environment with:")
150
- console.print(f" [cyan]cd {worktree_path.relative_to(Path.cwd())}[/cyan]")
156
+ try:
157
+ relative_path = worktree_path.relative_to(Path.cwd())
158
+ console.print(f" [cyan]cd {relative_path}[/cyan]")
159
+ except ValueError:
160
+ # If worktree_path is not relative to current directory, show absolute path
161
+ console.print(f" [cyan]cd {worktree_path}[/cyan]")
151
162
 
152
163
  # Exit successfully
153
164
  raise typer.Exit(0)
@@ -126,7 +126,10 @@ def find_available_port() -> PortNumber:
126
126
 
127
127
 
128
128
  def parse_env_template(
129
- template_path: Path, silent: bool = False, used_ports: PortSet | None = None
129
+ template_path: Path,
130
+ silent: bool = False,
131
+ used_ports: PortSet | None = None,
132
+ branch_name: str | None = None,
130
133
  ) -> str:
131
134
  """Parse .env.example template and process placeholders.
132
135
 
@@ -134,6 +137,7 @@ def parse_env_template(
134
137
  template_path: Path to the .env.example template file
135
138
  silent: If True, use stderr for prompts to keep stdout clean
136
139
  used_ports: Set of ports already in use (in addition to system-wide used ports)
140
+ branch_name: Branch name to use for {{ branch() }} placeholders
137
141
  """
138
142
  if not template_path.exists():
139
143
  raise SproutError(f".env.example file not found at {template_path}")
@@ -161,6 +165,10 @@ def parse_env_template(
161
165
 
162
166
  line = re.sub(r"{{\s*auto_port\(\)\s*}}", replace_auto_port, line)
163
167
 
168
+ # Process {{ branch() }} placeholders
169
+ if branch_name:
170
+ line = re.sub(r"{{\s*branch\(\)\s*}}", branch_name, line)
171
+
164
172
  # Process {{ VARIABLE }} placeholders
165
173
  def replace_variable(match: re.Match[str]) -> str:
166
174
  var_name = match.group(1).strip()
@@ -187,7 +195,8 @@ def parse_env_template(
187
195
  value = console.input(prompt)
188
196
  return value
189
197
 
190
- line = re.sub(r"{{\s*([^}]+)\s*}}", replace_variable, line)
198
+ # Only match variables that don't look like function calls (no parentheses)
199
+ line = re.sub(r"{{\s*([^}()]+)\s*}}", replace_variable, line)
191
200
 
192
201
  lines.append(line)
193
202
 
@@ -126,20 +126,37 @@ class TestCreateCommand:
126
126
  # stdout should be empty
127
127
  assert result.stdout == ""
128
128
 
129
- def test_create_no_env_example(self, mocker):
130
- """Test error when .env.example doesn't exist."""
129
+ def test_create_no_env_example(self, mocker, tmp_path):
130
+ """Test success when .env.example doesn't exist."""
131
131
  mocker.patch("sprout.commands.create.is_git_repository", return_value=True)
132
- mock_git_root = Path("/project")
132
+ mock_git_root = tmp_path / "project"
133
+ mock_git_root.mkdir()
133
134
  mocker.patch("sprout.commands.create.get_git_root", return_value=mock_git_root)
135
+ mocker.patch("sprout.commands.create.worktree_exists", return_value=False)
136
+ sprout_dir = tmp_path / ".sprout"
137
+ sprout_dir.mkdir()
138
+ mocker.patch("sprout.commands.create.ensure_sprout_dir", return_value=sprout_dir)
139
+ mocker.patch("sprout.commands.create.branch_exists", return_value=False)
134
140
 
135
- # Mock git ls-files to return empty list
141
+ # Mock git ls-files to return empty list (no .env.example files)
136
142
  mock_run = mocker.patch("sprout.commands.create.run_command")
137
143
  mock_run.return_value = Mock(stdout="", returncode=0)
138
144
 
139
- result = runner.invoke(app, ["create", "feature-branch"])
145
+ # Change to project directory to make relative path calculation work
146
+ import os
140
147
 
141
- assert result.exit_code == 1
142
- assert "No .env.example files found" in result.stdout
148
+ old_cwd = os.getcwd()
149
+ os.chdir(str(mock_git_root))
150
+
151
+ try:
152
+ result = runner.invoke(app, ["create", "feature-branch"])
153
+
154
+ assert result.exit_code == 0
155
+ assert "Warning: No .env.example files found" in result.stdout
156
+ assert "Workspace 'feature-branch' created successfully!" in result.stdout
157
+ assert "No .env files generated (no .env.example templates found)" in result.stdout
158
+ finally:
159
+ os.chdir(old_cwd)
143
160
 
144
161
  def test_create_worktree_exists(self, mocker):
145
162
  """Test error when worktree already exists."""
@@ -161,6 +178,28 @@ class TestCreateCommand:
161
178
  assert result.exit_code == 1
162
179
  assert "Worktree for branch 'feature-branch' already exists" in result.stdout
163
180
 
181
+ def test_create_without_env_example_path_mode(self, mocker, tmp_path):
182
+ """Test path mode with no .env.example files."""
183
+ mocker.patch("sprout.commands.create.is_git_repository", return_value=True)
184
+ mock_git_root = tmp_path / "project"
185
+ mock_git_root.mkdir()
186
+ mocker.patch("sprout.commands.create.get_git_root", return_value=mock_git_root)
187
+ mocker.patch("sprout.commands.create.worktree_exists", return_value=False)
188
+ sprout_dir = tmp_path / ".sprout"
189
+ sprout_dir.mkdir()
190
+ mocker.patch("sprout.commands.create.ensure_sprout_dir", return_value=sprout_dir)
191
+ mocker.patch("sprout.commands.create.branch_exists", return_value=False)
192
+
193
+ # Mock git ls-files to return empty list (no .env.example files)
194
+ mock_run = mocker.patch("sprout.commands.create.run_command")
195
+ mock_run.return_value = Mock(stdout="", returncode=0)
196
+
197
+ result = runner.invoke(app, ["create", "feature-branch", "--path"])
198
+
199
+ assert result.exit_code == 0
200
+ # In path mode, only the path should be printed to stdout
201
+ assert result.stdout.strip() == str(sprout_dir / "feature-branch")
202
+
164
203
 
165
204
  class TestLsCommand:
166
205
  """Test sprout ls command."""
@@ -198,6 +198,47 @@ class TestIntegrationWorkflow:
198
198
  worktree_path = git_repo / ".sprout" / "existing-branch"
199
199
  assert worktree_path.exists()
200
200
 
201
+ def test_branch_placeholder(self, git_repo, monkeypatch):
202
+ """Test that {{ branch() }} placeholder is replaced with branch name."""
203
+ git_repo, default_branch = git_repo
204
+ monkeypatch.chdir(git_repo)
205
+ monkeypatch.setenv("API_KEY", "test_key")
206
+
207
+ # Create .env.example with branch() placeholder
208
+ env_example = git_repo / ".env.example"
209
+ env_example.write_text(
210
+ "# Branch-specific configuration\n"
211
+ "BRANCH_NAME={{ branch() }}\n"
212
+ "SERVICE_NAME=myapp-{{ branch() }}\n"
213
+ "API_KEY={{ API_KEY }}\n"
214
+ "PORT={{ auto_port() }}\n"
215
+ "COMPOSE_VAR=${BRANCH_NAME}\n"
216
+ )
217
+
218
+ # Add to git
219
+ subprocess.run(["git", "add", ".env.example"], cwd=git_repo, check=True)
220
+ subprocess.run(
221
+ ["git", "commit", "-m", "Add .env.example with branch placeholder"],
222
+ cwd=git_repo,
223
+ check=True,
224
+ )
225
+
226
+ # Create worktree
227
+ branch_name = "feature-auth"
228
+ result = runner.invoke(app, ["create", branch_name])
229
+ assert result.exit_code == 0
230
+
231
+ # Verify .env was created with correct branch name substitution
232
+ env_file = git_repo / ".sprout" / branch_name / ".env"
233
+ env_content = env_file.read_text()
234
+
235
+ # Check branch name substitutions
236
+ assert f"BRANCH_NAME={branch_name}" in env_content
237
+ assert f"SERVICE_NAME=myapp-{branch_name}" in env_content
238
+ assert "API_KEY=test_key" in env_content
239
+ assert "PORT=" in env_content # Should have a port number
240
+ assert "COMPOSE_VAR=${BRANCH_NAME}" in env_content # Docker syntax preserved
241
+
201
242
  def test_error_cases(self, git_repo, monkeypatch, tmp_path):
202
243
  """Test various error conditions."""
203
244
  git_repo, default_branch = git_repo
@@ -223,11 +264,12 @@ class TestIntegrationWorkflow:
223
264
  result = runner.invoke(app, ["path", "nonexistent"])
224
265
  assert result.exit_code == 1
225
266
 
226
- # Remove .env.example and try to create
267
+ # Remove .env.example and try to create (should succeed now)
227
268
  (git_repo / ".env.example").unlink()
228
269
  result = runner.invoke(app, ["create", "another-branch"])
229
- assert result.exit_code == 1
230
- assert "No .env.example files found" in result.stdout
270
+ assert result.exit_code == 0
271
+ assert "Warning: No .env.example files found" in result.stdout
272
+ assert "Workspace 'another-branch' created successfully!" in result.stdout
231
273
 
232
274
  # Test outside git repo using a separate temp directory
233
275
  import tempfile
@@ -179,6 +179,47 @@ class TestEnvTemplateParser:
179
179
  result = parse_env_template(template)
180
180
  assert result == "COMPOSE_VAR=${COMPOSE_VAR:-default}"
181
181
 
182
+ def test_parse_env_template_branch_placeholder(self, tmp_path):
183
+ """Test parsing {{ branch() }} placeholders."""
184
+ template = tmp_path / ".env.example"
185
+ template.write_text("BRANCH_NAME={{ branch() }}\nFEATURE={{ branch() }}_feature")
186
+
187
+ result = parse_env_template(template, branch_name="feature-auth")
188
+ assert result == "BRANCH_NAME=feature-auth\nFEATURE=feature-auth_feature"
189
+
190
+ def test_parse_env_template_branch_placeholder_none(self, tmp_path):
191
+ """Test {{ branch() }} placeholder when branch_name is None."""
192
+ template = tmp_path / ".env.example"
193
+ template.write_text("BRANCH={{ branch() }}")
194
+
195
+ # When branch_name is None, placeholder should remain unchanged
196
+ result = parse_env_template(template, branch_name=None)
197
+ assert result == "BRANCH={{ branch() }}"
198
+
199
+ def test_parse_env_template_mixed_placeholders(self, tmp_path, mocker):
200
+ """Test parsing mixed placeholders in one template."""
201
+ mocker.patch("sprout.utils.find_available_port", return_value=8080)
202
+ mocker.patch.dict(os.environ, {"API_KEY": "secret"})
203
+ mocker.patch("sprout.utils.console.input", return_value="password123")
204
+
205
+ template = tmp_path / ".env.example"
206
+ template.write_text(
207
+ "API_KEY={{ API_KEY }}\n"
208
+ "PORT={{ auto_port() }}\n"
209
+ "BRANCH={{ branch() }}\n"
210
+ "DB_PASS={{ DB_PASS }}\n"
211
+ "COMPOSE_VAR=${COMPOSE_VAR}"
212
+ )
213
+
214
+ result = parse_env_template(template, branch_name="feature-xyz")
215
+ assert result == (
216
+ "API_KEY=secret\n"
217
+ "PORT=8080\n"
218
+ "BRANCH=feature-xyz\n"
219
+ "DB_PASS=password123\n"
220
+ "COMPOSE_VAR=${COMPOSE_VAR}"
221
+ )
222
+
182
223
  def test_parse_env_template_file_not_found(self, tmp_path):
183
224
  """Test error when template file doesn't exist."""
184
225
  template = tmp_path / "nonexistent.env"
File without changes
File without changes
File without changes
File without changes
File without changes