diff --git a/phase_cli/utils/const.py b/phase_cli/utils/const.py index 88e18d8d..e31487cb 100644 --- a/phase_cli/utils/const.py +++ b/phase_cli/utils/const.py @@ -21,7 +21,7 @@ |__/ """ -SECRET_REF_REGEX = re.compile(r"\$\{([^}]+)\}") +SECRET_REF_REGEX = re.compile(r"\$\{(?!\{)([^}]+)\}") # Define paths to Phase configs PHASE_ENV_CONFIG = ".phase.json" # Holds project and environment contexts in users repo, unique to each application. @@ -47,5 +47,6 @@ r"^pss_service:v(\d+):([a-fA-F0-9]{64}):([a-fA-F0-9]{64}):([a-fA-F0-9]{64}):([a-fA-F0-9]{64})$" ) -cross_env_pattern = re.compile(r"\$\{(.+?)\.(.+?)\}") -local_ref_pattern = re.compile(r"\$\{([^.]+?)\}") +CROSS_APP_ENV_PATTERN = re.compile(r"\$\{(?!\{)(.+?)::(.+?)\.(.+?)\}") +CROSS_ENV_PATTERN = re.compile(r"\$\{(?!\{)(?![^{]*::)([^.]+?)\.(.+?)\}") +LOCAL_REF_PATTERN = re.compile(r"\$\{(?!\{)([^.]+?)\}") diff --git a/phase_cli/utils/misc.py b/phase_cli/utils/misc.py index 00251ec7..dbbca0fd 100644 --- a/phase_cli/utils/misc.py +++ b/phase_cli/utils/misc.py @@ -12,7 +12,7 @@ from rich.box import ROUNDED from urllib.parse import urlparse from typing import Union, List -from phase_cli.utils.const import __version__, PHASE_ENV_CONFIG, PHASE_CLOUD_API_HOST, PHASE_SECRETS_DIR, cross_env_pattern, local_ref_pattern +from phase_cli.utils.const import __version__, PHASE_ENV_CONFIG, PHASE_CLOUD_API_HOST, PHASE_SECRETS_DIR, CROSS_ENV_PATTERN, LOCAL_REF_PATTERN import platform import shutil @@ -171,8 +171,8 @@ def format_secret_row(secret, value_width, show): comment = " 💬" if secret.get("comment") else "" key_display = f"{key}{tags}{comment}" - icon = '⛓️ ' if cross_env_pattern.search(value) else '' - icon += '🔗 ' if local_ref_pattern.search(value) else '' + icon = '⛓️ ' if CROSS_ENV_PATTERN.search(value) else '' + icon += '🔗 ' if LOCAL_REF_PATTERN.search(value) else '' personal_indicator = '🔏 ' if secret.get("overridden", False) else '' diff --git a/tests/secret_referencing.py b/tests/secret_referencing.py index 735677be..703e79c6 100644 --- a/tests/secret_referencing.py +++ b/tests/secret_referencing.py @@ -291,3 +291,51 @@ def test_multiple_occurrences_same_reference(phase, current_application_name, cu ] resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name) assert resolved_value == "A=v;B=v" + + +# ============================================================================= +# Syntax preservation tests to prevent referencing syntax overalp with third party platforms like Railway with ${{...}} +# ============================================================================= + +def test_railway_syntax_preserved(phase, current_application_name, current_env_name): + """Railway-style ${{...}} syntax should NOT be treated as a secret reference.""" + value = "Some value with ${{RAILWAY_REF}}" + all_secrets = [] + resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name) + assert resolved_value == "Some value with ${{RAILWAY_REF}}" + + +def test_railway_syntax_with_env_preserved(phase, current_application_name, current_env_name): + """Railway-style ${{env.key}} should NOT be treated as cross-env reference.""" + value = "${{production.DATABASE_URL}}" + all_secrets = [] + resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name) + assert resolved_value == "${{production.DATABASE_URL}}" + + +def test_mixed_railway_and_phase_refs(phase, current_application_name, current_env_name): + """Mix of ${{...}} Railway and ${...} Phase refs - only Phase should be resolved.""" + value = "Railway: ${{RAILWAY_TOKEN}}, Phase: ${KEY}" + all_secrets = [ + {"environment": current_env_name, "path": "/", "key": "KEY", "value": "secret_value"}, + ] + resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name) + assert resolved_value == "Railway: ${{RAILWAY_TOKEN}}, Phase: secret_value" + + +def test_secret_value_containing_railway_syntax(phase, current_application_name, current_env_name): + """Secret values containing ${{...}} should preserve the Railway syntax after resolution.""" + value = "${CONFIG}" + all_secrets = [ + {"environment": current_env_name, "path": "/", "key": "CONFIG", "value": "url=${{RAILWAY.STATIC_URL}}"}, + ] + resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name) + assert resolved_value == "url=${{RAILWAY.STATIC_URL}}" + + +def test_github_actions_syntax_preserved(phase, current_application_name, current_env_name): + """GitHub Actions ${{ secrets.X }} syntax should be preserved like Railway.""" + value = "${{ secrets.GITHUB_TOKEN }}" + all_secrets = [] + resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name) + assert resolved_value == "${{ secrets.GITHUB_TOKEN }}"