Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions phase_cli/utils/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"\$\{(?!\{)([^.]+?)\}")
6 changes: 3 additions & 3 deletions phase_cli/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 ''

Expand Down
48 changes: 48 additions & 0 deletions tests/secret_referencing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"
Loading