|
7 | 7 |
|
8 | 8 | import logging |
9 | 9 | import os |
| 10 | +import re |
10 | 11 | from pathlib import Path |
11 | 12 | from typing import Any, Optional |
12 | 13 |
|
|
34 | 35 | ) |
35 | 36 | from .validator import ConfigValidator |
36 | 37 |
|
| 38 | +# Regex for embedded env-var references in config strings. |
| 39 | +# Matches both ``${VAR}`` and ``$VAR`` (latter only for ASCII identifiers). |
| 40 | +# WHY: Some users write ``api_token: "${CONFLUENCE_API_TOKEN}"`` but others may |
| 41 | +# inadvertently write ``$CONFLUENCE_API_TOKEN`` or embed the reference in a |
| 42 | +# larger string (e.g. ``"Bearer ${TOKEN}"``); handling all three keeps behaviour |
| 43 | +# predictable and backward compatible. |
| 44 | +_ENV_VAR_PATTERN = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)") |
| 45 | + |
37 | 46 | logger = logging.getLogger(__name__) |
38 | 47 |
|
39 | 48 |
|
@@ -512,7 +521,7 @@ def _process_pm_config(cls, pm_data: dict[str, Any]) -> Optional[Any]: |
512 | 521 | ), |
513 | 522 | }, |
514 | 523 | )() |
515 | | - pm_config.jira = jira_sub_config |
| 524 | + pm_config.jira = jira_sub_config # type: ignore[misc] |
516 | 525 |
|
517 | 526 | return pm_config |
518 | 527 |
|
@@ -563,30 +572,74 @@ def _process_pm_integration_config( |
563 | 572 |
|
564 | 573 | @staticmethod |
565 | 574 | def _resolve_env_var(value: Optional[str]) -> Optional[str]: |
566 | | - """Resolve environment variable references. |
| 575 | + """Resolve environment variable references in a string. |
| 576 | +
|
| 577 | + Supports three syntaxes (all backward-compatible with prior |
| 578 | + exact-match behaviour): |
| 579 | +
|
| 580 | + * ``${VAR}`` — braced reference (preferred, works anywhere in the |
| 581 | + string). |
| 582 | + * ``$VAR`` — unbraced reference (must be a valid identifier). |
| 583 | + * Embedded references (e.g. ``"Bearer ${TOKEN}"``). |
| 584 | +
|
| 585 | + If a referenced variable is not set in ``os.environ``, the reference |
| 586 | + is replaced with an empty string and a warning is logged — this |
| 587 | + mirrors shell behaviour and lets callers decide whether the resulting |
| 588 | + value is acceptable. For the legacy whole-string ``${VAR}`` pattern |
| 589 | + the function preserves the prior contract of returning ``None`` when |
| 590 | + the variable is unset, so downstream ``or ""`` guards continue to |
| 591 | + work. |
567 | 592 |
|
568 | 593 | Args: |
569 | | - value: Value that may contain environment variable reference |
| 594 | + value: Value that may contain environment variable reference(s). |
570 | 595 |
|
571 | 596 | Returns: |
572 | | - Resolved value or None |
573 | | -
|
574 | | - Raises: |
575 | | - EnvironmentVariableError: If environment variable is not set |
| 597 | + Resolved string, or ``None`` if the input was empty / the legacy |
| 598 | + whole-string reference resolved to an unset variable. |
576 | 599 | """ |
| 600 | + if value is None: |
| 601 | + return None |
| 602 | + if not isinstance(value, str): |
| 603 | + return value # type: ignore[return-value] |
577 | 604 | if not value: |
578 | 605 | return None |
579 | 606 |
|
580 | | - if value.startswith("${") and value.endswith("}"): |
| 607 | + # Legacy whole-string ``${VAR}`` path — keep exact prior semantics so |
| 608 | + # callers that rely on ``None`` (rather than "") continue to work. |
| 609 | + if value.startswith("${") and value.endswith("}") and value.count("${") == 1: |
581 | 610 | env_var = value[2:-1] |
582 | 611 | resolved = os.environ.get(env_var) |
583 | 612 | if not resolved: |
584 | | - # Note: We don't raise here directly, let the caller handle it |
585 | | - # based on whether the field is required |
| 613 | + logger.warning( |
| 614 | + "Environment variable %r referenced in config is not set or empty; " |
| 615 | + "the resulting credential/value will be blank. Check your .env / " |
| 616 | + ".env.local files and shell environment.", |
| 617 | + env_var, |
| 618 | + ) |
586 | 619 | return None |
587 | 620 | return resolved |
588 | 621 |
|
589 | | - return value |
| 622 | + # Otherwise, perform substitution for every ``${VAR}`` / ``$VAR`` match. |
| 623 | + if "$" not in value: |
| 624 | + return value |
| 625 | + |
| 626 | + missing: list[str] = [] |
| 627 | + |
| 628 | + def _sub(match: "re.Match[str]") -> str: |
| 629 | + name = match.group(1) or match.group(2) |
| 630 | + resolved = os.environ.get(name, "") |
| 631 | + if not resolved: |
| 632 | + missing.append(name) |
| 633 | + return resolved |
| 634 | + |
| 635 | + substituted = _ENV_VAR_PATTERN.sub(_sub, value) |
| 636 | + if missing: |
| 637 | + logger.warning( |
| 638 | + "Environment variable(s) %s referenced in config are not set or empty; " |
| 639 | + "substituted with empty strings. Check your .env / .env.local files.", |
| 640 | + ", ".join(sorted(set(missing))), |
| 641 | + ) |
| 642 | + return substituted |
590 | 643 |
|
591 | 644 | @classmethod |
592 | 645 | def _resolve_config_dict(cls, config_dict: dict[str, Any]) -> dict[str, Any]: |
|
0 commit comments