Skip to content

Commit a25cc41

Browse files
committed
Merge with main
2 parents 162767f + 8acf918 commit a25cc41

File tree

5 files changed

+206
-17
lines changed

5 files changed

+206
-17
lines changed

src/Microsoft.Agents.A365.DevTools.Cli/Constants/ErrorCodes.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ public static class ErrorCodes
1616
public const string DeploymentScopesFailed = "DEPLOYMENT_SCOPES_FAILED";
1717
public const string DeploymentMcpFailed = "DEPLOYMENT_MCP_FAILED";
1818
public const string HighPrivilegeScopeDetected = "HIGH_PRIVILEGE_SCOPE_DETECTED";
19-
public const string SetupValidationFailed = "SETUP_VALIDATION_FAILED";
19+
public const string NodeBuildFailed = "NODE_BUILD_FAILED";
20+
public const string NodeDependencyInstallFailed = "NODE_DEPENDENCY_INSTALL_FAILED";
21+
public const string NodeProjectNotFound = "NODE_PROJECT_NOT_FOUND";
2022
public const string RetryExhausted = "RETRY_EXHAUSTED";
23+
public const string SetupValidationFailed = "SETUP_VALIDATION_FAILED";
2124
public const string ClientAppValidationFailed = "CLIENT_APP_VALIDATION_FAILED";
2225
}
2326
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Agents.A365.DevTools.Cli.Constants;
5+
6+
namespace Microsoft.Agents.A365.DevTools.Cli.Exceptions;
7+
8+
/// <summary>
9+
/// Thrown when the build process of a Node.js project fails.
10+
/// </summary>
11+
public sealed class NodeBuildFailedException : Agent365Exception
12+
{
13+
public override int ExitCode => 1;
14+
public override bool IsUserError => true;
15+
16+
public NodeBuildFailedException(string projectDirectory, string? npmErrorOutput)
17+
: base(
18+
errorCode: ErrorCodes.NodeBuildFailed,
19+
issueDescription: "Failed to build the Node.js project using 'npm run build'.",
20+
errorDetails: BuildDetails(projectDirectory, npmErrorOutput),
21+
mitigationSteps: new List<string>
22+
{
23+
"Run 'npm run build' locally in the project directory and fix any TypeScript/webpack/build errors.",
24+
"Verify that the 'build' script is defined correctly in package.json.",
25+
"If the build depends on environment variables or private packages, ensure those are configured on the machine running 'a365 deploy'.",
26+
"After resolving the build issues, rerun 'a365 deploy'."
27+
},
28+
context: new Dictionary<string, string>
29+
{
30+
["ProjectDirectory"] = projectDirectory
31+
})
32+
{
33+
}
34+
35+
private static List<string> BuildDetails(string projectDirectory, string? npmErrorOutput)
36+
{
37+
var details = new List<string>
38+
{
39+
$"Project directory: {projectDirectory}",
40+
};
41+
42+
if (!string.IsNullOrWhiteSpace(npmErrorOutput))
43+
{
44+
details.Add("npm build error output (truncated):");
45+
details.Add($" {TrimError(npmErrorOutput)}");
46+
}
47+
48+
return details;
49+
}
50+
51+
private static string TrimError(string error)
52+
{
53+
const int maxLen = 400;
54+
error = error.Trim();
55+
return error.Length <= maxLen ? error : error[..maxLen] + " ...";
56+
}
57+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Agents.A365.DevTools.Cli.Constants;
5+
6+
namespace Microsoft.Agents.A365.DevTools.Cli.Exceptions;
7+
8+
/// <summary>
9+
/// Thrown when installation of Node.js dependencies fails.
10+
/// </summary>
11+
public class NodeDependencyInstallException : Agent365Exception
12+
{
13+
public override int ExitCode => 1;
14+
public override bool IsUserError => true;
15+
16+
public NodeDependencyInstallException(string projectDirectory, string? npmErrorOutput)
17+
: base(
18+
errorCode: ErrorCodes.NodeDependencyInstallFailed,
19+
issueDescription: "Failed to install Node.js dependencies for the project.",
20+
errorDetails: BuildDetails(projectDirectory, npmErrorOutput),
21+
mitigationSteps: new List<string>
22+
{
23+
"Run 'npm install' (or 'npm ci') locally in the project directory and fix any errors.",
24+
"Check that your internet connection and npm registry access are working.",
25+
"If you use a private registry or npm auth, ensure those settings are configured on the machine running 'a365 deploy'.",
26+
"After fixing the issue, rerun 'a365 deploy'."
27+
},
28+
context: new Dictionary<string, string>
29+
{
30+
["ProjectDirectory"] = projectDirectory
31+
})
32+
{
33+
}
34+
35+
private static List<string> BuildDetails(string projectDirectory, string? npmErrorOutput)
36+
{
37+
var details = new List<string>
38+
{
39+
$"Project directory: {projectDirectory}",
40+
};
41+
42+
if (!string.IsNullOrWhiteSpace(npmErrorOutput))
43+
{
44+
details.Add("npm error output (truncated):");
45+
details.Add($" {TrimError(npmErrorOutput)}");
46+
}
47+
48+
return details;
49+
}
50+
51+
private static string TrimError(string error)
52+
{
53+
const int maxLen = 400;
54+
error = error.Trim();
55+
return error.Length <= maxLen ? error : error[..maxLen] + " ...";
56+
}
57+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Agents.A365.DevTools.Cli.Constants;
5+
6+
namespace Microsoft.Agents.A365.DevTools.Cli.Exceptions;
7+
8+
/// <summary>
9+
/// Thrown when a Node.js deployment is requested from a directory that does not contain a package.json.
10+
/// </summary>
11+
public class NodeProjectNotFoundException : Agent365Exception
12+
{
13+
public override int ExitCode => 1;
14+
public override bool IsUserError => true;
15+
16+
public NodeProjectNotFoundException(string projectDirectory)
17+
: base(
18+
errorCode: ErrorCodes.NodeProjectNotFound,
19+
issueDescription: "No Node.js project was found in the specified directory.",
20+
errorDetails: new List<string>
21+
{
22+
"The deployment expects a package.json file to identify the Node.js project.",
23+
$"Checked directory: {projectDirectory}"
24+
},
25+
mitigationSteps: new List<string>
26+
{
27+
"Run this command from the root of your Node.js project (where package.json is located), or",
28+
"Update the --project-path in your a365 config to point to the folder containing package.json.",
29+
"Verify that package.json is checked into source control and not ignored or deleted."
30+
},
31+
context: new Dictionary<string, string>
32+
{
33+
["ProjectDirectory"] = projectDirectory
34+
})
35+
{
36+
}
37+
}

src/Microsoft.Agents.A365.DevTools.Cli/Services/NodeBuilder.cs

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using System.Text.Json;
5+
using Microsoft.Agents.A365.DevTools.Cli.Exceptions;
56
using Microsoft.Agents.A365.DevTools.Cli.Models;
67
using Microsoft.Agents.A365.DevTools.Cli.Services.Helpers;
78
using Microsoft.Extensions.Logging;
@@ -77,7 +78,7 @@ public async Task<string> BuildAsync(string projectDir, string outputPath, bool
7778
var packageJsonPath = Path.Combine(projectDir, "package.json");
7879
if (!File.Exists(packageJsonPath))
7980
{
80-
throw new FileNotFoundException("package.json not found in project directory");
81+
throw new NodeProjectNotFoundException(projectDir);
8182
}
8283

8384
// Install dependencies
@@ -89,7 +90,7 @@ public async Task<string> BuildAsync(string projectDir, string outputPath, bool
8990
installResult = await _helper.ExecuteWithOutputAsync("npm", "install", projectDir, verbose);
9091
if (!installResult.Success)
9192
{
92-
throw new Exception($"npm install failed: {installResult.StandardError}");
93+
throw new NodeDependencyInstallException(projectDir, installResult.StandardError);
9394
}
9495
}
9596

@@ -103,7 +104,7 @@ public async Task<string> BuildAsync(string projectDir, string outputPath, bool
103104
var buildResult = await _helper.ExecuteWithOutputAsync("npm", "run build", projectDir, verbose);
104105
if (!buildResult.Success)
105106
{
106-
throw new Exception($"npm run build failed: {buildResult.StandardError}");
107+
throw new NodeBuildFailedException(projectDir, buildResult.StandardError);
107108
}
108109
}
109110
else
@@ -145,7 +146,7 @@ public async Task<string> BuildAsync(string projectDir, string outputPath, bool
145146
CopyDirectory(srcDir, Path.Combine(publishPath, "src"));
146147
}
147148

148-
// Copy server files (.js files in root)
149+
// Copy server/entry files (.js/.ts files in root)
149150
foreach (var jsFile in Directory.GetFiles(projectDir, "*.js"))
150151
{
151152
File.Copy(jsFile, Path.Combine(publishPath, Path.GetFileName(jsFile)));
@@ -155,6 +156,17 @@ public async Task<string> BuildAsync(string projectDir, string outputPath, bool
155156
File.Copy(tsFile, Path.Combine(publishPath, Path.GetFileName(tsFile)));
156157
}
157158

159+
var distDir = Path.Combine(projectDir, "dist");
160+
if (Directory.Exists(distDir))
161+
{
162+
_logger.LogInformation("Found dist folder, copying to publish output...");
163+
CopyDirectory(distDir, Path.Combine(publishPath, "dist"));
164+
}
165+
else
166+
{
167+
_logger.LogInformation("No dist folder found in project; relying on Oryx build (if configured) to produce runtime output.");
168+
}
169+
158170
// Create .deployment file to force Oryx build during Azure deployment
159171
await CreateDeploymentFile(publishPath);
160172

@@ -166,6 +178,11 @@ public async Task<OryxManifest> CreateManifestAsync(string projectDir, string pu
166178
_logger.LogInformation("Creating Oryx manifest for Node.js...");
167179

168180
var packageJsonPath = Path.Combine(projectDir, "package.json");
181+
if (!File.Exists(packageJsonPath))
182+
{
183+
throw new NodeProjectNotFoundException(projectDir);
184+
}
185+
169186
var packageJson = await File.ReadAllTextAsync(packageJsonPath);
170187

171188
// Parse package.json to detect start command and version
@@ -177,8 +194,7 @@ public async Task<OryxManifest> CreateManifestAsync(string projectDir, string pu
177194
if (root.TryGetProperty("engines", out var engines) &&
178195
engines.TryGetProperty("node", out var nodeVersionProp))
179196
{
180-
var versionString = nodeVersionProp.GetString() ?? "18";
181-
// Extract major version (e.g., "18.x" -> "18")
197+
var versionString = nodeVersionProp.GetString() ?? "20";
182198
var match = System.Text.RegularExpressions.Regex.Match(versionString, @"(\d+)");
183199
if (match.Success)
184200
{
@@ -192,7 +208,13 @@ public async Task<OryxManifest> CreateManifestAsync(string projectDir, string pu
192208
if (root.TryGetProperty("scripts", out var scripts) &&
193209
scripts.TryGetProperty("start", out var startScript))
194210
{
195-
startCommand = startScript.GetString() ?? startCommand;
211+
var scriptValue = startScript.GetString();
212+
if (!string.IsNullOrWhiteSpace(scriptValue))
213+
{
214+
// Use the script literally, same as package.json
215+
startCommand = scriptValue;
216+
}
217+
196218
_logger.LogInformation("Detected start command from package.json: {Command}", startCommand);
197219
}
198220
else if (root.TryGetProperty("main", out var mainProp))
@@ -203,34 +225,47 @@ public async Task<OryxManifest> CreateManifestAsync(string projectDir, string pu
203225
}
204226
else
205227
{
206-
// Look for common entry point files
228+
// Look for common entry point files under publish path
207229
var commonEntryPoints = new[] { "server.js", "app.js", "index.js", "main.js" };
208230
foreach (var entryPoint in commonEntryPoints)
209231
{
210232
if (File.Exists(Path.Combine(publishPath, entryPoint)))
211233
{
212234
startCommand = $"node {entryPoint}";
213-
_logger.LogInformation("Detected entry point: {Command}", startCommand);
235+
_logger.LogInformation("Detected entry point in publish folder: {Command}", startCommand);
214236
break;
215237
}
216238
}
217239
}
218240

219-
var buildCommand = "npm run build";
220-
var hasBuildScript = scripts.TryGetProperty("build", out var buildScript);
221-
if (hasBuildScript)
241+
// Detect build command – strictly Node
242+
string buildCommand = "";
243+
bool buildRequired = false;
244+
245+
if (root.TryGetProperty("scripts", out var scriptsForBuild) &&
246+
scriptsForBuild.TryGetProperty("build", out var buildScript))
247+
{
248+
var buildValue = buildScript.GetString();
249+
if (!string.IsNullOrWhiteSpace(buildValue))
250+
{
251+
// We always call through npm so it picks up the script from package.json
252+
buildCommand = "npm run build";
253+
buildRequired = true;
254+
_logger.LogInformation("Detected build script; using Oryx build command: {Command}", buildCommand);
255+
}
256+
}
257+
else
222258
{
223-
buildCommand = buildScript.GetString() ?? buildCommand;
224-
_logger.LogInformation("Detected build command from package.json: {Command}", buildCommand);
259+
_logger.LogInformation("No build script found; Oryx will only run npm install.");
225260
}
226261

227262
return new OryxManifest
228263
{
229264
Platform = "nodejs",
230265
Version = nodeVersion,
231266
Command = startCommand,
232-
BuildCommand = hasBuildScript ? buildCommand : "",
233-
BuildRequired = hasBuildScript
267+
BuildCommand = buildCommand,
268+
BuildRequired = buildRequired
234269
};
235270
}
236271

0 commit comments

Comments
 (0)