Skip to content

Commit 0faa5fe

Browse files
Copilotaishwaryabh
andauthored
Implement basic validations for each runtime during func pack (#4638)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: aishwaryabh <37918412+aishwaryabh@users.noreply.github.com> Co-authored-by: Aishwarya Bhandari <aibhandari@microsoft.com>
1 parent ba7d29c commit 0faa5fe

19 files changed

+853
-57
lines changed

release_notes.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,9 @@
88

99
#### Changes
1010
- Add `func pack` basic functionality (#4600)
11-
- Clean up HelpAction and add `func pack` to help menu (#4639)
11+
- Clean up HelpAction and add `func pack` to help menu (#4639)
12+
- Add comprehensive pack validations for all Azure Functions runtimes (#4625)
13+
- Enhanced user experience with real-time validation status (PASSED/FAILED/WARNING)
14+
- Runtime-specific validations for .NET, Python, Node.js, PowerShell, and Custom Handlers
15+
- Actionable error messages to help developers resolve issues during packaging
16+
- Validates folder structure, required files, programming models, and runtime-specific configurations

src/Cli/func/Actions/LocalActions/PackAction/CustomPackSubcommandAction.cs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

4+
using Azure.Functions.Cli.Common;
45
using Azure.Functions.Cli.Interfaces;
56
using Fclp;
7+
using Newtonsoft.Json;
8+
using Newtonsoft.Json.Linq;
69

710
namespace Azure.Functions.Cli.Actions.LocalActions.PackAction
811
{
@@ -19,6 +22,70 @@ public async Task RunAsync(PackOptions packOptions)
1922
await ExecuteAsync(packOptions);
2023
}
2124

25+
protected internal override void ValidateFunctionApp(string functionAppRoot, PackOptions options)
26+
{
27+
var validations = new List<Action<string>>
28+
{
29+
dir =>
30+
{
31+
// Validate custom handler configuration and executable
32+
try
33+
{
34+
var hostJsonPath = Path.Combine(dir, Constants.HostJsonFileName);
35+
var hostJsonContent = FileSystemHelpers.ReadAllTextFromFileAsync(hostJsonPath).Result;
36+
var hostConfig = JObject.Parse(hostJsonContent);
37+
var customHandler = hostConfig["customHandler"];
38+
var validateCustomHandlerTitle = "Validate Custom Handler Configuration";
39+
var configWarning = "No custom handler configuration found in host.json. Please visit https://aka.ms/custom-handler-host-json" +
40+
" to view examples on how to configure the app.";
41+
42+
if (customHandler is null)
43+
{
44+
PackValidationHelper.DisplayValidationWarning(
45+
validateCustomHandlerTitle,
46+
configWarning);
47+
return;
48+
}
49+
50+
var description = customHandler["description"];
51+
if (description is null)
52+
{
53+
PackValidationHelper.DisplayValidationWarning(
54+
validateCustomHandlerTitle,
55+
configWarning);
56+
return;
57+
}
58+
59+
var defaultExecutablePath = description["defaultExecutablePath"]?.ToString();
60+
if (string.IsNullOrEmpty(defaultExecutablePath))
61+
{
62+
PackValidationHelper.DisplayValidationWarning(validateCustomHandlerTitle, "No defaultExecutablePath specified in host.json");
63+
return;
64+
}
65+
66+
var executablePath = Path.Combine(dir, defaultExecutablePath);
67+
var executableExists = FileSystemHelpers.FileExists(executablePath);
68+
if (!executableExists)
69+
{
70+
PackValidationHelper.DisplayValidationWarning(
71+
validateCustomHandlerTitle,
72+
$"Custom handler executable '{defaultExecutablePath}' not found. Ensure the executable exists.");
73+
return;
74+
}
75+
76+
// If we get to this point, validation has succeeded
77+
PackValidationHelper.DisplayValidationResult(validateCustomHandlerTitle, true);
78+
}
79+
catch (Exception ex)
80+
{
81+
PackValidationHelper.DisplayValidationEnd();
82+
throw new CliException($"Could not parse host.json to validate custom handler configuration: {ex.Message}");
83+
}
84+
}
85+
};
86+
PackValidationHelper.RunValidations(functionAppRoot, validations);
87+
}
88+
2289
protected override Task<string> GetPackingRootAsync(string functionAppRoot, PackOptions options)
2390
{
2491
// Custom worker packs from the function app root without extra steps

src/Cli/func/Actions/LocalActions/PackAction/DotnetPackSubcommandAction.cs

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

4-
using System.IO;
54
using Azure.Functions.Cli.Common;
65
using Azure.Functions.Cli.Helpers;
7-
using Azure.Functions.Cli.Interfaces;
86
using Colors.Net;
97
using Fclp;
108
using static Azure.Functions.Cli.Common.OutputTheme;
@@ -14,11 +12,11 @@ namespace Azure.Functions.Cli.Actions.LocalActions.PackAction
1412
[Action(Name = "pack dotnet", ParentCommandName = "pack", ShowInHelp = false, HelpText = "Arguments specific to .NET apps when running func pack")]
1513
internal class DotnetPackSubcommandAction : PackSubcommandAction
1614
{
17-
private readonly ISecretsManager _secretsManager;
15+
private readonly bool _isDotnetIsolated;
1816

19-
public DotnetPackSubcommandAction(ISecretsManager secretsManager)
17+
public DotnetPackSubcommandAction(bool isDotnetIsolated)
2018
{
21-
_secretsManager = secretsManager;
19+
_isDotnetIsolated = isDotnetIsolated;
2220
}
2321

2422
public override ICommandLineParserResult ParseArgs(string[] args)
@@ -38,16 +36,70 @@ public override Task RunAsync()
3836
return Task.CompletedTask;
3937
}
4038

41-
protected override void ValidateFunctionApp(string functionAppRoot, PackOptions options)
39+
protected internal override void ValidateFunctionApp(string functionAppRoot, PackOptions options)
4240
{
43-
var requiredFiles = new[] { "host.json" };
41+
var validations = new List<Action<string>>();
42+
43+
// .NET isolated: validate folder structure if --no-build
44+
if (options.NoBuild && _isDotnetIsolated)
45+
{
46+
validations.Add(dir => RunDotnetIsolatedFolderStructureValidation(dir));
47+
}
48+
49+
PackValidationHelper.RunValidations(functionAppRoot, validations);
50+
}
51+
52+
internal static bool ValidateDotnetIsolatedFolderStructure(string directory, out string errorMessage)
53+
{
54+
errorMessage = string.Empty;
55+
56+
if (string.IsNullOrWhiteSpace(directory))
57+
{
58+
errorMessage = "Deployment directory path not specified.";
59+
return false;
60+
}
61+
62+
// Required artifacts
63+
var requiredFiles = new[] { "functions.metadata" };
64+
var requiredDirectories = new[] { ".azurefunctions" };
65+
66+
// Validate files
4467
foreach (var file in requiredFiles)
4568
{
46-
if (!FileSystemHelpers.FileExists(Path.Combine(functionAppRoot, file)))
69+
var filePath = Path.Combine(directory, file);
70+
if (!FileSystemHelpers.FileExists(filePath))
71+
{
72+
errorMessage = $"Required file '{file}' not found in deployment structure. Ensure 'dotnet publish' has been run.";
73+
return false;
74+
}
75+
}
76+
77+
// Validate directories
78+
foreach (var dir in requiredDirectories)
79+
{
80+
var dirPath = Path.Combine(directory, dir);
81+
if (!FileSystemHelpers.DirectoryExists(dirPath))
4782
{
48-
throw new CliException($"Required file '{file}' not found in build output directory: {functionAppRoot}");
83+
errorMessage = $"Required directory '{dir}' not found in deployment structure. Ensure 'dotnet publish' has been run.";
84+
return false;
4985
}
5086
}
87+
88+
return true;
89+
}
90+
91+
private static void RunDotnetIsolatedFolderStructureValidation(string directory)
92+
{
93+
var isValidStructure = ValidateDotnetIsolatedFolderStructure(directory, out string errorMessage);
94+
PackValidationHelper.DisplayValidationResult(
95+
"Validate Folder Structure",
96+
isValidStructure,
97+
isValidStructure ? null : errorMessage);
98+
if (!isValidStructure)
99+
{
100+
PackValidationHelper.DisplayValidationEnd();
101+
throw new CliException(errorMessage);
102+
}
51103
}
52104

53105
protected override async Task<string> GetPackingRootAsync(string functionAppRoot, PackOptions options)
@@ -121,6 +173,24 @@ private async Task RunDotNetPublish(string functionAppRoot)
121173
{
122174
throw new CliException("Error publishing .NET project");
123175
}
176+
177+
if (_isDotnetIsolated)
178+
{
179+
// Validate the published structure
180+
ColoredConsole.WriteLine();
181+
ColoredConsole.WriteLine("Validating published output...");
182+
183+
var isValidStructure = ValidateDotnetIsolatedFolderStructure(outputPath, out string errorMessage);
184+
PackValidationHelper.DisplayValidationResult(
185+
"Validate Published Structure",
186+
isValidStructure,
187+
isValidStructure ? null : errorMessage);
188+
189+
if (!isValidStructure)
190+
{
191+
throw new CliException($"Published output validation failed: {errorMessage}");
192+
}
193+
}
124194
}
125195
}
126196
}

src/Cli/func/Actions/LocalActions/PackAction/NodePackSubcommandAction.cs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,13 @@ protected override void ParseSubcommandArgs(string[] args)
4545
ParseArgs(args);
4646
}
4747

48-
protected override void ValidateFunctionApp(string functionAppRoot, PackOptions options)
48+
protected internal override void ValidateFunctionApp(string functionAppRoot, PackOptions options)
4949
{
50-
// ValidateFunctionApp package.json exists
51-
var packageJsonPath = Path.Combine(functionAppRoot, "package.json");
52-
if (!FileSystemHelpers.FileExists(packageJsonPath))
50+
var validations = new List<Action<string>>
5351
{
54-
throw new CliException($"package.json not found in {functionAppRoot}. This is required for Node.js function apps.");
55-
}
56-
57-
if (StaticSettings.IsDebug)
58-
{
59-
ColoredConsole.WriteLine(VerboseColor($"Found package.json at {packageJsonPath}"));
60-
}
52+
dir => PackValidationHelper.RunRequiredFilesValidation(dir, new[] { "package.json", "host.json" }, "Validate Folder Structure")
53+
};
54+
PackValidationHelper.RunValidations(functionAppRoot, validations);
6155
}
6256

6357
protected override async Task<string> GetPackingRootAsync(string functionAppRoot, PackOptions options)

src/Cli/func/Actions/LocalActions/PackAction/PackAction.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ public override async Task RunAsync()
110110
private async Task RunRuntimeSpecificPackAsync(WorkerRuntime runtime, PackOptions packOptions) =>
111111
await (runtime switch
112112
{
113-
WorkerRuntime.Dotnet or WorkerRuntime.DotnetIsolated => new DotnetPackSubcommandAction(_secretsManager).RunAsync(packOptions),
114-
WorkerRuntime.Python => new PythonPackSubcommandAction(_secretsManager).RunAsync(packOptions, Args),
113+
WorkerRuntime.Dotnet or WorkerRuntime.DotnetIsolated => new DotnetPackSubcommandAction(runtime is WorkerRuntime.DotnetIsolated).RunAsync(packOptions),
114+
WorkerRuntime.Python => new PythonPackSubcommandAction().RunAsync(packOptions, Args),
115115
WorkerRuntime.Node => new NodePackSubcommandAction(_secretsManager).RunAsync(packOptions, Args),
116116
WorkerRuntime.Powershell => new PowershellPackSubcommandAction().RunAsync(packOptions),
117117
WorkerRuntime.Custom => new CustomPackSubcommandAction().RunAsync(packOptions),

src/Cli/func/Actions/LocalActions/PackAction/PackHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ internal static class PackHelpers
1515
public static string ResolveFunctionAppRoot(string folderPath)
1616
{
1717
return string.IsNullOrEmpty(folderPath)
18-
? ScriptHostHelpers.GetFunctionAppRootDirectory(Environment.CurrentDirectory)
18+
? Environment.CurrentDirectory
1919
: Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, folderPath));
2020
}
2121

src/Cli/func/Actions/LocalActions/PackAction/PackSubcommandAction.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ protected virtual void ParseSubcommandArgs(string[] args)
4444
}
4545

4646
// Hook: optional validation prior to determining packing root
47-
protected virtual void ValidateFunctionApp(string functionAppRoot, PackOptions options)
47+
protected internal virtual void ValidateFunctionApp(string functionAppRoot, PackOptions options)
4848
{
4949
}
5050

0 commit comments

Comments
 (0)