mirror of
https://github.com/ChristianLempa/boilerplates.git
synced 2026-08-24 10:04:44 -05:00
updates
This commit is contained in:
+15
-3
@@ -7,18 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.0] - 2025-10-14
|
||||
|
||||
### Added
|
||||
- Support for required variables independent of section state (#1355)
|
||||
- Variables can now be marked with `required: true` in template specs
|
||||
- Required variables are always prompted, validated, and included in rendering
|
||||
- Display shows yellow `(required)` indicator for required variables
|
||||
- Required variables from disabled sections are still collected and available
|
||||
|
||||
### Changed
|
||||
- Improved error handling and display output consistency
|
||||
- Updated dependency PyYAML to v6.0.3 (Python 3.14 compatibility)
|
||||
- Updated dependency rich to v14.2.0 (Python 3.14 compatibility)
|
||||
|
||||
### Fixed
|
||||
- Repository fetch fails when library directory already exists
|
||||
- Install script reliability improvements
|
||||
- Absolute paths without leading slash treated as relative paths in generate command (#1357)
|
||||
- Paths like `Users/xcad/Projects/test` are now correctly normalized to `/Users/xcad/Projects/test`
|
||||
- Supports common Unix/Linux root directories: Users/, home/, usr/, opt/, var/, tmp/
|
||||
- Repository fetch fails when library directory already exists (#1279)
|
||||
|
||||
## [0.0.4] - 2025-01-XX
|
||||
|
||||
Initial public release with core CLI functionality.
|
||||
|
||||
[unreleased]: https://github.com/christianlempa/boilerplates/compare/v0.0.4...HEAD
|
||||
[unreleased]: https://github.com/christianlempa/boilerplates/compare/v0.1.0...HEAD
|
||||
[0.1.0]: https://github.com/christianlempa/boilerplates/compare/v0.0.4...v0.1.0
|
||||
[0.0.4]: https://github.com/christianlempa/boilerplates/releases/tag/v0.0.4
|
||||
|
||||
@@ -91,6 +91,11 @@ def main(
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj['log_level'] = log_level
|
||||
|
||||
# Check for local config.yaml and show indicator
|
||||
local_config = Path.cwd() / "config.yaml"
|
||||
if local_config.exists() and local_config.is_file():
|
||||
console.print(f"[dim]→ Using local config: config.yaml[/dim]")
|
||||
|
||||
# If no subcommand is provided, show help and friendly intro
|
||||
if ctx.invoked_subcommand is None:
|
||||
console.print(ctx.get_help())
|
||||
|
||||
+36
-14
@@ -303,8 +303,9 @@ class VariableCollection:
|
||||
"""Get variable values only from sections with satisfied dependencies.
|
||||
|
||||
This respects both toggle states and section dependencies, ensuring that:
|
||||
- Variables from disabled sections (toggle=false) are excluded
|
||||
- Variables from disabled sections (toggle=false) are excluded EXCEPT required variables
|
||||
- Variables from sections with unsatisfied dependencies are excluded
|
||||
- Required variables are always included if their section dependencies are satisfied
|
||||
|
||||
Returns:
|
||||
Dictionary of variable names to values for satisfied sections only
|
||||
@@ -312,19 +313,25 @@ class VariableCollection:
|
||||
satisfied_values = {}
|
||||
|
||||
for section_key, section in self._sections.items():
|
||||
# Skip sections with unsatisfied dependencies
|
||||
# Skip sections with unsatisfied dependencies (even required variables need satisfied deps)
|
||||
if not self.is_section_satisfied(section_key):
|
||||
logger.debug(f"Excluding variables from section '{section_key}' - dependencies not satisfied")
|
||||
continue
|
||||
|
||||
# Skip disabled sections (toggle check)
|
||||
if not section.is_enabled():
|
||||
logger.debug(f"Excluding variables from section '{section_key}' - section is disabled")
|
||||
continue
|
||||
# Check if section is enabled
|
||||
is_enabled = section.is_enabled()
|
||||
|
||||
# Include all variables from this satisfied section
|
||||
for var_name, variable in section.variables.items():
|
||||
satisfied_values[var_name] = variable.convert(variable.value)
|
||||
if is_enabled:
|
||||
# Include all variables from enabled section
|
||||
for var_name, variable in section.variables.items():
|
||||
satisfied_values[var_name] = variable.convert(variable.value)
|
||||
else:
|
||||
# Section is disabled - only include required variables
|
||||
logger.debug(f"Section '{section_key}' is disabled - including only required variables")
|
||||
for var_name, variable in section.variables.items():
|
||||
if variable.required:
|
||||
logger.debug(f"Including required variable '{var_name}' from disabled section '{section_key}'")
|
||||
satisfied_values[var_name] = variable.convert(variable.value)
|
||||
|
||||
return satisfied_values
|
||||
|
||||
@@ -381,17 +388,32 @@ class VariableCollection:
|
||||
return successful
|
||||
|
||||
def validate_all(self) -> None:
|
||||
"""Validate all variables in the collection, skipping disabled and unsatisfied sections."""
|
||||
"""Validate all variables in the collection.
|
||||
|
||||
Validates:
|
||||
- All variables in enabled sections with satisfied dependencies
|
||||
- Required variables even if their section is disabled (but dependencies must be satisfied)
|
||||
"""
|
||||
errors: list[str] = []
|
||||
|
||||
for section_key, section in self._sections.items():
|
||||
# Skip sections with unsatisfied dependencies or disabled via toggle
|
||||
if not self.is_section_satisfied(section_key) or not section.is_enabled():
|
||||
logger.debug(f"Skipping validation for section '{section_key}'")
|
||||
# Skip sections with unsatisfied dependencies (even for required variables)
|
||||
if not self.is_section_satisfied(section_key):
|
||||
logger.debug(f"Skipping validation for section '{section_key}' - dependencies not satisfied")
|
||||
continue
|
||||
|
||||
# Check if section is enabled
|
||||
is_enabled = section.is_enabled()
|
||||
|
||||
if not is_enabled:
|
||||
logger.debug(f"Section '{section_key}' is disabled - validating only required variables")
|
||||
|
||||
# Validate each variable in the section
|
||||
# Validate variables in the section
|
||||
for var_name, variable in section.variables.items():
|
||||
# Skip non-required variables in disabled sections
|
||||
if not is_enabled and not variable.required:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Skip autogenerated variables when empty
|
||||
if variable.autogenerated and not variable.value:
|
||||
|
||||
+31
-9
@@ -37,19 +37,33 @@ class ConfigManager:
|
||||
"""Initialize the configuration manager.
|
||||
|
||||
Args:
|
||||
config_path: Path to the configuration file. If None, uses default location.
|
||||
config_path: Path to the configuration file. If None, auto-detects:
|
||||
1. Checks for ./config.yaml (local project config)
|
||||
2. Falls back to ~/.config/boilerplates/config.yaml (global config)
|
||||
"""
|
||||
if config_path is None:
|
||||
# Default to ~/.config/boilerplates/config.yaml
|
||||
config_dir = Path.home() / ".config" / "boilerplates"
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.config_path = config_dir / "config.yaml"
|
||||
# Check for local config.yaml in current directory first
|
||||
local_config = Path.cwd() / "config.yaml"
|
||||
if local_config.exists() and local_config.is_file():
|
||||
self.config_path = local_config
|
||||
self.is_local = True
|
||||
logger.info(f"Using local config: {local_config}")
|
||||
else:
|
||||
# Fall back to global config
|
||||
config_dir = Path.home() / ".config" / "boilerplates"
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.config_path = config_dir / "config.yaml"
|
||||
self.is_local = False
|
||||
else:
|
||||
self.config_path = Path(config_path)
|
||||
self.is_local = False
|
||||
|
||||
# Create default config if it doesn't exist
|
||||
# Create default config if it doesn't exist (only for global config)
|
||||
if not self.config_path.exists():
|
||||
self._create_default_config()
|
||||
if not self.is_local:
|
||||
self._create_default_config()
|
||||
else:
|
||||
raise ConfigError(f"Local config file not found: {self.config_path}")
|
||||
else:
|
||||
# Migrate existing config if needed
|
||||
self._migrate_config_if_needed()
|
||||
@@ -411,12 +425,20 @@ class ConfigManager:
|
||||
raise ConfigValidationError(f"Library 'enabled' at index {i} must be a boolean")
|
||||
|
||||
def get_config_path(self) -> Path:
|
||||
"""Get the path to the configuration file.
|
||||
"""Get the path to the configuration file being used.
|
||||
|
||||
Returns:
|
||||
Path to the configuration file.
|
||||
Path to the configuration file (global or local).
|
||||
"""
|
||||
return self.config_path
|
||||
|
||||
def is_using_local_config(self) -> bool:
|
||||
"""Check if a local configuration file is being used.
|
||||
|
||||
Returns:
|
||||
True if using local config, False if using global config.
|
||||
"""
|
||||
return self.is_local
|
||||
|
||||
def get_defaults(self, module_name: str) -> Dict[str, Any]:
|
||||
"""Get default variable values for a module.
|
||||
|
||||
+3
-1
@@ -444,7 +444,9 @@ class DisplayManager:
|
||||
|
||||
# Add lock icon for sensitive variables
|
||||
sensitive_icon = f" {IconManager.lock()}" if variable.sensitive else ""
|
||||
var_display = f" {var_name}{sensitive_icon}"
|
||||
# Add required indicator for required variables
|
||||
required_indicator = " [yellow](required)[/yellow]" if variable.required else ""
|
||||
var_display = f" {var_name}{sensitive_icon}{required_indicator}"
|
||||
|
||||
variables_table.add_row(
|
||||
var_display,
|
||||
|
||||
+9
-1
@@ -560,7 +560,15 @@ class Module(ABC):
|
||||
logger.info(f"Successfully rendered template '{id}'")
|
||||
|
||||
# Determine output directory
|
||||
output_dir = Path(directory) if directory else Path(id)
|
||||
if directory:
|
||||
output_dir = Path(directory)
|
||||
# Check if path looks like an absolute path but is missing the leading slash
|
||||
# This handles cases like "Users/username/path" which should be "/Users/username/path"
|
||||
if not output_dir.is_absolute() and str(output_dir).startswith(("Users/", "home/", "usr/", "opt/", "var/", "tmp/")):
|
||||
output_dir = Path("/") / output_dir
|
||||
logger.debug(f"Normalized relative-looking absolute path to: {output_dir}")
|
||||
else:
|
||||
output_dir = Path(id)
|
||||
|
||||
# Check for conflicts and get confirmation (skip in quiet mode)
|
||||
if not quiet:
|
||||
|
||||
+15
-2
@@ -34,6 +34,7 @@ class PromptHandler:
|
||||
return {}
|
||||
|
||||
collected: Dict[str, Any] = {}
|
||||
prompted_variables: set[str] = set() # Track which variables we've already prompted for
|
||||
|
||||
# Process each section
|
||||
for section_key, section in variables.get_sections().items():
|
||||
@@ -60,6 +61,9 @@ class PromptHandler:
|
||||
# Always show section header first
|
||||
self.display.display_section_header(section.title, section.description)
|
||||
|
||||
# Track whether this section will be enabled
|
||||
section_will_be_enabled = True
|
||||
|
||||
# Handle section toggle - skip for required sections
|
||||
if section.required:
|
||||
# Required sections are always processed, no toggle prompt needed
|
||||
@@ -78,18 +82,27 @@ class PromptHandler:
|
||||
|
||||
# Use section's native is_enabled() method
|
||||
if not section.is_enabled():
|
||||
continue
|
||||
section_will_be_enabled = False
|
||||
|
||||
# Collect variables in this section
|
||||
for var_name, variable in section.variables.items():
|
||||
# Skip toggle variable (already handled)
|
||||
if section.toggle and var_name == section.toggle:
|
||||
continue
|
||||
|
||||
|
||||
# Skip non-required variables if section is disabled
|
||||
if not section_will_be_enabled and not variable.required:
|
||||
logger.debug(f"Skipping non-required variable '{var_name}' from disabled section '{section_key}'")
|
||||
continue
|
||||
|
||||
# Prompt for the variable
|
||||
current_value = variable.convert(variable.value)
|
||||
# Pass section.required so _prompt_variable can enforce required inputs
|
||||
new_value = self._prompt_variable(variable, required=section.required)
|
||||
|
||||
# Track that we've prompted for this variable
|
||||
prompted_variables.add(var_name)
|
||||
|
||||
# For autogenerated variables, always update even if None (signals autogeneration)
|
||||
if variable.autogenerated and new_value is None:
|
||||
collected[var_name] = None
|
||||
|
||||
+16
-3
@@ -48,6 +48,8 @@ class Variable:
|
||||
self.extra: Optional[str] = data.get("extra")
|
||||
# Flag indicating this variable should be auto-generated when empty
|
||||
self.autogenerated: bool = data.get("autogenerated", False)
|
||||
# Flag indicating this variable is required even when section is disabled
|
||||
self.required: bool = data.get("required", False)
|
||||
# Original value before config override (used for display)
|
||||
self.original_value: Optional[Any] = data.get("original_value")
|
||||
|
||||
@@ -212,6 +214,8 @@ class Variable:
|
||||
result['sensitive'] = True
|
||||
if self.autogenerated:
|
||||
result['autogenerated'] = True
|
||||
if self.required:
|
||||
result['required'] = True
|
||||
if self.options is not None: # Allow empty list
|
||||
result['options'] = self.options
|
||||
|
||||
@@ -312,13 +316,21 @@ class Variable:
|
||||
"""Check if this variable requires a value (cannot be empty/None).
|
||||
|
||||
A variable is considered required if:
|
||||
- It doesn't have a default value (value is None)
|
||||
- It's not marked as autogenerated (which can be empty and generated later)
|
||||
- It's not a boolean type (booleans default to False if not set)
|
||||
- It has an explicit 'required: true' flag (highest precedence)
|
||||
- OR it doesn't have a default value (value is None)
|
||||
AND it's not marked as autogenerated (which can be empty and generated later)
|
||||
AND it's not a boolean type (booleans default to False if not set)
|
||||
|
||||
Returns:
|
||||
True if the variable must have a non-empty value, False otherwise
|
||||
"""
|
||||
# Explicit required flag takes highest precedence
|
||||
if self.required:
|
||||
# But autogenerated variables can still be empty (will be generated later)
|
||||
if self.autogenerated:
|
||||
return False
|
||||
return True
|
||||
|
||||
# Autogenerated variables can be empty (will be generated later)
|
||||
if self.autogenerated:
|
||||
return False
|
||||
@@ -359,6 +371,7 @@ class Variable:
|
||||
'sensitive': self.sensitive,
|
||||
'extra': self.extra,
|
||||
'autogenerated': self.autogenerated,
|
||||
'required': self.required,
|
||||
'original_value': self.original_value,
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
external_url '{{ external_url }}'
|
||||
|
||||
# GitLab Shell SSH settings
|
||||
gitlab_rails['gitlab_shell_ssh_port'] = {{ ssh_port }}
|
||||
gitlab_rails['gitlab_shell_ssh_port'] = {{ ports_ssh }}
|
||||
|
||||
# Let's Encrypt and built-in TLS settings are currently not supported by the template
|
||||
# as we are using Traefik as a reverse proxy
|
||||
|
||||
@@ -62,6 +62,7 @@ spec:
|
||||
type: int
|
||||
description: SSH port
|
||||
default: 2424
|
||||
required: true
|
||||
ports_registry:
|
||||
type: int
|
||||
description: Container Registry port
|
||||
|
||||
Reference in New Issue
Block a user