Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using Microsoft.Agents.A365.DevTools.Cli.Constants;
using Microsoft.Agents.A365.DevTools.Cli.Exceptions;
using Microsoft.Agents.A365.DevTools.Cli.Helpers;
using Microsoft.Agents.A365.DevTools.Cli.Models;
using Microsoft.Agents.A365.DevTools.Cli.Services;
Expand Down Expand Up @@ -315,6 +316,10 @@ await ProjectSettingsSyncHelper.ExecuteAsync(
// Display comprehensive setup summary
DisplaySetupSummary(setupResults, logger);
}
catch (Agent365Exception ex)
{
ExceptionHandler.HandleAgent365Exception(ex);
}
catch (Exception ex)
{
logger.LogError(ex, "Setup failed: {Message}", ex.Message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private static List<string> BuildMitigation(string resourceType, bool isPermissi
return new List<string>
{
"Check your Azure subscription permissions",
$"Ensure you have Contributor or Owner role on the subscription",
$"Ensure you have Contributor or Owner role on the subscription or at least the Resource Group",
"Contact your Azure administrator if needed",
"Run 'az account show' to verify your account"
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Serilog;

namespace Microsoft.Agents.A365.DevTools.Cli.Exceptions;

/// <summary>
/// Centralized exception handling utility for Agent365 CLI.
/// Provides consistent error display and logging.
/// Follows Microsoft CLI best practices (Azure CLI, dotnet CLI patterns).
/// </summary>
public static class ExceptionHandler
{
/// <summary>
/// Handles Agent365Exception with user-friendly output (no stack traces for user errors).
/// Displays formatted error messages to console and logs to Serilog for diagnostics.
/// </summary>
/// <param name="ex">The Agent365Exception to handle</param>
public static void HandleAgent365Exception(Agent365Exception ex)
{
// Display formatted error message
Console.Error.Write(ex.GetFormattedMessage());

// For system errors (not user errors), suggest reporting as bug
if (!ex.IsUserError)
{
Console.Error.WriteLine("If this error persists, please report it at:");
Console.Error.WriteLine("https://github.com/microsoft/Agent365-devTools/issues");
Console.Error.WriteLine();
}

// Log for diagnostics (but don't show stack trace to user)
Log.Error("Operation failed. ErrorCode={ErrorCode}, IssueDescription={IssueDescription}",
ex.ErrorCode, ex.IssueDescription);
}
}
29 changes: 5 additions & 24 deletions src/Microsoft.Agents.A365.DevTools.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Events;
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Parsing;
Expand All @@ -28,7 +29,9 @@ static async Task<int> Main(string[] args)
// Configure Serilog with both console and file output
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Is(isVerbose ? Serilog.Events.LogEventLevel.Debug : Serilog.Events.LogEventLevel.Information)
.WriteTo.Console() // Console output (user-facing)
.WriteTo.Logger(lc => lc
.Filter.ByExcluding(e => e.Level >= LogEventLevel.Error) // ✅ Exclude Error/Fatal from console
.WriteTo.Console())
.WriteTo.File( // File output (for debugging)
path: logFilePath,
rollingInterval: RollingInterval.Infinite,
Expand Down Expand Up @@ -108,7 +111,7 @@ static async Task<int> Main(string[] args)
{
if (exception is Agent365Exception myEx)
{
HandleAgent365Exception(myEx);
ExceptionHandler.HandleAgent365Exception(myEx);
context.ExitCode = myEx.ExitCode;
}
else
Expand All @@ -132,28 +135,6 @@ static async Task<int> Main(string[] args)
}
}


/// <summary>
/// Handles Agent365Exception with user-friendly output (no stack traces for user errors).
/// Follows Microsoft CLI best practices (Azure CLI, dotnet CLI patterns).
/// </summary>
private static void HandleAgent365Exception(Agent365Exception ex)
{
// Display formatted error message
Console.Error.WriteLine(ex.GetFormattedMessage());

// For system errors (not user errors), suggest reporting as bug
if (!ex.IsUserError)
{
Console.Error.WriteLine("If this error persists, please report it at:");
Console.Error.WriteLine("https://github.com/microsoft/Agent365-devTools/issues");
}

// Log for diagnostics (but don't show stack trace to user)
Log.Error("Operation failed. ErrorCode={ErrorCode}, IssueDescription={IssueDescription}",
ex.ErrorCode, ex.IssueDescription);
}

private static void ConfigureServices(IServiceCollection services)
{
// Add logging
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Azure.Identity;
using Azure.Core;
using Microsoft.Agents.A365.DevTools.Cli.Constants;
using Microsoft.Agents.A365.DevTools.Cli.Exceptions;

namespace Microsoft.Agents.A365.DevTools.Cli.Services;

Expand Down Expand Up @@ -275,16 +276,23 @@ public async Task<bool> RunAsync(string configPath, string generatedConfigPath,
}

// Web App
var webShow = await _executor.ExecuteAsync("az", $"webapp show -g {resourceGroup} -n {webAppName} --subscription {subscriptionId}", captureOutput: true, suppressErrorLogging: true);
var webShow = await _executor.ExecuteAsync("az", $"webapp show -g {resourceGroup} -n {webAppName} --subscription {subscriptionId}", captureOutput: true);
if (!webShow.Success)
{
var runtime = GetRuntimeForPlatform(platform);
_logger.LogInformation("Creating web app {App} with runtime {Runtime}", webAppName, runtime);
var createResult = await _executor.ExecuteAsync("az", $"webapp create -g {resourceGroup} -p {planName} -n {webAppName} --runtime \"{runtime}\" --subscription {subscriptionId}");
var createResult = await _executor.ExecuteAsync("az", $"webapp create -g {resourceGroup} -p {planName} -n {webAppName} --runtime \"{runtime}\" --subscription {subscriptionId}", captureOutput: true, suppressErrorLogging: true);
if (!createResult.Success)
{
_logger.LogError("ERROR: Web app creation failed: {Err}", createResult.StandardError);
throw new InvalidOperationException($"Failed to create web app '{webAppName}'. Setup cannot continue.");
if (createResult.StandardError.Contains("AuthorizationFailed", StringComparison.OrdinalIgnoreCase))
{
throw new AzureResourceException("WebApp", webAppName, createResult.StandardError, true);
}
else
{
_logger.LogError("ERROR: Web app creation failed: {Err}", createResult.StandardError);
throw new InvalidOperationException($"Failed to create web app '{webAppName}'. Setup cannot continue.");
}
}
}
else
Expand Down Expand Up @@ -1198,13 +1206,18 @@ private string ProtectSecret(string plaintext)

private async Task AzWarnAsync(string args, string description)
{
var result = await _executor.ExecuteAsync("az", args);
var result = await _executor.ExecuteAsync("az", args, suppressErrorLogging: true);
if (!result.Success)
{
if (result.StandardError.Contains("already exists", StringComparison.OrdinalIgnoreCase))
{
_logger.LogInformation("{Description} already exists (skipping creation)", description);
}
else if (result.StandardError.Contains("AuthorizationFailed", StringComparison.OrdinalIgnoreCase))
{
var exception = new AzureResourceException(description, string.Empty, result.StandardError, true);
ExceptionHandler.HandleAgent365Exception(exception);
}
else
{
_logger.LogWarning("az {Description} returned non-success (exit code {Code}). Error: {Err}",
Expand Down