sprout-cli 0.5.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.
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/PKG-INFO +12 -3
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/README.md +11 -2
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/src/sprout/__init__.py +1 -1
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/src/sprout/commands/create.py +4 -1
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/src/sprout/utils.py +11 -2
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/tests/test_integration.py +41 -0
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/tests/test_utils.py +41 -0
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/.gitignore +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/LICENSE +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/pyproject.toml +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/src/sprout/__main__.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/src/sprout/cli.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/src/sprout/commands/__init__.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/src/sprout/commands/ls.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/src/sprout/commands/path.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/src/sprout/commands/rm.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/src/sprout/exceptions.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/src/sprout/types.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/tests/__init__.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/tests/test_commands.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.6.0}/tests/test_multi_env.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sprout-cli
|
|
3
|
-
Version: 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
|
|
@@ -68,6 +68,10 @@ API_PORT={{ auto_port() }}
|
|
|
68
68
|
DB_HOST=localhost
|
|
69
69
|
DB_PORT={{ auto_port() }}
|
|
70
70
|
|
|
71
|
+
# Branch-specific Configuration
|
|
72
|
+
SERVICE_NAME=myapp-{{ branch() }}
|
|
73
|
+
DEPLOYMENT_ENV={{ branch() }}
|
|
74
|
+
|
|
71
75
|
# Example: Docker Compose variables (preserved as-is)
|
|
72
76
|
# sprout will NOT process ${...} syntax - it's passed through unchanged
|
|
73
77
|
# DB_NAME=${DB_NAME}
|
|
@@ -180,7 +184,7 @@ Show the version of sprout.
|
|
|
180
184
|
|
|
181
185
|
## Template Syntax
|
|
182
186
|
|
|
183
|
-
sprout supports
|
|
187
|
+
sprout supports three types of placeholders in `.env.example`:
|
|
184
188
|
|
|
185
189
|
1. **Variable Placeholders**: `{{ VARIABLE_NAME }}`
|
|
186
190
|
- **First**: Checks if the variable exists in your environment (e.g., `export API_KEY=xxx`)
|
|
@@ -193,7 +197,12 @@ sprout supports two types of placeholders in `.env.example`:
|
|
|
193
197
|
- Checks system port availability
|
|
194
198
|
- Ensures global uniqueness even in monorepo setups
|
|
195
199
|
|
|
196
|
-
3. **
|
|
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}`
|
|
197
206
|
- NOT processed by sprout - passed through as-is
|
|
198
207
|
- Useful for Docker Compose variable substitution
|
|
199
208
|
- Example: `${DB_NAME:-default}` remains unchanged in generated `.env`
|
|
@@ -40,6 +40,10 @@ API_PORT={{ auto_port() }}
|
|
|
40
40
|
DB_HOST=localhost
|
|
41
41
|
DB_PORT={{ auto_port() }}
|
|
42
42
|
|
|
43
|
+
# Branch-specific Configuration
|
|
44
|
+
SERVICE_NAME=myapp-{{ branch() }}
|
|
45
|
+
DEPLOYMENT_ENV={{ branch() }}
|
|
46
|
+
|
|
43
47
|
# Example: Docker Compose variables (preserved as-is)
|
|
44
48
|
# sprout will NOT process ${...} syntax - it's passed through unchanged
|
|
45
49
|
# DB_NAME=${DB_NAME}
|
|
@@ -152,7 +156,7 @@ Show the version of sprout.
|
|
|
152
156
|
|
|
153
157
|
## Template Syntax
|
|
154
158
|
|
|
155
|
-
sprout supports
|
|
159
|
+
sprout supports three types of placeholders in `.env.example`:
|
|
156
160
|
|
|
157
161
|
1. **Variable Placeholders**: `{{ VARIABLE_NAME }}`
|
|
158
162
|
- **First**: Checks if the variable exists in your environment (e.g., `export API_KEY=xxx`)
|
|
@@ -165,7 +169,12 @@ sprout supports two types of placeholders in `.env.example`:
|
|
|
165
169
|
- Checks system port availability
|
|
166
170
|
- Ensures global uniqueness even in monorepo setups
|
|
167
171
|
|
|
168
|
-
3. **
|
|
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}`
|
|
169
178
|
- NOT processed by sprout - passed through as-is
|
|
170
179
|
- Useful for Docker Compose variable substitution
|
|
171
180
|
- Example: `${DB_NAME:-default}` remains unchanged in generated `.env`
|
|
@@ -109,7 +109,10 @@ def create_worktree(branch_name: BranchName, path_only: bool = False) -> Never:
|
|
|
109
109
|
|
|
110
110
|
# Parse template with combined used ports
|
|
111
111
|
env_content = parse_env_template(
|
|
112
|
-
env_example,
|
|
112
|
+
env_example,
|
|
113
|
+
silent=path_only,
|
|
114
|
+
used_ports=all_used_ports | session_ports,
|
|
115
|
+
branch_name=branch_name,
|
|
113
116
|
)
|
|
114
117
|
|
|
115
118
|
# Extract ports from generated content and add to session_ports
|
|
@@ -126,7 +126,10 @@ def find_available_port() -> PortNumber:
|
|
|
126
126
|
|
|
127
127
|
|
|
128
128
|
def parse_env_template(
|
|
129
|
-
template_path: Path,
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|