Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
92 changes: 90 additions & 2 deletions src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,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);
Expand Down Expand Up @@ -1437,7 +1470,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));
}
}
Expand All @@ -1446,7 +1479,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));
}
}
Expand All @@ -1456,5 +1489,60 @@ private void ShowEolMessage(FunctionsStacks stacks, WindowsRuntimeSettings curre
// ignore. Failure to show the EOL message should not fail the deployment.
}
}

/// <summary>
/// Determines the version of the specified worker runtime.
/// </summary>
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.
}
}
}
}
1 change: 1 addition & 0 deletions src/Cli/func/Common/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ internal static class Constants
public const string AzureDevSessionsRemoteHostName = "AzureDevSessionsRemoteHostName";
public const string AzureDevSessionsPortSuffixPlaceholder = "<port>";
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-<port>.asse.devtunnels.ms/

Expand Down
13 changes: 11 additions & 2 deletions src/Cli/func/Helpers/EolMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,21 @@ 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)
Expand Down
19 changes: 19 additions & 0 deletions src/Cli/func/Helpers/JavaHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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;
}
}
}
18 changes: 18 additions & 0 deletions src/Cli/func/Helpers/NodeJSHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Threading.Tasks;
using Azure.Functions.Cli.Common;
using Colors.Net;
using Newtonsoft.Json.Linq;
using static Azure.Functions.Cli.Common.OutputTheme;

namespace Azure.Functions.Cli.Helpers
Expand Down Expand Up @@ -58,5 +59,22 @@ 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;
}
}
}
26 changes: 26 additions & 0 deletions src/Cli/func/Helpers/PowerShellHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Azure.Functions.Cli.Common;
using System.Net.Http;
using System.Xml;
using System.Text.Json;

namespace Azure.Functions.Cli.Helpers
{
Expand Down Expand Up @@ -93,5 +94,30 @@ await RetryHelper.Retry(async () =>

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;
}
}
}
6 changes: 6 additions & 0 deletions src/Cli/func/Helpers/PythonHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -653,5 +653,11 @@ public static bool HasPySteinFile()
{
return FileSystemHelpers.FileExists(Path.Combine(Environment.CurrentDirectory, Constants.PySteinFunctionAppPy));
}

public static async Task<string> GetPythonVersion(string functionAppRoot)
{
var versionInfo = await PythonHelpers.GetVersion();
return versionInfo?.Version;
}
}
}
87 changes: 87 additions & 0 deletions src/Cli/func/Helpers/StacksApiHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,92 @@ public static bool IsInNextSixMonths(this DateTime? date)
else
return date < DateTime.Now.AddMonths(6);
}

public static T GetOtherRuntimeSettings<T>(this FunctionsStacks stacks, string workerRuntime, string runtimeVersion, Func<StackSettings, T> 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<Properties, IEnumerable<string>> 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;
}
}
}
Loading