diff --git a/docs/how_to/config_deployment.md b/docs/how_to/config_deployment.md
index 00a7871e..549a848c 100644
--- a/docs/how_to/config_deployment.md
+++ b/docs/how_to/config_deployment.md
@@ -22,7 +22,7 @@ from fabric_cicd import deploy_with_config
# Deploy using a config file
deploy_with_config(
- config_file_path="C:/dev/workspace/config.yml",
+ config_file_path="C:/dev/workspace/config.yml", # required
environment="dev"
)
```
@@ -96,22 +96,22 @@ core:
Required Fields:
-- Workspace Identifier:
- - Workspace ID takes precedence over workspace name when both are provided.
- - `workspace_id` must be a valid string GUID.
-- Repository Directory Path:
- - Supports relative or absolute path.
- - Relative path must be relative to the `config.yml` file location.
+- Workspace Identifier:
+ - Workspace ID takes precedence over workspace name when both are provided.
+ - `workspace_id` must be a valid string GUID.
+- Repository Directory Path:
+ - Supports relative or absolute path.
+ - Relative path must be relative to the `config.yml` file location.
Optional Fields:
-- Item Types in Scope:
- - If `item_types_in_scope` is not specified, all item types will be included by default.
- - Item types must be provided as a list, use `-` or `[]` notation.
- - Only accepts supported item types.
-- Parameter Path:
- - Supports relative or absolute path.
- - Relative path must be relative to the `config.yml` file location.
+- Item Types in Scope:
+ - If `item_types_in_scope` is not specified, all item types will be included by default.
+ - Item types must be provided as a list, use `-` or `[]` notation.
+ - Only accepts supported item types.
+- Parameter Path:
+ - Supports relative or absolute path.
+ - Relative path must be relative to the `config.yml` file location.
### Publish Settings
@@ -239,7 +239,89 @@ constants:
:
```
-### Sample `config.yml` File
+## Environment-Specific Values
+
+All configuration fields support environment-specific values using a mapping format:
+
+```yaml
+core:
+ workspace_id:
+ dev: "dev-workspace-id"
+ test: "test-workspace-id"
+ prod: "prod-workspace-id"
+```
+
+### Required vs Optional Fields
+
+Fields are categorized as **required** or **optional**, which affects how missing environment values are handled when environment is passed into `deploy_with_config()`:
+
+| Field | Required | Environment Missing Behavior |
+| --------------------------------------- | -------- | ------------------------------- |
+| `core.workspace_id` or `core.workspace` | ✅ | Validation error |
+| `core.repository_directory` | ✅ | Validation error |
+| `core.item_types_in_scope` | ❌ | Warning logged, setting skipped |
+| `core.parameter` | ❌ | Warning logged, setting skipped |
+| `publish.exclude_regex` | ❌ | Debug logged, setting skipped |
+| `publish.folder_exclude_regex` | ❌ | Debug logged, setting skipped |
+| `publish.shortcut_exclude_regex` | ❌ | Debug logged, setting skipped |
+| `publish.items_to_include` | ❌ | Debug logged, setting skipped |
+| `publish.skip` | ❌ | Defaults to `False` |
+| `unpublish.exclude_regex` | ❌ | Debug logged, setting skipped |
+| `unpublish.items_to_include` | ❌ | Debug logged, setting skipped |
+| `unpublish.skip` | ❌ | Defaults to `False` |
+| `features` | ❌ | Debug logged, setting skipped |
+| `constants` | ❌ | Debug logged, setting skipped |
+
+### Selective Environment Configuration
+
+Optional fields allow you to apply settings to specific environments without affecting others. This is useful when you want different behavior per environment:
+
+```yaml
+core:
+ workspace_id:
+ dev: "dev-workspace-id"
+ test: "test-workspace-id"
+ prod: "prod-workspace-id"
+ repository_directory: "./workspace" # Same for all environments
+
+publish:
+ # Only exclude legacy folders in prod environment
+ folder_exclude_regex:
+ prod: "^legacy_.*"
+ # dev and test not specified - no folder exclusion applied
+
+ # Skip publish in dev, run in test and prod
+ skip:
+ dev: true
+ # test and prod default to false
+```
+
+In this example:
+
+- Deploying to `dev`: No folder exclusion applied, `skip` = `true`
+- Deploying to `test`: No folder exclusion applied, `skip` = `false`
+- Deploying to `prod`: `folder_exclude_regex` = `"^legacy_.*"`, `skip` = `false`
+
+### Logging Behavior
+
+When an optional field uses environment mapping and does not include the target environment:
+
+- **Important optional fields** (`item_types_in_scope`, `parameter`): A **warning** is logged to alert users that the setting is being skipped.
+- **Other optional fields**: A **debug** message is logged, visible only when debug logging is enabled.
+
+Example log output when deploying to `prod` with the config above:
+
+```
+[Debug] - No value for 'folder_exclude_regex' in environment 'prod'. Available environments: ['dev']. This setting will be skipped.
+```
+
+To enable debug logging:
+
+```python
+change_log_level()
+```
+
+## Sample `config.yml` File
```yaml
core:
@@ -359,12 +441,12 @@ deploy_with_config(
**Important Considerations:**
-- **Caution:** Exercise caution when overriding configuration values for _production_ environments.
-- **Support:** Configuration overrides are supported for all sections and settings in the configuration file.
-- **Rules:**
- - Existing values can be overridden for any field in the configuration.
- - New values can only be added for optional fields that aren't present in the original configuration.
- - Required fields must exist in the original configuration in order to override.
+- **Caution:** Exercise caution when overriding configuration values for _production_ environments.
+- **Support:** Configuration overrides are supported for all sections and settings in the configuration file.
+- **Rules:**
+ - Existing values can be overridden for any field in the configuration.
+ - New values can only be added for optional fields that aren't present in the original configuration.
+ - Required fields must exist in the original configuration in order to override.
## Troubleshooting Guide
diff --git a/src/fabric_cicd/_common/_config_utils.py b/src/fabric_cicd/_common/_config_utils.py
index 7327aa23..555fbbc5 100644
--- a/src/fabric_cicd/_common/_config_utils.py
+++ b/src/fabric_cicd/_common/_config_utils.py
@@ -27,47 +27,77 @@ def load_config_file(config_file_path: str, environment: str, config_override: O
return validator.validate_config_file(config_file_path, environment, config_override)
+def get_config_value(config_section: dict, key: str, environment: str) -> str | list | bool | None:
+ """Extract a value from config, handling both single and environment-specific formats.
+
+ Args:
+ config_section: The config section to extract from
+ key: The key to extract
+ environment: Target environment
+
+ Returns:
+ The extracted value, or None if key doesn't exist or environment not found in dict
+ """
+ if key not in config_section:
+ return None
+
+ value = config_section[key]
+
+ if isinstance(value, dict):
+ return value.get(environment)
+
+ return value
+
+
+def update_setting(
+ settings: dict,
+ config: dict,
+ key: str,
+ environment: str,
+ default_value: Optional[str] = None,
+ output_key: Optional[str] = None,
+) -> None:
+ """
+ Gets a config value using get_config_value and updates the settings dictionary
+ if the value is not None.
+
+ Args:
+ settings: The settings dictionary to update
+ config: The configuration dictionary
+ key: The key to extract from the config
+ environment: Target environment
+ default_value: The default value to set if the config value is None
+ output_key: The key to use in the settings dictionary (defaults to `key` if None)
+ """
+ value = get_config_value(config, key, environment)
+ target_key = output_key or key
+ if value is not None:
+ settings[target_key] = value
+ elif default_value is not None:
+ settings[target_key] = default_value
+
+
def extract_workspace_settings(config: dict, environment: str) -> dict:
"""Extract workspace-specific settings from config for the given environment."""
environment = environment.strip()
core = config["core"]
settings = {}
- # Extract workspace ID or name based on environment
+ # Workspace ID or name - required, validation ensures value exists for target environment
if "workspace_id" in core:
- if isinstance(core["workspace_id"], dict):
- settings["workspace_id"] = core["workspace_id"][environment]
- else:
- settings["workspace_id"] = core["workspace_id"]
-
+ settings["workspace_id"] = get_config_value(core, "workspace_id", environment)
logger.info(f"Using workspace ID '{settings['workspace_id']}'")
-
elif "workspace" in core:
- if isinstance(core["workspace"], dict):
- settings["workspace_name"] = core["workspace"][environment]
- else:
- settings["workspace_name"] = core["workspace"]
-
+ settings["workspace_name"] = get_config_value(core, "workspace", environment)
logger.info(f"Using workspace '{settings['workspace_name']}'")
- # Extract other settings
+ # Repository directory - required, validation ensures value exists for target environment
if "repository_directory" in core:
- if isinstance(core["repository_directory"], dict):
- settings["repository_directory"] = core["repository_directory"][environment]
- else:
- settings["repository_directory"] = core["repository_directory"]
-
- if "item_types_in_scope" in core:
- if isinstance(core["item_types_in_scope"], dict):
- settings["item_types_in_scope"] = core["item_types_in_scope"][environment]
- else:
- settings["item_types_in_scope"] = core["item_types_in_scope"]
+ settings["repository_directory"] = get_config_value(core, "repository_directory", environment)
- if "parameter" in core:
- if isinstance(core["parameter"], dict):
- settings["parameter_file_path"] = core["parameter"][environment]
- else:
- settings["parameter_file_path"] = core["parameter"]
+ # Optional settings - validation logs warning if value not found for target environment
+ update_setting(settings, core, "item_types_in_scope", environment)
+ update_setting(settings, core, "parameter", environment, output_key="parameter_file_path")
return settings
@@ -79,35 +109,18 @@ def extract_publish_settings(config: dict, environment: str) -> dict:
if "publish" in config:
publish_config = config["publish"]
- if "exclude_regex" in publish_config:
- if isinstance(publish_config["exclude_regex"], dict):
- settings["exclude_regex"] = publish_config["exclude_regex"][environment]
- else:
- settings["exclude_regex"] = publish_config["exclude_regex"]
-
- if "folder_exclude_regex" in publish_config:
- if isinstance(publish_config["folder_exclude_regex"], dict):
- settings["folder_exclude_regex"] = publish_config["folder_exclude_regex"][environment]
- else:
- settings["folder_exclude_regex"] = publish_config["folder_exclude_regex"]
-
- if "items_to_include" in publish_config:
- if isinstance(publish_config["items_to_include"], dict):
- settings["items_to_include"] = publish_config["items_to_include"][environment]
- else:
- settings["items_to_include"] = publish_config["items_to_include"]
-
- if "shortcut_exclude_regex" in publish_config:
- if isinstance(publish_config["shortcut_exclude_regex"], dict):
- settings["shortcut_exclude_regex"] = publish_config["shortcut_exclude_regex"][environment]
- else:
- settings["shortcut_exclude_regex"] = publish_config["shortcut_exclude_regex"]
-
- if "skip" in publish_config:
- if isinstance(publish_config["skip"], dict):
- settings["skip"] = publish_config["skip"].get(environment, False)
- else:
- settings["skip"] = publish_config["skip"]
+ # Optional settings - validation logs debug if value not found for target environment
+ settings_to_update = [
+ "exclude_regex",
+ "folder_exclude_regex",
+ "items_to_include",
+ "shortcut_exclude_regex",
+ ]
+ for key in settings_to_update:
+ update_setting(settings, publish_config, key, environment)
+
+ # Skip defaults to False if setting not found
+ update_setting(settings, publish_config, "skip", environment, default_value=False)
return settings
@@ -119,23 +132,16 @@ def extract_unpublish_settings(config: dict, environment: str) -> dict:
if "unpublish" in config:
unpublish_config = config["unpublish"]
- if "exclude_regex" in unpublish_config:
- if isinstance(unpublish_config["exclude_regex"], dict):
- settings["exclude_regex"] = unpublish_config["exclude_regex"][environment]
- else:
- settings["exclude_regex"] = unpublish_config["exclude_regex"]
-
- if "items_to_include" in unpublish_config:
- if isinstance(unpublish_config["items_to_include"], dict):
- settings["items_to_include"] = unpublish_config["items_to_include"][environment]
- else:
- settings["items_to_include"] = unpublish_config["items_to_include"]
-
- if "skip" in unpublish_config:
- if isinstance(unpublish_config["skip"], dict):
- settings["skip"] = unpublish_config["skip"].get(environment, False)
- else:
- settings["skip"] = unpublish_config["skip"]
+ # Optional settings - validation logs debug if value not found for target environment
+ settings_to_update = [
+ "exclude_regex",
+ "items_to_include",
+ ]
+ for key in settings_to_update:
+ update_setting(settings, unpublish_config, key, environment)
+
+ # Skip defaults to False if setting not found
+ update_setting(settings, unpublish_config, "skip", environment, default_value=False)
return settings
diff --git a/src/fabric_cicd/_common/_config_validator.py b/src/fabric_cicd/_common/_config_validator.py
index 0943ae95..449b89c3 100644
--- a/src/fabric_cicd/_common/_config_validator.py
+++ b/src/fabric_cicd/_common/_config_validator.py
@@ -342,14 +342,14 @@ def _validate_environment_exists(self) -> None:
# Handle no target environment case
if any(
field_name in section and isinstance(section[field_name], dict)
- for section, field_name, _ in _get_config_fields(self.config)
+ for section, field_name, _, _, _ in _get_config_fields(self.config)
if not (field_name == "constants" and _is_regular_constants_dict(section.get(field_name, {})))
):
self.errors.append(constants.CONFIG_VALIDATION_MSGS["environment"]["no_env_with_mappings"])
return
# Check each field for target environment presence
- for section, field_name, display_name in _get_config_fields(self.config):
+ for section, field_name, display_name, is_required, log_warning in _get_config_fields(self.config):
if field_name in section:
field_value = section[field_name]
# Handle constants special case
@@ -359,12 +359,22 @@ def _validate_environment_exists(self) -> None:
# If it's a dict (environment mapping), check if target environment exists
if isinstance(field_value, dict) and self.environment not in field_value:
available_envs = list(field_value.keys())
- self.errors.append(
- constants.CONFIG_VALIDATION_MSGS["environment"]["env_not_found"].format(
- self.environment, display_name, available_envs
- )
+ msg = (
+ f"Environment '{self.environment}' not found in '{display_name}'. "
+ f"Available environments: {available_envs}. This setting will be skipped."
)
+ if is_required:
+ self.errors.append(
+ constants.CONFIG_VALIDATION_MSGS["environment"]["env_not_found"].format(
+ self.environment, display_name, available_envs
+ )
+ )
+ elif log_warning:
+ logger.warning(msg)
+ else:
+ logger.debug(msg)
+
def _validate_environment_mapping(self, field_value: dict, field_name: str, accepted_type: type) -> bool:
"""Validate field with environment mapping."""
if not field_value:
@@ -572,6 +582,12 @@ def _resolve_path_field(
# If environment mapping is used and target environment is provided, only process that environment path
if self.environment and self.environment != "N/A" and isinstance(field_value, dict):
+ if self.environment not in paths_to_resolve:
+ # Skip if environment not in mapping (for parameter field, which is optional)
+ logger.debug(
+ f"Skipping path resolution for '{field_name}' - environment '{self.environment}' not in mapping"
+ )
+ return
paths_to_resolve = {self.environment: paths_to_resolve[self.environment]}
for env_key, path_str in paths_to_resolve.items():
@@ -954,26 +970,35 @@ def _validate_constants_dict(self, constants_dict: dict, context: str) -> None:
)
-def _get_config_fields(config: dict) -> list[tuple[dict, str, str]]:
- """Get list of all fields that support environment mappings."""
+def _get_config_fields(config: dict) -> list[tuple[dict, str, str, bool, bool]]:
+ """Get list of all fields that support environment mappings.
+
+ Returns:
+ List of tuples: (section_dict, field_name, display_name, is_required, log_warning)
+ - is_required: If True, missing environment causes error.
+ - log_warning: logging type (e.g., warning (True), debug (False)).
+ """
return [
- # Core section fields
- (config.get("core", {}), "workspace_id", "core.workspace_id"),
- (config.get("core", {}), "workspace", "core.workspace"),
- (config.get("core", {}), "repository_directory", "core.repository_directory"),
- (config.get("core", {}), "item_types_in_scope", "core.item_types_in_scope"),
- (config.get("core", {}), "parameter", "core.parameter"),
- # Publish section fields
- (config.get("publish", {}), "exclude_regex", "publish.exclude_regex"),
- (config.get("publish", {}), "items_to_include", "publish.items_to_include"),
- (config.get("publish", {}), "skip", "publish.skip"),
- # Unpublish section fields
- (config.get("unpublish", {}), "exclude_regex", "unpublish.exclude_regex"),
- (config.get("unpublish", {}), "items_to_include", "unpublish.items_to_include"),
- (config.get("unpublish", {}), "skip", "unpublish.skip"),
- # Top-level sections
- (config, "features", "features"),
- (config, "constants", "constants"),
+ # Core section fields - required
+ (config.get("core", {}), "workspace_id", "core.workspace_id", True, False),
+ (config.get("core", {}), "workspace", "core.workspace", True, False),
+ (config.get("core", {}), "repository_directory", "core.repository_directory", True, False),
+ # Core section fields - optional but important (warn if missing)
+ (config.get("core", {}), "item_types_in_scope", "core.item_types_in_scope", False, True),
+ (config.get("core", {}), "parameter", "core.parameter", False, True),
+ # Publish section fields - optional (debug if missing)
+ (config.get("publish", {}), "exclude_regex", "publish.exclude_regex", False, False),
+ (config.get("publish", {}), "folder_exclude_regex", "publish.folder_exclude_regex", False, False),
+ (config.get("publish", {}), "shortcut_exclude_regex", "publish.shortcut_exclude_regex", False, False),
+ (config.get("publish", {}), "items_to_include", "publish.items_to_include", False, False),
+ (config.get("publish", {}), "skip", "publish.skip", False, False),
+ # Unpublish section fields - optional (debug if missing)
+ (config.get("unpublish", {}), "exclude_regex", "unpublish.exclude_regex", False, False),
+ (config.get("unpublish", {}), "items_to_include", "unpublish.items_to_include", False, False),
+ (config.get("unpublish", {}), "skip", "unpublish.skip", False, False),
+ # Top-level sections - optional (debug if missing)
+ (config, "features", "features", False, False),
+ (config, "constants", "constants", False, False),
]
diff --git a/tests/test_config_validator.py b/tests/test_config_validator.py
index 801dc106..0192d3ee 100644
--- a/tests/test_config_validator.py
+++ b/tests/test_config_validator.py
@@ -765,6 +765,28 @@ def test_resolve_path_field_environment_mapping(self, tmp_path):
# PROD should remain unchanged since it wasn't the target environment
assert self.validator.config["test_section"]["test_field"]["PROD"] == "prod_dir"
+ def test_resolve_path_field_environment_not_in_mapping(self, tmp_path):
+ """Test _resolve_path_field skips gracefully when environment is not in mapping."""
+ self.validator.environment = "prod" # Target environment
+
+ # Create directory only for 'dev' environment
+ dev_dir = tmp_path / "dev_dir"
+ dev_dir.mkdir()
+
+ # Parameter mapping only has 'dev', not 'prod'
+ field_value = {"dev": "dev_dir"}
+
+ self.validator.config = {"test_section": {"test_field": field_value}}
+ self.validator.config_path = tmp_path / "config.yml"
+
+ # Should NOT raise KeyError - should skip gracefully
+ self.validator._resolve_path_field(field_value, "test_field", "test_section", "directory")
+
+ # No errors should be added (optional field behavior)
+ assert self.validator.errors == []
+ # Config should remain unchanged since resolution was skipped
+ assert self.validator.config["test_section"]["test_field"] == {"dev": "dev_dir"}
+
def test_resolve_path_field_nonexistent_path(self, tmp_path):
"""Test _resolve_path_field with nonexistent path."""
self.validator.config = {"test_section": {"test_field": "nonexistent_dir"}}
@@ -1351,7 +1373,13 @@ def test_get_config_fields_complete_config(self):
"item_types_in_scope": ["Notebook"],
"parameter": "param.yml",
},
- "publish": {"exclude_regex": ".*_test", "items_to_include": ["item1"], "skip": False},
+ "publish": {
+ "exclude_regex": ".*_test",
+ "folder_exclude_regex": "^temp/",
+ "shortcut_exclude_regex": "^shortcut_temp/",
+ "items_to_include": ["item1"],
+ "skip": False,
+ },
"unpublish": {"exclude_regex": ".*_old", "items_to_include": ["item2"], "skip": True},
"features": ["feature1"],
"constants": {"KEY": "value"},
@@ -1360,16 +1388,28 @@ def test_get_config_fields_complete_config(self):
fields = _get_config_fields(config)
# Should return all fields from all sections
- assert len(fields) == 13
+ assert len(fields) == 15 # Updated count with folder_exclude_regex and shortcut_exclude_regex
# Check some specific fields
field_names = [field[1] for field in fields]
assert "workspace_id" in field_names
assert "repository_directory" in field_names
assert "parameter" in field_names
+ assert "folder_exclude_regex" in field_names
+ assert "shortcut_exclude_regex" in field_names
assert "features" in field_names
assert "constants" in field_names
+ # Check required vs optional flags
+ for _section, field_name, _display_name, is_required, warn_if_missing in fields:
+ if field_name in ["workspace_id", "workspace", "repository_directory"]:
+ assert is_required is True, f"{field_name} should be required"
+ else:
+ assert is_required is False, f"{field_name} should be optional"
+
+ if field_name in ["item_types_in_scope", "parameter"]:
+ assert warn_if_missing is True, f"{field_name} should warn if missing"
+
def test_is_regular_constants_dict_regular(self):
"""Test _is_regular_constants_dict with regular constants dictionary."""
from fabric_cicd._common._config_validator import _is_regular_constants_dict
@@ -1865,10 +1905,12 @@ def test_environment_mismatch_in_multiple_fields(self):
self.validator._validate_environment_exists()
- # Should get multiple errors for each field that has environment mapping
- assert len(self.validator.errors) >= 3 # At least workspace_id, repository_directory, and item_types
+ # Only required fields (workspace_id, repository_directory) cause errors
+ # Optional fields (item_types_in_scope, skip) only log warnings/debug
+ assert len(self.validator.errors) == 2
error_text = " ".join(self.validator.errors)
- assert "Environment 'test' not found" in error_text
+ assert "workspace_id" in error_text
+ assert "repository_directory" in error_text
def test_environment_mapping_vs_basic_values_mixed(self):
"""Test configuration with both environment mappings and basic values."""
@@ -1902,17 +1944,67 @@ def test_environment_mapping_vs_basic_values_mismatch(self):
},
"publish": {
"exclude_regex": "^TEST.*", # Basic value - should be ignored
- "skip": {"dev": True}, # Environment mapping - missing 'prod'
+ "skip": {"dev": True}, # Environment mapping - missing 'prod' (optional, no error)
+ },
+ }
+ self.validator.environment = "prod"
+
+ self.validator._validate_environment_exists()
+
+ # Should get error only for required field with environment mapping
+ # Optional fields (skip) only log debug, not errors
+ assert len(self.validator.errors) == 1
+ error_text = " ".join(self.validator.errors)
+ assert "workspace_id" in error_text
+ assert "skip" not in error_text # Optional field should not cause error
+
+ def test_environment_mismatch_optional_fields_log_only(self):
+ """Test that optional fields only log warnings/debug when environment is missing."""
+ import logging
+
+ self.validator.config = {
+ "core": {
+ "workspace_id": "simple-id", # Not a mapping, won't trigger error
+ "repository_directory": "/path",
+ "item_types_in_scope": {"dev": ["Notebook"]}, # Optional, warn_if_missing=True
+ "parameter": {"dev": "param.yml"}, # Optional, warn_if_missing=True
+ },
+ "publish": {
+ "exclude_regex": {"dev": "^TEST.*"}, # Optional, warn_if_missing=False
+ "skip": {"dev": True}, # Optional, warn_if_missing=False
+ },
+ }
+ self.validator.environment = "prod"
+
+ with (
+ patch.object(logging.getLogger("fabric_cicd._common._config_validator"), "warning") as mock_warning,
+ patch.object(logging.getLogger("fabric_cicd._common._config_validator"), "debug") as mock_debug,
+ ):
+ self.validator._validate_environment_exists()
+
+ # No errors for optional fields
+ assert self.validator.errors == []
+
+ # Warnings should be logged for item_types_in_scope and parameter
+ assert mock_warning.call_count == 2
+
+ # Debug should be logged for exclude_regex and skip
+ assert mock_debug.call_count == 2
+
+ def test_environment_mismatch_required_fields_error(self):
+ """Test that required fields cause errors when environment is missing."""
+ self.validator.config = {
+ "core": {
+ "workspace_id": {"dev": "dev-id"}, # Required
+ "repository_directory": {"dev": "/dev/path"}, # Required
},
}
self.validator.environment = "prod"
self.validator._validate_environment_exists()
- # Should get errors only for fields with environment mappings
+ # Should get errors for both required fields
assert len(self.validator.errors) == 2
error_text = " ".join(self.validator.errors)
assert "workspace_id" in error_text
- assert "skip" in error_text
- assert "repository_directory" not in error_text # Basic value should not cause error
- assert "exclude_regex" not in error_text # Basic value should not cause error
+ assert "repository_directory" in error_text
diff --git a/tests/test_deploy_with_config.py b/tests/test_deploy_with_config.py
index 194bd084..f1ddfb68 100644
--- a/tests/test_deploy_with_config.py
+++ b/tests/test_deploy_with_config.py
@@ -762,3 +762,150 @@ def test_extract_publish_settings_with_environment_specific_folder_exclude_regex
settings = extract_publish_settings(config, "prod")
assert settings["folder_exclude_regex"] == "^PROD_FOLDER/"
+
+ def test_extract_publish_settings_missing_environment_skips_setting(self):
+ """Test that missing environment in optional publish settings skips the setting."""
+ config = {
+ "publish": {
+ "exclude_regex": {"dev": "^DEV.*"}, # Only dev defined
+ "folder_exclude_regex": {"dev": "^DEV_FOLDER/"}, # Only dev defined
+ }
+ }
+
+ # prod environment not defined - settings should be skipped
+ settings = extract_publish_settings(config, "prod")
+ assert "exclude_regex" not in settings
+ assert "folder_exclude_regex" not in settings
+
+ def test_extract_unpublish_settings_missing_environment_skips_setting(self):
+ """Test that missing environment in optional unpublish settings skips the setting."""
+ config = {
+ "unpublish": {
+ "exclude_regex": {"dev": "^DEV.*"}, # Only dev defined
+ "items_to_include": {"dev": ["item1"]}, # Only dev defined
+ }
+ }
+
+ # prod environment not defined - settings should be skipped
+ settings = extract_unpublish_settings(config, "prod")
+ assert "exclude_regex" not in settings
+ assert "items_to_include" not in settings
+
+ def test_extract_publish_settings_skip_defaults_false_when_env_missing(self):
+ """Test that skip defaults to False when environment is not in skip mapping."""
+ config = {
+ "publish": {
+ "skip": {"dev": True}, # Only dev defined
+ }
+ }
+
+ # prod environment not defined - skip should default to False
+ settings = extract_publish_settings(config, "prod")
+ assert settings["skip"] is False
+
+ def test_extract_unpublish_settings_skip_defaults_false_when_env_missing(self):
+ """Test that skip defaults to False when environment is not in skip mapping."""
+ config = {
+ "unpublish": {
+ "skip": {"dev": True}, # Only dev defined
+ }
+ }
+
+ # prod environment not defined - skip should default to False
+ settings = extract_unpublish_settings(config, "prod")
+ assert settings["skip"] is False
+
+ def test_extract_workspace_settings_optional_fields_missing_environment(self):
+ """Test that optional workspace fields are skipped when environment is missing."""
+ config = {
+ "core": {
+ "workspace_id": "12345678-1234-1234-1234-123456789abc", # Simple value
+ "repository_directory": "/path/to/repo",
+ "item_types_in_scope": {"dev": ["Notebook"]}, # Only dev defined
+ "parameter": {"dev": "dev-param.yml"}, # Only dev defined
+ }
+ }
+
+ # prod environment not defined for optional fields - they should be skipped
+ settings = extract_workspace_settings(config, "prod")
+ assert "item_types_in_scope" not in settings
+ assert "parameter_file_path" not in settings
+ # Required fields should still be present
+ assert settings["workspace_id"] == "12345678-1234-1234-1234-123456789abc"
+ assert settings["repository_directory"] == "/path/to/repo"
+
+ def test_extract_publish_settings_shortcut_exclude_regex_missing_environment(self):
+ """Test that shortcut_exclude_regex is skipped when environment is missing."""
+ config = {
+ "publish": {
+ "shortcut_exclude_regex": {"dev": "^dev_temp_.*"}, # Only dev defined
+ }
+ }
+
+ # prod environment not defined - setting should be skipped
+ settings = extract_publish_settings(config, "prod")
+ assert "shortcut_exclude_regex" not in settings
+
+ def test_extract_publish_settings_items_to_include_missing_environment(self):
+ """Test that items_to_include is skipped when environment is missing."""
+ config = {
+ "publish": {
+ "items_to_include": {"dev": ["item1.Notebook", "item2.DataPipeline"]}, # Only dev defined
+ }
+ }
+
+ # prod environment not defined - setting should be skipped
+ settings = extract_publish_settings(config, "prod")
+ assert "items_to_include" not in settings
+
+
+class TestGetConfigValue:
+ """Test the get_config_value utility function."""
+
+ def test_get_config_value_key_not_present(self):
+ """Test get_config_value when key doesn't exist."""
+ from fabric_cicd._common._config_utils import get_config_value
+
+ config = {"other_key": "value"}
+ result = get_config_value(config, "missing_key", "dev")
+ assert result is None
+
+ def test_get_config_value_simple_value(self):
+ """Test get_config_value with simple (non-dict) value."""
+ from fabric_cicd._common._config_utils import get_config_value
+
+ config = {"key": "simple_value"}
+ result = get_config_value(config, "key", "dev")
+ assert result == "simple_value"
+
+ def test_get_config_value_dict_with_environment(self):
+ """Test get_config_value with dict containing target environment."""
+ from fabric_cicd._common._config_utils import get_config_value
+
+ config = {"key": {"dev": "dev_value", "prod": "prod_value"}}
+ result = get_config_value(config, "key", "dev")
+ assert result == "dev_value"
+
+ def test_get_config_value_dict_missing_environment(self):
+ """Test get_config_value with dict missing target environment."""
+ from fabric_cicd._common._config_utils import get_config_value
+
+ config = {"key": {"dev": "dev_value"}}
+ result = get_config_value(config, "key", "prod")
+ assert result is None
+
+ def test_get_config_value_list_value(self):
+ """Test get_config_value with list value."""
+ from fabric_cicd._common._config_utils import get_config_value
+
+ config = {"key": ["item1", "item2"]}
+ result = get_config_value(config, "key", "dev")
+ assert result == ["item1", "item2"]
+
+ def test_get_config_value_bool_value(self):
+ """Test get_config_value with boolean value."""
+ from fabric_cicd._common._config_utils import get_config_value
+
+ config = {"key": True}
+ result = get_config_value(config, "key", "dev")
+ assert result is True