diff --git a/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs b/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs
index 01bbe6665..fc218e203 100644
--- a/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs
+++ b/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs
@@ -209,6 +209,39 @@ public override async Task RunAsync()
// We do not change the default targetFramework if no .csproj file is found
}
+ // Show warning message for other worker runtimes (Node, Python, Powershell, Java)
+ if (workerRuntime != WorkerRuntime.Dotnet && workerRuntime != WorkerRuntime.DotnetIsolated)
+ {
+ string workerRuntimeStr = Convert.ToString(workerRuntime);
+ string runtimeVersion = GetWorkerRuntimeVersion(workerRuntime, functionAppRoot);
+
+ if (!string.IsNullOrEmpty(runtimeVersion))
+ {
+ // Get runtime stacks
+ var stacks = await AzureHelper.GetFunctionsStacks(AccessToken, ManagementURL);
+ DateTime currentDate = DateTime.Now;
+
+ object runtimeSettings = (workerRuntime == WorkerRuntime.Python) ? stacks.GetOtherRuntimeSettings(workerRuntimeStr, runtimeVersion, s => s.LinuxRuntimeSettings) : stacks.GetOtherRuntimeSettings(workerRuntimeStr, runtimeVersion, s => s.WindowsRuntimeSettings);
+
+ DateTime? eolDate = runtimeSettings switch
+ {
+ LinuxRuntimeSettings linux => linux.EndOfLifeDate,
+ WindowsRuntimeSettings windows => windows.EndOfLifeDate,
+ _ => null
+ };
+
+ if (eolDate.HasValue)
+ {
+ DateTime warningThresholdDate = eolDate.Value.AddMonths(-6);
+ if (currentDate > eolDate || currentDate >= warningThresholdDate)
+ {
+ // Show EOL warning message
+ ShowEolMessageForOtherStack(stacks, eolDate.Value, workerRuntimeStr, runtimeVersion);
+ }
+ }
+ }
+ }
+
// Check for any additional conditions or app settings that need to change
// before starting any of the publish activity.
var additionalAppSettings = await ValidateFunctionAppPublish(functionApp, workerRuntime, functionAppRoot);
@@ -1453,7 +1486,7 @@ private void ShowEolMessage(FunctionsStacks stacks, WindowsRuntimeSettings curre
var nextDotnetVersion = stacks.GetNextDotnetVersion(majorDotnetVersion.Value);
if (nextDotnetVersion != null)
{
- var warningMessage = EolMessages.GetAfterEolUpdateMessageDotNet(majorDotnetVersion.ToString(), nextDotnetVersion.ToString(), currentRuntimeSettings.EndOfLifeDate.Value);
+ var warningMessage = EolMessages.GetAfterEolUpdateMessageDotNet(majorDotnetVersion.ToString(), nextDotnetVersion.ToString(), currentRuntimeSettings.EndOfLifeDate.Value, Constants.FunctionsStackUpgrade);
ColoredConsole.WriteLine(WarningColor(warningMessage));
}
}
@@ -1462,7 +1495,7 @@ private void ShowEolMessage(FunctionsStacks stacks, WindowsRuntimeSettings curre
var nextDotnetVersion = stacks.GetNextDotnetVersion(majorDotnetVersion.Value);
if (nextDotnetVersion != null)
{
- var warningMessage = EolMessages.GetEarlyEolUpdateMessageDotNet(majorDotnetVersion.ToString(), nextDotnetVersion.ToString(), currentRuntimeSettings.EndOfLifeDate.Value);
+ var warningMessage = EolMessages.GetEarlyEolUpdateMessageDotNet(majorDotnetVersion.ToString(), nextDotnetVersion.ToString(), currentRuntimeSettings.EndOfLifeDate.Value, Constants.FunctionsStackUpgrade);
ColoredConsole.WriteLine(WarningColor(warningMessage));
}
}
@@ -1473,6 +1506,69 @@ private void ShowEolMessage(FunctionsStacks stacks, WindowsRuntimeSettings curre
}
}
+ ///
+ /// Determines the version of the specified worker runtime.
+ ///
+ private string GetWorkerRuntimeVersion(WorkerRuntime workerRuntime, string functionAppRoot)
+ {
+ switch (workerRuntime)
+ {
+ case WorkerRuntime.Node:
+ return NodeJSHelpers.GetNodeVersion(functionAppRoot);
+ case WorkerRuntime.Python:
+ return PythonHelpers.GetPythonVersion(functionAppRoot).GetAwaiter().GetResult();
+ case WorkerRuntime.Powershell:
+ return PowerShellHelper.GetPowerShellVersion(functionAppRoot);
+ case WorkerRuntime.Java:
+ return JavaHelper.GetJavaVersion(functionAppRoot);
+ default:
+ return null;
+ }
+ }
+
+ private void ShowEolMessageForOtherStack(FunctionsStacks stacks, DateTime eolDate, string workerRuntime, string runtimeVersion)
+ {
+ try
+ {
+ string nextVersion, displayName, warningMessage = string.Empty;
+ (nextVersion, displayName) = workerRuntime switch
+ {
+ var wr when wr.Equals(WorkerRuntime.Python.ToString(), StringComparison.OrdinalIgnoreCase)
+ || wr.Equals(WorkerRuntime.Powershell.ToString(), StringComparison.OrdinalIgnoreCase)
+ || wr.Equals(WorkerRuntime.Java.ToString(), StringComparison.OrdinalIgnoreCase)
+ => stacks.GetNextRuntimeVersion(
+ workerRuntime,
+ runtimeVersion,
+ properties => properties.MajorVersions
+ .SelectMany(mv => mv.MinorVersions, (major, minor) => minor.Value),
+ isNumericVersion: false),
+
+ var wr when wr.Equals(WorkerRuntime.Node.ToString(), StringComparison.OrdinalIgnoreCase)
+ => stacks.GetNextRuntimeVersion(
+ workerRuntime,
+ runtimeVersion,
+ properties => properties.MajorVersions
+ .Select(mv => mv.Value),
+ isNumericVersion: true),
+ _ => (null, workerRuntime) // Default case: No next version available
+ };
+ if (StacksApiHelper.ExpiresInNextSixMonths(eolDate))
+ {
+ warningMessage = EolMessages.GetEarlyEolUpdateMessage(displayName, runtimeVersion, nextVersion, eolDate, Constants.FunctionsStackUpgrade);
+ ColoredConsole.WriteLine(WarningColor(warningMessage));
+ }
+ else
+ {
+ warningMessage = EolMessages.GetAfterEolUpdateMessage(displayName, runtimeVersion, nextVersion, eolDate, Constants.FunctionsStackUpgrade);
+ ColoredConsole.WriteLine(WarningColor(warningMessage));
+ }
+ }
+ catch (Exception)
+ {
+ // ignore. Failure to show the EOL message should not fail the deployment.
+ }
+ }
+
// For testing
internal class AzureHelperService
{
diff --git a/src/Cli/func/Common/Constants.cs b/src/Cli/func/Common/Constants.cs
index 0ae7ea7b6..3ef6faa60 100644
--- a/src/Cli/func/Common/Constants.cs
+++ b/src/Cli/func/Common/Constants.cs
@@ -97,6 +97,7 @@ internal static partial class Constants
public const string AzureDevSessionsRemoteHostName = "AzureDevSessionsRemoteHostName";
public const string AzureDevSessionsPortSuffixPlaceholder = "";
public const string GitHubReleaseApiUrl = "https://api.github.com/repos/Azure/azure-functions-core-tools/releases/latest";
+ public const string FunctionsStackUpgrade = "https://aka.ms/FunctionsStackUpgrade";
// Sample format https://n12abc3t-.asse.devtunnels.ms/
public static readonly Dictionary> WorkerRuntimeImages = new Dictionary>
diff --git a/src/Cli/func/Helpers/EolMessages.cs b/src/Cli/func/Helpers/EolMessages.cs
index 2ea3b1644..8cff18af1 100644
--- a/src/Cli/func/Helpers/EolMessages.cs
+++ b/src/Cli/func/Helpers/EolMessages.cs
@@ -19,17 +19,27 @@ public static string GetAfterEolCreateMessageDotNet(string stackVersion, DateTim
public static string GetEarlyEolUpdateMessageDotNet(string currentStackVersion, string nextStackVersion, DateTime eol, string link = "")
{
- return $"Upgrade your app to .NET {nextStackVersion} as .NET {currentStackVersion} will reach EOL on {FormatDate(eol)} and will no longer be supported. {link}";
+ return $"Upgrade to .NET {nextStackVersion} as .NET {currentStackVersion} will reach end-of-life on {FormatDate(eol)} and will no longer be supported. Learn more: {link}";
}
public static string GetAfterEolUpdateMessageDotNet(string currentStackVersion, string nextStackVersion, DateTime eol, string link = "")
{
- return $"Upgrade your app to .NET {nextStackVersion} as .NET {currentStackVersion} has reached EOL on {FormatDate(eol)} and is no longer supported. {link}";
+ return $"Upgrade to .NET {nextStackVersion} as .NET {currentStackVersion} has reached end-of-life on {FormatDate(eol)} and is no longer supported. Learn more: {link}";
+ }
+
+ public static string GetEarlyEolUpdateMessage(string displayName, string currentStackVersion, string nextStackVersion, DateTime eol, string link = "")
+ {
+ return $"Upgrade to {displayName} {nextStackVersion} as {displayName} {currentStackVersion} will reach end-of-life on {FormatDate(eol)} and will no longer be supported. Learn more: {link}";
+ }
+
+ public static string GetAfterEolUpdateMessage(string displayName, string currentStackVersion, string nextStackVersion, DateTime eol, string link = "")
+ {
+ return $"Upgrade to {displayName} {nextStackVersion} as {displayName} {currentStackVersion} has reached end-of-life on {FormatDate(eol)} and is no longer supported. Learn more: {link}";
}
private static string FormatDate(DateTime dateTime)
{
- return dateTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
+ return dateTime.ToString(CultureInfo.CurrentCulture);
}
}
}
diff --git a/src/Cli/func/Helpers/JavaHelper.cs b/src/Cli/func/Helpers/JavaHelper.cs
new file mode 100644
index 000000000..9c0752c5f
--- /dev/null
+++ b/src/Cli/func/Helpers/JavaHelper.cs
@@ -0,0 +1,23 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+using System.Xml.Linq;
+
+namespace Azure.Functions.Cli.Helpers
+{
+ public static class JavaHelper
+ {
+ public static string GetJavaVersion(string functionAppRoot)
+ {
+ string pomXmlPath = Path.Combine(functionAppRoot, "pom.xml");
+ if (File.Exists(pomXmlPath))
+ {
+ var xmlDoc = XDocument.Load(pomXmlPath);
+ var versionElement = xmlDoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "java.version");
+ return versionElement?.Value;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Cli/func/Helpers/NodeJSHelpers.cs b/src/Cli/func/Helpers/NodeJSHelpers.cs
index 81b581e78..587e4231e 100644
--- a/src/Cli/func/Helpers/NodeJSHelpers.cs
+++ b/src/Cli/func/Helpers/NodeJSHelpers.cs
@@ -3,6 +3,7 @@
using Azure.Functions.Cli.Common;
using Colors.Net;
+using Newtonsoft.Json.Linq;
using static Azure.Functions.Cli.Common.OutputTheme;
namespace Azure.Functions.Cli.Helpers
@@ -61,5 +62,25 @@ public static async Task SetupProject(ProgrammingModel programmingModel, string
await FileSystemHelpers.WriteFileIfNotExists("tsconfig.json", await StaticResources.TsConfig);
}
}
+
+ public static string GetNodeVersion(string functionAppRoot)
+ {
+ string packageJsonPath = Path.Combine(functionAppRoot, "package.json");
+ if (!File.Exists(packageJsonPath))
+ {
+ return null;
+ }
+
+ var packageJson = JObject.Parse(File.ReadAllText(packageJsonPath));
+
+ // Check if "engines" field specifies Node.js version
+ string nodeVersion = packageJson["engines"]?["node"]?.ToString();
+ if (!string.IsNullOrEmpty(nodeVersion))
+ {
+ return nodeVersion;
+ }
+
+ return null;
+ }
}
}
diff --git a/src/Cli/func/Helpers/PowerShellHelper.cs b/src/Cli/func/Helpers/PowerShellHelper.cs
index 612608b0a..97e7d1615 100644
--- a/src/Cli/func/Helpers/PowerShellHelper.cs
+++ b/src/Cli/func/Helpers/PowerShellHelper.cs
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
+using System.Text.Json;
using System.Xml;
using Azure.Functions.Cli.Common;
@@ -95,5 +96,32 @@ await RetryHelper.Retry(
return latestMajorVersion;
}
+
+ public static string GetPowerShellVersion(string functionAppRoot)
+ {
+ // Check environment variable (for Azure)
+ string runtimeVersion = Environment.GetEnvironmentVariable("FUNCTIONS_WORKER_RUNTIME_VERSION");
+ if (!string.IsNullOrEmpty(runtimeVersion))
+ {
+ return runtimeVersion;
+ }
+
+ // Fallback: Check local.settings.json (for local development)
+ string settingsPath = Path.Combine(functionAppRoot, "local.settings.json");
+ if (File.Exists(settingsPath))
+ {
+ var jsonText = File.ReadAllText(settingsPath);
+ using (JsonDocument doc = JsonDocument.Parse(jsonText))
+ {
+ if (doc.RootElement.TryGetProperty("Values", out JsonElement values) &&
+ values.TryGetProperty("FUNCTIONS_WORKER_RUNTIME_VERSION", out JsonElement versionElement))
+ {
+ return versionElement.GetString();
+ }
+ }
+ }
+
+ return null;
+ }
}
}
diff --git a/src/Cli/func/Helpers/PythonHelpers.cs b/src/Cli/func/Helpers/PythonHelpers.cs
index 2afb69abe..790a28c51 100644
--- a/src/Cli/func/Helpers/PythonHelpers.cs
+++ b/src/Cli/func/Helpers/PythonHelpers.cs
@@ -692,5 +692,11 @@ public static bool HasPySteinFile()
{
return FileSystemHelpers.FileExists(Path.Combine(Environment.CurrentDirectory, Constants.PySteinFunctionAppPy));
}
+
+ public static async Task GetPythonVersion(string functionAppRoot)
+ {
+ var versionInfo = await PythonHelpers.GetVersion();
+ return versionInfo?.Version;
+ }
}
}
diff --git a/src/Cli/func/Helpers/StacksApiHelper.cs b/src/Cli/func/Helpers/StacksApiHelper.cs
index 2bb44bcfc..c699704b1 100644
--- a/src/Cli/func/Helpers/StacksApiHelper.cs
+++ b/src/Cli/func/Helpers/StacksApiHelper.cs
@@ -60,5 +60,100 @@ public static bool IsInNextSixMonths(this DateTime? date)
return date < DateTime.Now.AddMonths(6);
}
}
+
+ public static T GetOtherRuntimeSettings(this FunctionsStacks stacks, string workerRuntime, string runtimeVersion, Func settingsSelector)
+ {
+ if (WorkerRuntime.Java.ToString() == workerRuntime)
+ {
+ if (runtimeVersion.StartsWith("1."))
+ {
+ runtimeVersion = runtimeVersion.Substring(2); // Removes "1."
+ }
+ }
+
+ var languageStack = stacks?.Languages
+ .FirstOrDefault(x => x.Name.Equals(workerRuntime, StringComparison.InvariantCultureIgnoreCase));
+
+ var majorVersion = languageStack?.Properties.MajorVersions?
+ .FirstOrDefault(mv => runtimeVersion.StartsWith(mv.Value, StringComparison.InvariantCultureIgnoreCase));
+
+ var minorVersion = majorVersion?.MinorVersions?
+ .FirstOrDefault(mv => runtimeVersion.StartsWith(mv.Value, StringComparison.InvariantCultureIgnoreCase))
+ ?? majorVersion?.MinorVersions?.LastOrDefault();
+
+ return settingsSelector(minorVersion?.StackSettings);
+ }
+
+ public static (string NextVersion, string DisplayText) GetNextRuntimeVersion(
+ this FunctionsStacks stacks,
+ string workerRuntime,
+ string currentRuntimeVersion,
+ Func> versionSelector,
+ bool isNumericVersion = false) // Handle Node.js separately as it has integer versions
+ {
+ var runtimeStack = stacks?.Languages
+ .FirstOrDefault(x => x.Name.Equals(workerRuntime, StringComparison.InvariantCultureIgnoreCase));
+ if (runtimeStack?.Properties == null)
+ {
+ return (null, null); // No matching runtime found
+ }
+
+ string displayName = runtimeStack.Properties.DisplayText;
+
+ // Extract and sort supported versions using the provided selector function
+ var supportedVersions = versionSelector(runtimeStack.Properties)?
+ .Where(v => !string.IsNullOrEmpty(v))
+ .ToList();
+ if (supportedVersions == null || supportedVersions.Count == 0)
+ {
+ return (null, displayName); // No valid versions found
+ }
+
+ if (isNumericVersion)
+ {
+ // Special case for Node.js: Versions are integers
+ var numericVersions = supportedVersions
+ .Select(v => int.TryParse(v, out int version) ? version : (int?)null)
+ .Where(v => v.HasValue)
+ .OrderByDescending(v => v)
+ .ToList();
+ if (!int.TryParse(currentRuntimeVersion, out int currentMajorVersion))
+ {
+ return (null, displayName); // Invalid current version
+ }
+
+ var nextVersion = numericVersions.FirstOrDefault(v => v > currentMajorVersion);
+ return ((nextVersion ?? numericVersions.First()).ToString(), displayName);
+ }
+ else
+ {
+ // Standard versioning (Python, Java, PowerShell)
+ var parsedVersions = supportedVersions
+ .Where(v => Version.TryParse(v, out _))
+ .Select(v => Version.Parse(v))
+ .OrderByDescending(v => v)
+ .ToList();
+ if (!Version.TryParse(currentRuntimeVersion, out Version currentVersion))
+ {
+ return (null, displayName); // Invalid current version
+ }
+
+ var nextVersion = parsedVersions.FirstOrDefault(v => v > currentVersion);
+ return ((nextVersion ?? parsedVersions.First()).ToString(), displayName);
+ }
+ }
+
+ public static bool ExpiresInNextSixMonths(this DateTime? date)
+ {
+ if (!date.HasValue)
+ {
+ return false; // Null check
+ }
+
+ DateTime currentDate = DateTime.UtcNow;
+ DateTime sixMonthsFromNow = currentDate.AddMonths(6);
+
+ return currentDate <= date.Value && date.Value <= sixMonthsFromNow;
+ }
}
}
diff --git a/test/Azure.Functions.Cli.Tests/TestsToMigrate/PublishActionTests.cs b/test/Azure.Functions.Cli.Tests/TestsToMigrate/PublishActionTests.cs
index dfdcce2db..496f991c1 100644
--- a/test/Azure.Functions.Cli.Tests/TestsToMigrate/PublishActionTests.cs
+++ b/test/Azure.Functions.Cli.Tests/TestsToMigrate/PublishActionTests.cs
@@ -8,6 +8,7 @@
using Azure.Functions.Cli.Arm.Models;
using Azure.Functions.Cli.Common;
using Azure.Functions.Cli.Helpers;
+using Azure.Functions.Cli.StacksApi;
using Xunit;
using static Azure.Functions.Cli.Actions.AzureActions.PublishFunctionAppAction;
@@ -189,6 +190,227 @@ public void ValidateFunctionAppPublish_ThrowException_WhenWorkerRuntimeIsNone()
Assert.Equal($"Worker runtime cannot be '{WorkerRuntime.None}'. Please set a valid runtime.", ex.Message);
}
+ private FunctionsStacks GetMockFunctionStacks()
+ {
+ return new FunctionsStacks
+ {
+ Languages = new List
+ {
+ new Language
+ {
+ Name = "python",
+ Properties = new Properties
+ {
+ DisplayText = "Python",
+ MajorVersions = new List
+ {
+ new MajorVersion
+ {
+ Value = "3",
+ MinorVersions = new List
+ {
+ new MinorVersion
+ {
+ Value = "3.8",
+ StackSettings = new StackSettings
+ {
+ LinuxRuntimeSettings = new LinuxRuntimeSettings
+ {
+ RuntimeVersion = "Python|3.8"
+ }
+ }
+ },
+ new MinorVersion
+ {
+ Value = "3.12",
+ StackSettings = new StackSettings
+ {
+ LinuxRuntimeSettings = new LinuxRuntimeSettings
+ {
+ RuntimeVersion = "Python|3.12"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ new Language
+ {
+ Name = "node",
+Properties = new Properties
+{
+ DisplayText = "Node.js",
+ MajorVersions = new List
+ {
+ new MajorVersion
+ {
+ Value = "14",
+ MinorVersions = new List
+ {
+ new MinorVersion
+ {
+ Value = "14.17",
+ StackSettings = new StackSettings
+ {
+ WindowsRuntimeSettings = new WindowsRuntimeSettings
+ {
+ RuntimeVersion = "Node|14.17"
+ }
+ }
+ },
+ new MinorVersion
+ {
+ Value = "14.20 LTS", // Ensure an LTS version exists
+ StackSettings = new StackSettings
+ {
+ WindowsRuntimeSettings = new WindowsRuntimeSettings
+ {
+ RuntimeVersion = "Node|14.20 LTS"
+ }
+ }
+ }
+ }
+ },
+ new MajorVersion
+ {
+ Value = "22",
+ MinorVersions = new List
+ {
+ new MinorVersion
+ {
+ Value = "22.0",
+ StackSettings = new StackSettings
+ {
+ WindowsRuntimeSettings = new WindowsRuntimeSettings
+ {
+ RuntimeVersion = "Node|22.0"
+ }
+ }
+ },
+ new MinorVersion
+ {
+ Value = "22.0 LTS", // Ensure an LTS version exists
+ StackSettings = new StackSettings
+ {
+ WindowsRuntimeSettings = new WindowsRuntimeSettings
+ {
+ RuntimeVersion = "Node|22.0 LTS"
+ }
+ }
+ }
+ }
+ }
+ }
+}
+ },
+ new Language
+{
+ Name = "powershell",
+ Properties = new Properties
+ {
+ DisplayText = "PowerShell",
+ MajorVersions = new List
+ {
+ new MajorVersion
+ {
+ Value = "7",
+ MinorVersions = new List
+ {
+ new MinorVersion
+ {
+ Value = "7",
+ StackSettings = new StackSettings
+ {
+ WindowsRuntimeSettings = new WindowsRuntimeSettings
+ {
+ RuntimeVersion = "PowerShell|7"
+ }
+ }
+ },
+ new MinorVersion
+ {
+ Value = "7.2 LTS",
+ StackSettings = new StackSettings
+ {
+ WindowsRuntimeSettings = new WindowsRuntimeSettings
+ {
+ RuntimeVersion = "PowerShell|7.2 LTS"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+ }
+ };
+ }
+
+ [Theory]
+ [InlineData("node", "14", "22")] // Node.js 14 should return next supported 22
+ public void GetNextRuntimeNodeVersion_ShouldReturnCorrectVersion(string runtime, string currentVersion, string expectedNextVersion)
+ {
+ // Arrange
+ var stacks = GetMockFunctionStacks();
+ // Act
+ var (nextVersion, _) = stacks.GetNextRuntimeVersion(runtime, currentVersion, p => p.MajorVersions.Select(mv => mv.Value), isNumericVersion: true);
+ // Assert
+ Assert.Equal(expectedNextVersion, nextVersion);
+ }
+
+ [Theory]
+ [InlineData("python", "3.8", "3.12")] // Python 3.8 should return next supported 3.12
+ public void GetNextRuntimePythonVersion_ShouldReturnCorrectVersion(string runtime, string currentVersion, string expectedNextVersion)
+ {
+ // Arrange
+ var stacks = GetMockFunctionStacks();
+ // Act
+ var (nextVersion, _) = stacks.GetNextRuntimeVersion(runtime, currentVersion, p => p.MajorVersions.SelectMany(mv => mv.MinorVersions, (major, minor) => minor.Value));
+ // Assert
+ Assert.Equal(expectedNextVersion, nextVersion);
+ }
+
+ [Theory]
+ [InlineData("node", "14.17", true)] // Test for a known valid version
+ [InlineData("node", "14.20 LTS", true)] // Test for an LTS version
+ public void GetRuntimeSettingsForNode_ShouldReturnValidSettings(string runtime, string version, bool expectedNotNull)
+ {
+ // Arrange
+ var stacks = GetMockFunctionStacks();
+ // Act
+ var settings = stacks.GetOtherRuntimeSettings(runtime, version, s => s.WindowsRuntimeSettings);
+ // Assert
+ Assert.Equal(expectedNotNull, settings != null);
+ }
+
+ [Theory]
+ [InlineData("python", "3.8", true)] // Python 3.8 should return runtime settings
+ public void GetRuntimeSettingsForPython_ShouldReturnValidSettings(string runtime, string version, bool expectedNotNull)
+ {
+ // Arrange
+ var stacks = GetMockFunctionStacks();
+ // Act
+ var settings = stacks.GetOtherRuntimeSettings(runtime, version, s => s.LinuxRuntimeSettings);
+ // Assert
+ Assert.Equal(expectedNotNull, settings != null);
+ }
+
+ [Theory]
+ [InlineData("powershell", "7", true)] // PowerShell 7 should return runtime settings
+ [InlineData("powershell", "7.2 LTS", true)] // PowerShell 7.2 LTS should return runtime settings
+ public void GetRuntimeSettingsForPowerShell_ShouldReturnValidSettings(string runtime, string version, bool expectedNotNull)
+ {
+ // Arrange
+ var stacks = GetMockFunctionStacks();
+ // Act
+ var settings = stacks.GetOtherRuntimeSettings(runtime, version, s => s.WindowsRuntimeSettings);
+ // Assert
+ Assert.Equal(expectedNotNull, settings != null);
+ }
+
private class TestAzureHelperService : AzureHelperService
{
public Dictionary UpdatedSettings { get; private set; }
@@ -204,5 +426,49 @@ public override Task> UpdateWebSettings(Site function
return Task.FromResult(new HttpResult(string.Empty));
}
}
+
+ [Theory]
+ [InlineData("node", "22", "22")] // Node.js 22 is highest → should return itself
+ [InlineData("python", "3.12", "3.12")] // Python 3.12 is highest → should return itself
+ public void GetNextRuntimeVersion_ShouldReturnCurrentIfNoNewerExists(string runtime, string currentVersion, string expectedVersion)
+ {
+ // Arrange
+ var stacks = GetMockFunctionStacks();
+ var selector = runtime == "node"
+ ? (Func>)(p => p.MajorVersions.Select(mv => mv.Value))
+ : p => p.MajorVersions.SelectMany(mv => mv.MinorVersions.Select(minor => minor.Value));
+ bool isNumeric = runtime == "node";
+ // Act
+ var (nextVersion, _) = stacks.GetNextRuntimeVersion(runtime, currentVersion, selector, isNumeric);
+ // Assert
+ Assert.Equal(expectedVersion, nextVersion);
+ }
+
+ [Fact]
+ public void GetNextRuntimeVersion_ShouldReturnNull_WhenNoVersionsAvailable()
+ {
+ // Arrange
+ var stacks = new FunctionsStacks
+ {
+ Languages = new List
+ {
+ new Language
+ {
+ Name = "java",
+ Properties = new Properties
+ {
+ DisplayText = "Java",
+ MajorVersions = new List() // No versions
+ }
+ }
+ }
+ };
+ // Act
+ var (nextVersion, displayName) = stacks.GetNextRuntimeVersion(
+ "java", "11", p => p.MajorVersions.Select(mv => mv.Value), isNumericVersion: true);
+ // Assert
+ Assert.Null(nextVersion);
+ Assert.Equal("Java", displayName);
+ }
}
}
diff --git a/test/Cli/Func.Unit.Tests/ActionsTests/PublishActionTests.cs b/test/Cli/Func.Unit.Tests/ActionsTests/PublishActionTests.cs
new file mode 100644
index 000000000..fdc38a48a
--- /dev/null
+++ b/test/Cli/Func.Unit.Tests/ActionsTests/PublishActionTests.cs
@@ -0,0 +1,316 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Azure.Functions.Cli.Actions.AzureActions;
+using Azure.Functions.Cli.Arm.Models;
+using Azure.Functions.Cli.Common;
+using Azure.Functions.Cli.Helpers;
+using Azure.Functions.Cli.StacksApi;
+using Xunit;
+using static Azure.Functions.Cli.Actions.AzureActions.PublishFunctionAppAction;
+
+namespace Azure.Functions.Cli.Unit.Test.ActionsTests
+{
+ public class PublishActionTests
+ {
+ private FunctionsStacks GetMockFunctionStacks()
+ {
+ return new FunctionsStacks
+ {
+ Languages = new List
+ {
+ new Language
+ {
+ Name = "python",
+ Properties = new Properties
+ {
+ DisplayText = "Python",
+ MajorVersions = new List
+ {
+ new MajorVersion
+ {
+ Value = "3",
+ MinorVersions = new List
+ {
+ new MinorVersion
+ {
+ Value = "3.8",
+ StackSettings = new StackSettings
+ {
+ LinuxRuntimeSettings = new LinuxRuntimeSettings
+ {
+ RuntimeVersion = "Python|3.8"
+ }
+ }
+ },
+ new MinorVersion
+ {
+ Value = "3.12",
+ StackSettings = new StackSettings
+ {
+ LinuxRuntimeSettings = new LinuxRuntimeSettings
+ {
+ RuntimeVersion = "Python|3.12"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ new Language
+ {
+ Name = "node",
+ Properties = new Properties
+{
+ DisplayText = "Node.js",
+ MajorVersions = new List
+ {
+ new MajorVersion
+ {
+ Value = "14",
+ MinorVersions = new List
+ {
+ new MinorVersion
+ {
+ Value = "14.17",
+ StackSettings = new StackSettings
+ {
+ WindowsRuntimeSettings = new WindowsRuntimeSettings
+ {
+ RuntimeVersion = "Node|14.17"
+ }
+ }
+ },
+ new MinorVersion
+ {
+ Value = "14.20 LTS", // Ensure an LTS version exists
+ StackSettings = new StackSettings
+ {
+ WindowsRuntimeSettings = new WindowsRuntimeSettings
+ {
+ RuntimeVersion = "Node|14.20 LTS"
+ }
+ }
+ }
+ }
+ },
+ new MajorVersion
+ {
+ Value = "22",
+ MinorVersions = new List
+ {
+ new MinorVersion
+ {
+ Value = "22.0",
+ StackSettings = new StackSettings
+ {
+ WindowsRuntimeSettings = new WindowsRuntimeSettings
+ {
+ RuntimeVersion = "Node|22.0"
+ }
+ }
+ },
+ new MinorVersion
+ {
+ Value = "22.0 LTS", // Ensure an LTS version exists
+ StackSettings = new StackSettings
+ {
+ WindowsRuntimeSettings = new WindowsRuntimeSettings
+ {
+ RuntimeVersion = "Node|22.0 LTS"
+ }
+ }
+ }
+ }
+ }
+ }
+}
+ },
+ new Language
+{
+ Name = "powershell",
+ Properties = new Properties
+ {
+ DisplayText = "PowerShell",
+ MajorVersions = new List
+ {
+ new MajorVersion
+ {
+ Value = "7",
+ MinorVersions = new List
+ {
+ new MinorVersion
+ {
+ Value = "7",
+ StackSettings = new StackSettings
+ {
+ WindowsRuntimeSettings = new WindowsRuntimeSettings
+ {
+ RuntimeVersion = "PowerShell|7"
+ }
+ }
+ },
+ new MinorVersion
+ {
+ Value = "7.2 LTS",
+ StackSettings = new StackSettings
+ {
+ WindowsRuntimeSettings = new WindowsRuntimeSettings
+ {
+ RuntimeVersion = "PowerShell|7.2 LTS"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+ }
+ };
+ }
+
+ [Theory]
+ [InlineData("node", "14", "22")] // Node.js 14 should return next supported 22
+ public void GetNextRuntimeNodeVersion_ShouldReturnCorrectVersion(string runtime, string currentVersion, string expectedNextVersion)
+ {
+ // Arrange
+ var stacks = GetMockFunctionStacks();
+
+ // Act
+ var (nextVersion, _) = stacks.GetNextRuntimeVersion(runtime, currentVersion, p => p.MajorVersions.Select(mv => mv.Value), isNumericVersion: true);
+
+ // Assert
+ Assert.Equal(expectedNextVersion, nextVersion);
+ }
+
+ [Theory]
+ [InlineData("python", "3.8", "3.12")] // Python 3.8 should return next supported 3.12
+ public void GetNextRuntimePythonVersion_ShouldReturnCorrectVersion(string runtime, string currentVersion, string expectedNextVersion)
+ {
+ // Arrange
+ var stacks = GetMockFunctionStacks();
+
+ // Act
+ var (nextVersion, _) = stacks.GetNextRuntimeVersion(runtime, currentVersion, p => p.MajorVersions.SelectMany(mv => mv.MinorVersions, (major, minor) => minor.Value));
+
+ // Assert
+ Assert.Equal(expectedNextVersion, nextVersion);
+ }
+
+ [Theory]
+ [InlineData("node", "14.17", true)] // Test for a known valid version
+ [InlineData("node", "14.20 LTS", true)] // Test for an LTS version
+ public void GetRuntimeSettingsForNode_ShouldReturnValidSettings(string runtime, string version, bool expectedNotNull)
+ {
+ // Arrange
+ var stacks = GetMockFunctionStacks();
+
+ // Act
+ var settings = stacks.GetOtherRuntimeSettings(runtime, version, s => s.WindowsRuntimeSettings);
+
+ // Assert
+ Assert.Equal(expectedNotNull, settings != null);
+ }
+
+ [Theory]
+ [InlineData("python", "3.8", true)] // Python 3.8 should return runtime settings
+ public void GetRuntimeSettingsForPython_ShouldReturnValidSettings(string runtime, string version, bool expectedNotNull)
+ {
+ // Arrange
+ var stacks = GetMockFunctionStacks();
+
+ // Act
+ var settings = stacks.GetOtherRuntimeSettings(runtime, version, s => s.LinuxRuntimeSettings);
+
+ // Assert
+ Assert.Equal(expectedNotNull, settings != null);
+ }
+
+ [Theory]
+ [InlineData("powershell", "7", true)] // PowerShell 7 should return runtime settings
+ [InlineData("powershell", "7.2 LTS", true)] // PowerShell 7.2 LTS should return runtime settings
+ public void GetRuntimeSettingsForPowerShell_ShouldReturnValidSettings(string runtime, string version, bool expectedNotNull)
+ {
+ // Arrange
+ var stacks = GetMockFunctionStacks();
+
+ // Act
+ var settings = stacks.GetOtherRuntimeSettings(runtime, version, s => s.WindowsRuntimeSettings);
+
+ // Assert
+ Assert.Equal(expectedNotNull, settings != null);
+ }
+
+ [Theory]
+ [InlineData("node", "22", "22")] // Node.js 22 is highest → should return itself
+ [InlineData("python", "3.12", "3.12")] // Python 3.12 is highest → should return itself
+ public void GetNextRuntimeVersion_ShouldReturnCurrentIfNoNewerExists(string runtime, string currentVersion, string expectedVersion)
+ {
+ // Arrange
+ var stacks = GetMockFunctionStacks();
+ var selector = runtime == "node"
+ ? (Func>)(p => p.MajorVersions.Select(mv => mv.Value))
+ : p => p.MajorVersions.SelectMany(mv => mv.MinorVersions.Select(minor => minor.Value));
+ bool isNumeric = runtime == "node";
+
+ // Act
+ var (nextVersion, _) = stacks.GetNextRuntimeVersion(runtime, currentVersion, selector, isNumeric);
+
+ // Assert
+ Assert.Equal(expectedVersion, nextVersion);
+ }
+
+ [Fact]
+ public void GetNextRuntimeVersion_ShouldReturnNull_WhenNoVersionsAvailable()
+ {
+ // Arrange
+ var stacks = new FunctionsStacks
+ {
+ Languages = new List
+ {
+ new Language
+ {
+ Name = "java",
+ Properties = new Properties
+ {
+ DisplayText = "Java",
+ MajorVersions = new List() // No versions
+ }
+ }
+ }
+ };
+
+ // Act
+ var (nextVersion, displayName) = stacks.GetNextRuntimeVersion(
+ "java", "11", p => p.MajorVersions.Select(mv => mv.Value), isNumericVersion: true);
+
+ // Assert
+ Assert.Null(nextVersion);
+ Assert.Equal("Java", displayName);
+ }
+
+ private class TestAzureHelperService : AzureHelperService
+ {
+ public TestAzureHelperService()
+ : base(null, null)
+ {
+ UpdatedSettings = new Dictionary();
+ }
+
+ public Dictionary UpdatedSettings { get; private set; }
+
+ public override Task> UpdateWebSettings(Site functionApp, Dictionary updatedSettings)
+ {
+ UpdatedSettings = updatedSettings;
+ return Task.FromResult(new HttpResult(string.Empty));
+ }
+ }
+ }
+}