Skip to content
Draft
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
27 changes: 23 additions & 4 deletions src/azure-cli/azure/cli/command_modules/appservice/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -7559,7 +7559,6 @@ def _get_raw_stacks_from_api(self):
def _parse_raw_stacks(self, stacks):
# Track seen runtime display names to avoid duplicates in Linux parsing.
# Linux Java containers (e.g., JBOSSEAP) can produce duplicate entries across major versions.
# Windows parsing doesn't have this issue due to its different structure.

java_eol_map = self._build_java_eol_map(stacks)

Expand All @@ -7580,6 +7579,16 @@ def _parse_raw_stacks(self, stacks):
runtime_family=lang.display_text,
java_eol_map=java_eol_map)

unique_runtimes = []
seen_runtime_names = set()
for runtime in self._stacks:
runtime_key = (runtime.linux, runtime.display_name.lower())
if runtime_key in seen_runtime_names:
continue
seen_runtime_names.add(runtime_key)
unique_runtimes.append(runtime)
self._stacks = unique_runtimes

def _build_java_eol_map(self, stacks):
"""Build Java version -> EOL date map from the 'Java' stack.

Expand Down Expand Up @@ -7618,7 +7627,7 @@ def remove_delimiters(cls, runtime):
return cls.DEFAULT_DELIMETER.join(filter(None, runtime))

def resolve(self, display_name, linux=False):
display_name = display_name.lower()
display_name = self.standardize_node_runtime_name(display_name).lower()
stack = next((s for s in self.stacks if s.linux == linux and s.display_name.lower() == display_name), None)
if stack is None: # help convert previously acceptable stack names into correct ones if runtime not found
old_to_new_windows = {
Expand Down Expand Up @@ -7707,6 +7716,13 @@ def _format_windows_display_text(cls, display_text):
t = re.sub(r"\(.*\)", "", t) # remove "(LTS)"
return t.replace(" ", "|", 1).replace(" ", "")

@staticmethod
def standardize_node_runtime_name(runtime_name):
match = re.fullmatch(r'node\|(\d+)(?:-?lts)?', runtime_name, re.IGNORECASE)
if match and int(match.group(1)) >= 26:
return "NODE|{}".format(match.group(1))
return runtime_name

@classmethod
def _is_valid_runtime_setting(cls, runtime_setting, include_eol=False):
# Using datetime module imported at the top level
Expand Down Expand Up @@ -7908,6 +7924,7 @@ def _parse_major_version_windows(self, major_version, parsed_results, config_map
eol_date = self._format_eol_date(getattr(settings, 'end_of_life_date', None))
if "Java" not in minor_version.display_text:
runtime_name = self._format_windows_display_text(minor_version.display_text)
runtime_name = self.standardize_node_runtime_name(runtime_name)

runtime = self.Runtime(display_name=runtime_name, linux=False,
os="Windows", runtime_family=runtime_family,
Expand Down Expand Up @@ -8019,7 +8036,7 @@ def _parse_major_version_linux(self, major_version, parsed_results, seen_runtime
major_version, linux=True, java=False, include_eol=self._include_eol)
for minor_version in minor_versions:
settings = minor_version.stack_settings.linux_runtime_settings
runtime_name = settings.runtime_version
runtime_name = self.standardize_node_runtime_name(settings.runtime_version)
runtime = self.Runtime(display_name=runtime_name,
configs={"linux_fx_version": runtime_name},
linux=True,
Expand Down Expand Up @@ -11921,6 +11938,8 @@ def delete_function_key(cmd, resource_group_name, name, key_name, function_name=
def add_github_actions(cmd, resource_group, name, repo, runtime=None, token=None, slot=None, # pylint: disable=too-many-statements,too-many-branches
branch='master', login_with_github=False, force=False):
runtime = _StackRuntimeHelper(cmd).remove_delimiters(runtime) # normalize "runtime:version"
if runtime:
runtime = _StackRuntimeHelper.standardize_node_runtime_name(runtime)
if not token and not login_with_github:
raise_missing_token_suggestion()
elif not token:
Expand Down Expand Up @@ -12860,7 +12879,7 @@ def _get_app_runtime_info_helper(cmd, app_runtime, app_runtime_version, is_linux
if gh_props.get("github_actions_version"):
if is_linux:
return {
"display_name": app_runtime,
"display_name": matched_runtime.display_name,
"github_actions_version": gh_props["github_actions_version"]
}
if gh_props.get("app_runtime_version").lower() == app_runtime_version.lower():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def setUp(self):
@mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory')
@mock.patch('azure.cli.command_modules.appservice.custom.get_app_details')
def test_webapp_github_actions_add(self, get_app_details_mock, web_client_factory_mock, site_availability_mock, *args):
runtime = "python:3.9"
runtime = "NODE:26-lts"
rg = "group"
is_linux = True
cmd = _get_test_cmd()
Expand All @@ -82,7 +82,7 @@ def test_webapp_github_actions_add(self, get_app_details_mock, web_client_factor

with mock.patch('azure.cli.command_modules.appservice.custom._runtime_supports_github_actions', autospec=True) as m:
add_github_actions(cmd, rg, "name", "repo", runtime, "token")
m.assert_called_with(cmd, runtime.replace(":", "|"), is_linux)
m.assert_called_with(cmd, "NODE|26", is_linux)

@mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
def test_set_deployment_user_creds(self, client_factory_mock):
Expand Down Expand Up @@ -1643,6 +1643,94 @@ def __len__(self):
return len(self._data)


class TestStackRuntimeNodeStandardization(unittest.TestCase):
Comment thread
aamos-company marked this conversation as resolved.
@staticmethod
def _new_helper():
from azure.cli.command_modules.appservice.custom import _StackRuntimeHelper
helper = _StackRuntimeHelper.__new__(_StackRuntimeHelper)
helper._linux = True
helper._windows = True
helper._include_eol = False
helper._stacks = []
helper.windows_config_mappings = {'node': 'WEBSITE_NODE_DEFAULT_VERSION'}
return helper

@staticmethod
def _node_stack(version, display_text, linux_runtime):
git_hub_action_settings = types.SimpleNamespace(is_supported=True, supported_version="{}.x".format(version))
linux_settings = types.SimpleNamespace(
runtime_version=linux_runtime,
is_hidden=False,
is_deprecated=False,
end_of_life_date=None,
git_hub_action_settings=git_hub_action_settings,
)
windows_settings = types.SimpleNamespace(
runtime_version="~{}".format(version),
is_hidden=False,
is_deprecated=False,
end_of_life_date=None,
git_hub_action_settings=git_hub_action_settings,
)
minor = types.SimpleNamespace(
display_text=display_text,
stack_settings=types.SimpleNamespace(
linux_container_settings=None,
linux_runtime_settings=linux_settings,
windows_container_settings=None,
windows_runtime_settings=windows_settings,
),
)
major = types.SimpleNamespace(display_text=display_text, minor_versions=[minor])
return types.SimpleNamespace(display_text='Node', major_versions=[major])

def test_node_26_uses_standard_identifier_on_both_platforms(self):
from azure.cli.command_modules.appservice.custom import _get_app_runtime_info_helper

helper = self._new_helper()
helper._parse_raw_stacks([self._node_stack('26', 'Node 26 LTS', 'NODE|26-lts')])

self.assertEqual(
[(runtime.os, runtime.display_name) for runtime in helper._stacks],
[('Linux', 'NODE|26'), ('Windows', 'NODE|26')])
self.assertEqual(
[(row['os'], row['config']) for row in helper.get_stacks_as_table(runtime_filter='node')],
[('Linux', 'NODE|26'), ('Windows', 'NODE|26')])
self.assertEqual(helper.resolve('NODE|26', linux=True).configs['linux_fx_version'], 'NODE|26')
self.assertEqual(
helper.resolve('NODE|26', linux=False).configs['WEBSITE_NODE_DEFAULT_VERSION'], '~26')
self.assertEqual(helper.resolve('NODE|26-lts', linux=True).configs['linux_fx_version'], 'NODE|26')
self.assertEqual(
helper.resolve('NODE|26LTS', linux=False).configs['WEBSITE_NODE_DEFAULT_VERSION'], '~26')

with mock.patch('azure.cli.command_modules.appservice.custom._StackRuntimeHelper', return_value=helper):
runtime_info = _get_app_runtime_info_helper(mock.Mock(), 'NODE|26-lts', '', True)

self.assertEqual(runtime_info, {
'display_name': 'NODE|26',
'github_actions_version': '26.x',
})

def test_older_node_identifiers_remain_unchanged(self):
helper = self._new_helper()
helper._parse_raw_stacks([self._node_stack('24', 'Node 24 LTS', 'NODE|24-lts')])

self.assertEqual(
[(runtime.os, runtime.display_name) for runtime in helper._stacks],
[('Linux', 'NODE|24-lts'), ('Windows', 'NODE|24LTS')])

def test_node_26_transition_identifiers_are_deduplicated(self):
helper = self._new_helper()
helper._parse_raw_stacks([
self._node_stack('26', 'Node 26 LTS', 'NODE|26-lts'),
self._node_stack('26', 'Node 26', 'NODE|26'),
])

self.assertEqual(
[(runtime.os, runtime.display_name) for runtime in helper._stacks],
[('Linux', 'NODE|26'), ('Windows', 'NODE|26')])


class TestStackRuntimeJavaSELinux(unittest.TestCase):
"""Regression tests for `az webapp list-runtimes` Linux Java SE parsing.

Expand Down