mirror of
https://github.com/ChristianLempa/boilerplates.git
synced 2026-08-24 10:04:44 -05:00
fix(cli): ship v0.2.0-1 hotfix
This commit is contained in:
@@ -16,6 +16,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Default library migration now rewrites the built-in repository from `christianlempa/boilerplates` to `christianlempa/boilerplates-library`, with startup notices and legacy `/library` path fallback (#1762)
|
||||
- Variable definitions now use the `secret` type and nested `config` metadata for options, placeholders, sliders, and secret autogeneration across the runtime, schemas, and migrated template specs (#1767)
|
||||
|
||||
## [0.2.0-1] - 2026-04-23
|
||||
|
||||
### Fixed
|
||||
- Interactive prompts now show and preserve effective defaults loaded from config, var-files, and CLI overrides.
|
||||
- `generate --dry-run` no longer prompts for local versus remote destinations when none are provided and now ends with a dry-run-specific success message.
|
||||
|
||||
## [0.1.2] - 2025-12-11
|
||||
|
||||
### Fixed
|
||||
|
||||
+1
-1
@@ -2,6 +2,6 @@
|
||||
Boilerplates CLI - A sophisticated command-line tool for managing infrastructure boilerplates.
|
||||
"""
|
||||
|
||||
__version__ = "0.2.0"
|
||||
__version__ = "0.2.0-1"
|
||||
__author__ = "Christian Lempa"
|
||||
__description__ = "CLI tool for managing infrastructure boilerplates"
|
||||
|
||||
@@ -120,7 +120,8 @@ class PromptHandler:
|
||||
prompt_text = variable.get_prompt_text()
|
||||
default_value = variable.get_normalized_default()
|
||||
has_explicit_default = "default" in variable._explicit_fields or "value" in variable._explicit_fields
|
||||
if not has_explicit_default and not variable.autogenerated and not variable.is_required():
|
||||
has_applied_default = variable.origin in {"config", "var-file", "cli"}
|
||||
if not has_explicit_default and not has_applied_default and not variable.autogenerated and not variable.is_required():
|
||||
default_value = None
|
||||
|
||||
# Add lock icon before default value for secret or autogenerated variables
|
||||
|
||||
@@ -419,6 +419,7 @@ def generate_template(module_instance, config: GenerationConfig) -> None: # noq
|
||||
display = DisplayManager(quiet=config.quiet) if config.quiet else module_instance.display
|
||||
template = _prepare_template(module_instance, config.id, config.var_file, config.var, display)
|
||||
slug = getattr(template, "slug", template.id)
|
||||
used_implicit_dry_run_destination = False
|
||||
|
||||
try:
|
||||
destination = resolve_cli_destination(config.output, config.remote, config.remote_path, slug)
|
||||
@@ -439,7 +440,10 @@ def generate_template(module_instance, config: GenerationConfig) -> None: # noq
|
||||
rendered_files, _variable_values = _render_template(template, config.id, display, config.interactive)
|
||||
|
||||
if destination is None:
|
||||
if config.interactive:
|
||||
if config.dry_run:
|
||||
destination = GenerationDestination(mode="local", local_output_dir=Path.cwd() / slug)
|
||||
used_implicit_dry_run_destination = True
|
||||
elif config.interactive:
|
||||
destination = prompt_generation_destination(slug)
|
||||
else:
|
||||
destination = GenerationDestination(mode="local", local_output_dir=Path.cwd() / slug)
|
||||
@@ -447,7 +451,8 @@ def generate_template(module_instance, config: GenerationConfig) -> None: # noq
|
||||
if not destination.is_remote:
|
||||
output_dir = destination.local_output_dir or (Path.cwd() / slug)
|
||||
if (
|
||||
not config.quiet
|
||||
not config.dry_run
|
||||
and not config.quiet
|
||||
and check_output_directory(output_dir, rendered_files, config.interactive, display) is None
|
||||
):
|
||||
return
|
||||
@@ -494,7 +499,11 @@ def generate_template(module_instance, config: GenerationConfig) -> None: # noq
|
||||
display.success(f"Boilerplate uploaded successfully to '{remote_target}'")
|
||||
elif config.dry_run and dry_run_stats:
|
||||
total_files, overwrite_files, size_str = dry_run_stats
|
||||
if overwrite_files > 0:
|
||||
if used_implicit_dry_run_destination:
|
||||
display.success(
|
||||
f"Dry run complete: boilerplate rendered successfully ({total_files} files, {size_str}, preview only)"
|
||||
)
|
||||
elif overwrite_files > 0:
|
||||
display.warning(
|
||||
f"Dry run complete: {total_files} files ({size_str}) would be written to '{output_dir}' "
|
||||
f"({overwrite_files} would be overwritten)"
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "boilerplates"
|
||||
version = "0.2.0"
|
||||
version = "0.2.0-1"
|
||||
description = "CLI tool for managing infrastructure boilerplates"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
|
||||
@@ -4,17 +4,34 @@ from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from cli.core.module.base_commands import list_templates
|
||||
from cli.core.module.base_commands import GenerationConfig, generate_template, list_templates
|
||||
|
||||
|
||||
class _DisplayCapture:
|
||||
def __init__(self) -> None:
|
||||
self.lines: list[str] = []
|
||||
self.templates = SimpleNamespace(
|
||||
render_template_header=lambda *args, **kwargs: None,
|
||||
render_file_tree=lambda *args, **kwargs: None,
|
||||
)
|
||||
self.variables = SimpleNamespace(render_variables_table=lambda *args, **kwargs: None)
|
||||
|
||||
def text(self, value: str, style: str | None = None) -> None:
|
||||
del style
|
||||
self.lines.append(value)
|
||||
|
||||
def success(self, value: str, *args, **kwargs) -> None:
|
||||
del args, kwargs
|
||||
self.lines.append(value)
|
||||
|
||||
def warning(self, value: str, *args, **kwargs) -> None:
|
||||
del args, kwargs
|
||||
self.lines.append(value)
|
||||
|
||||
def error(self, value: str, *args, **kwargs) -> None:
|
||||
del args, kwargs
|
||||
self.lines.append(value)
|
||||
|
||||
def data_table(self, *args, **kwargs) -> None:
|
||||
del args, kwargs
|
||||
raise AssertionError("data_table should not be used for raw output")
|
||||
@@ -47,3 +64,38 @@ def test_list_templates_raw_outputs_tab_separated_rows() -> None:
|
||||
|
||||
assert returned_templates == [template]
|
||||
assert display.lines == ["whoami\tWhoami\tdocker,test\t1.0.0\tdefault"]
|
||||
|
||||
|
||||
def test_generate_template_dry_run_skips_destination_prompt_and_overwrite_check(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
"""Dry runs without explicit destinations should not ask where to write or confirm overwrites."""
|
||||
display = _DisplayCapture()
|
||||
template = SimpleNamespace(id="whoami", slug="whoami")
|
||||
module_instance = SimpleNamespace(name="compose", display=display)
|
||||
|
||||
monkeypatch.setattr("cli.core.module.base_commands._prepare_template", lambda *args, **kwargs: template)
|
||||
monkeypatch.setattr(
|
||||
"cli.core.module.base_commands._render_template",
|
||||
lambda *args, **kwargs: ({"compose.yaml": "services:\n"}, {}),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"cli.core.module.base_commands.prompt_generation_destination",
|
||||
lambda slug: (_ for _ in ()).throw(AssertionError(f"prompt_generation_destination called for {slug}")),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"cli.core.module.base_commands.check_output_directory",
|
||||
lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("check_output_directory should not run")),
|
||||
)
|
||||
|
||||
generate_template(
|
||||
module_instance,
|
||||
GenerationConfig(
|
||||
id="whoami",
|
||||
interactive=True,
|
||||
dry_run=True,
|
||||
),
|
||||
)
|
||||
|
||||
assert any("boilerplate rendered successfully" in line for line in display.lines)
|
||||
assert any("preview only" in line for line in display.lines)
|
||||
|
||||
@@ -66,3 +66,63 @@ def test_required_integer_without_default_still_uses_integer_prompt(monkeypatch:
|
||||
|
||||
assert prompt_handler._prompt_variable(variable) == TEST_REQUIRED_INTEGER
|
||||
assert calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize("origin", ["config", "var-file", "cli"])
|
||||
def test_applied_optional_string_defaults_are_shown_and_preserved(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
origin: str,
|
||||
) -> None:
|
||||
"""Applied defaults from external sources should be visible in prompts and kept on Enter."""
|
||||
prompts: list[dict[str, Any]] = []
|
||||
|
||||
def fake_ask(prompt_text: str, default: str = "", show_default: bool = False, **kwargs: Any) -> str:
|
||||
prompts.append(
|
||||
{
|
||||
"prompt_text": prompt_text,
|
||||
"default": default,
|
||||
"show_default": show_default,
|
||||
**kwargs,
|
||||
}
|
||||
)
|
||||
return default
|
||||
|
||||
monkeypatch.setattr("cli.core.input.prompt_manager.Prompt.ask", fake_ask)
|
||||
|
||||
variable = Variable({"name": "service_name", "type": "str"})
|
||||
variable.value = "grafana"
|
||||
variable.origin = origin
|
||||
|
||||
prompt_handler = PromptHandler()
|
||||
raw_value = prompt_handler._prompt_variable(variable)
|
||||
|
||||
assert raw_value == "grafana"
|
||||
assert prompts
|
||||
assert prompts[0]["default"] == "grafana"
|
||||
assert prompts[0]["show_default"] is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("origin", ["config", "var-file", "cli"])
|
||||
def test_applied_optional_bool_defaults_are_shown_and_preserved(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
origin: str,
|
||||
) -> None:
|
||||
"""Applied boolean defaults should use confirm prompts with the current value as default."""
|
||||
calls: list[tuple[str, Any]] = []
|
||||
|
||||
def fake_confirm(self, prompt: str, default: bool | None = None) -> bool:
|
||||
del self
|
||||
calls.append((prompt, default))
|
||||
assert default is True
|
||||
return default
|
||||
|
||||
monkeypatch.setattr("cli.core.input.prompt_manager.InputManager.confirm", fake_confirm)
|
||||
|
||||
variable = Variable({"name": "traefik_enabled", "type": "bool"})
|
||||
variable.value = True
|
||||
variable.origin = origin
|
||||
|
||||
prompt_handler = PromptHandler()
|
||||
|
||||
assert prompt_handler._prompt_variable(variable) is True
|
||||
assert calls
|
||||
|
||||
Reference in New Issue
Block a user