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
51 changes: 34 additions & 17 deletions src/Microsoft.Agents.A365.DevTools.Cli/Commands/CleanupCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,18 @@ private static Command CreateAzureCleanupCommand(
}

// Azure CLI cleanup commands
var commandsList = new List<(string, string)>
var commandsList = new List<(string, string)>();

// If WebAppName is configured
if (config.NeedDeployment)
{
($"az webapp delete --name {config.WebAppName} --resource-group {config.ResourceGroup} --subscription {config.SubscriptionId}", "Web App"),
($"az appservice plan delete --name {config.AppServicePlanName} --resource-group {config.ResourceGroup} --subscription {config.SubscriptionId} --yes", "App Service Plan")
};
commandsList.Add(($"az webapp delete --name {config.WebAppName} --resource-group {config.ResourceGroup} --subscription {config.SubscriptionId}", "Web App"));
// Only add App Service Plan deletion if AppServicePlanName is configured
if (!string.IsNullOrWhiteSpace(config.AppServicePlanName))
{
commandsList.Add(($"az appservice plan delete --name {config.AppServicePlanName} --resource-group {config.ResourceGroup} --subscription {config.SubscriptionId} --yes", "App Service Plan"));
}
}

// Add bot deletion if bot exists
if (!string.IsNullOrEmpty(config.BotName))
Expand All @@ -192,21 +199,31 @@ private static Command CreateAzureCleanupCommand(
}
}

var commands = commandsList.ToArray();

foreach (var (cmd, name) in commands)
// Check if there are any Azure resources to delete
if (commandsList.Count == 0)
{
logger.LogInformation("Deleting {Name}...", name);
var parts = cmd.Split(' ', 2);
var result = await executor.ExecuteAsync(parts[0], parts[1], captureOutput: true);
if (result.ExitCode == 0)
{
logger.LogInformation("{Name} deleted successfully", name);
}
else
logger.LogInformation("No Azure Web App resources found to clean up.");
logger.LogInformation("This agent is configured with an external messaging endpoint: {MessagingEndpoint}",
config.MessagingEndpoint ?? "(not configured)");
}
else
{
var commands = commandsList.ToArray();

foreach (var (cmd, name) in commands)
{
logger.LogWarning("Failed to delete {Name}: {Error}", name, result.StandardError);
logger.LogInformation("Deleting {Name}...", name);
var parts = cmd.Split(' ', 2);
var result = await executor.ExecuteAsync(parts[0], parts[1], captureOutput: true);

if (result.ExitCode == 0)
{
logger.LogInformation("{Name} deleted successfully", name);
}
else
{
logger.LogWarning("Failed to delete {Name}: {Error}", name, result.StandardError);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,11 +219,6 @@ public static Command CreateCommand(ILogger<CreateInstanceCommand> logger, IConf
// Update configuration with the populated values
logger.LogInformation("Updating configuration with generated values...");

// Update Agent365Config state properties
instanceConfig.BotId = instanceConfig.AgentBlueprintId ?? endpointName;
instanceConfig.BotMsaAppId = instanceConfig.AgentBlueprintId;
instanceConfig.BotMessagingEndpoint = $"https://{instanceConfig.WebAppName}.azurewebsites.net/api/messages";

logger.LogInformation(" Agent Blueprint ID: {AgentBlueprintId}", instanceConfig.AgentBlueprintId);
logger.LogInformation(" Agent Instance ID: {AgenticAppId}", instanceConfig.AgenticAppId);
logger.LogInformation(" Agent User ID: {AgenticUserId}", instanceConfig.AgenticUserId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ public static Command CreateCommand(
return;
}

// Check if web app deployment should be skipped (external messaging endpoint)
if (!configData.NeedDeployment)
{
logger.LogInformation("Web App deployment is skipped as per configuration.");
return;
}

var validatedConfig = await ValidateDeploymentPrerequisitesAsync(
config.FullName, configService, azureValidator, executor, logger);
if (validatedConfig == null) return;
Expand Down Expand Up @@ -141,6 +148,13 @@ private static Command CreateAppSubcommand(
return;
}

// Check if web app deployment should be skipped (external messaging endpoint)
if (!configData.NeedDeployment)
{
logger.LogInformation("Web App deployment is skipped as per configuration.");
return;
}

var validatedConfig = await ValidateDeploymentPrerequisitesAsync(
config.FullName, configService, azureValidator, executor, logger);
if (validatedConfig == null) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ await EnsureMcpInheritablePermissionsAsync(
setupConfig = await configService.LoadAsync(config.FullName);

await RegisterBlueprintMessagingEndpointAsync(setupConfig, logger, botConfigurator);
await configService.SaveStateAsync(setupConfig);
setupResults.MessagingEndpointRegistered = true;
logger.LogInformation("Blueprint messaging endpoint registered successfully");
}
Expand Down Expand Up @@ -493,6 +494,7 @@ private static async Task RegisterBlueprintMessagingEndpointAsync(
var hostPart = uri.Host.Replace('.', '-');
var baseEndpointName = $"{hostPart}-endpoint";
endpointName = EndpointHelper.GetEndpointName(baseEndpointName);

}

if (endpointName.Length < 4)
Expand All @@ -518,6 +520,11 @@ private static async Task RegisterBlueprintMessagingEndpointAsync(
logger.LogError("Failed to register blueprint messaging endpoint");
throw new SetupValidationException("Blueprint messaging endpoint registration failed");
}
// Update Agent365Config state properties
setupConfig.BotId = setupConfig.AgentBlueprintId;
setupConfig.BotMsaAppId = setupConfig.AgentBlueprintId;
setupConfig.BotMessagingEndpoint = messagingEndpoint;

}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ public static class ConfigConstants
"Sites.Read.All"
};

public const string DefaultAppServicePlanSku = "B1";

/// <summary>
/// Default Microsoft Graph API scopes for agent application
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.Agents.A365.DevTools.Cli.Constants;
namespace Microsoft.Agents.A365.DevTools.Cli.Exceptions;
/// <summary>
/// Validation errors that occur during `a365 setup` (user-fixable issues).
/// </summary>
public sealed class SetupValidationException : Agent365Exception
{
public override int ExitCode => 2;
public SetupValidationException(
string issueDescription,
List<string>? errorDetails = null,
List<string>? mitigationSteps = null,
Dictionary<string, string>? context = null,
Exception? innerException = null)
: base(
errorCode: ErrorCodes.SetupValidationFailed,
issueDescription: issueDescription,
errorDetails: errorDetails,
mitigationSteps: mitigationSteps,
context: context,
innerException: innerException)
{
}
}
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Agents.A365.DevTools.Cli.Constants;

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

/// <summary>
/// Validation errors that occur during `a365 setup` (user-fixable issues).
/// </summary>
public sealed class SetupValidationException : Agent365Exception
{
public override int ExitCode => 2;

public SetupValidationException(
string issueDescription,
List<string>? errorDetails = null,
List<string>? mitigationSteps = null,
Dictionary<string, string>? context = null,
Exception? innerException = null)
: base(
errorCode: ErrorCodes.SetupValidationFailed,
issueDescription: issueDescription,
errorDetails: errorDetails,
mitigationSteps: mitigationSteps,
context: context,
innerException: innerException)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,24 @@ public List<string> Validate()

if (string.IsNullOrWhiteSpace(TenantId)) errors.Add("tenantId is required.");

if (string.IsNullOrWhiteSpace(SubscriptionId)) errors.Add("subscriptionId is required.");
if (string.IsNullOrWhiteSpace(ResourceGroup)) errors.Add("resourceGroup is required.");

if (NeedDeployment)
{
if (string.IsNullOrWhiteSpace(SubscriptionId)) errors.Add("subscriptionId is required.");
if (string.IsNullOrWhiteSpace(ResourceGroup)) errors.Add("resourceGroup is required.");
if (string.IsNullOrWhiteSpace(Location)) errors.Add("location is required.");
if (string.IsNullOrWhiteSpace(AppServicePlanName)) errors.Add("appServicePlanName is required.");
if (string.IsNullOrWhiteSpace(WebAppName)) errors.Add("webAppName is required.");
}
else
{
// Non-Azure hosting
if (string.IsNullOrWhiteSpace(MessagingEndpoint))
errors.Add("messagingEndpoint is required when needDeployment is 'no'.");
}

if (string.IsNullOrWhiteSpace(AgentIdentityDisplayName)) errors.Add("agentIdentityDisplayName is required.");
if (string.IsNullOrWhiteSpace(DeploymentProjectPath)) errors.Add("deploymentProjectPath is required.");

// agentIdentityScopes and agentApplicationScopes are now hardcoded defaults
// botName and botDisplayName are now derived, not required in config
// Add more validation as needed (e.g., GUID format, allowed values, etc.)
return errors;
}
// ========================================================================
Expand Down Expand Up @@ -87,14 +84,14 @@ public List<string> Validate()
/// Default: preprod
/// </summary>
[JsonPropertyName("environment")]
public string Environment { get; init; } = "preprod";
public string Environment { get; init; } = "prod";

/// <summary>
/// For External hosting, this is the HTTPS messaging endpoint that Bot Framework will call.
/// For AzureAppService, this is optional; the CLI derives the endpoint from webAppName.
/// </summary>
[JsonPropertyName("messagingEndpoint")]
public string? MessagingEndpoint { get; init; }
public string MessagingEndpoint { get; init; } = string.Empty;

/// <summary>
/// Whether the CLI should create and deploy an Azure Web App for this agent.
Expand All @@ -119,7 +116,7 @@ public List<string> Validate()
/// App Service Plan SKU/pricing tier (e.g., "B1", "S1", "P1v2").
/// </summary>
[JsonPropertyName("appServicePlanSku")]
public string AppServicePlanSku { get; init; } = "B1";
public string AppServicePlanSku { get; init; } = string.Empty;

/// <summary>
/// Name of the Azure Web App (must be globally unique).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,16 @@

using Azure.Core;
using Azure.Identity;
using System.Linq;
using Microsoft.Agents.A365.DevTools.Cli.Constants;
using Microsoft.Agents.A365.DevTools.Cli.Exceptions;
using Microsoft.Agents.A365.DevTools.Cli.Services.Helpers;
using Microsoft.Extensions.Logging;
using Microsoft.Graph;
using Microsoft.Graph.Models;
using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Nodes;
using static System.Formats.Asn1.AsnWriter;

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

Expand Down Expand Up @@ -133,7 +130,7 @@ public async Task<bool> RunAsync(string configPath, string generatedConfigPath,
var webAppName = Get("webAppName");
var location = Get("location");
var planSku = Get("appServicePlanSku");
if (string.IsNullOrWhiteSpace(planSku)) planSku = "B1";
if (string.IsNullOrWhiteSpace(planSku)) planSku = ConfigConstants.DefaultAppServicePlanSku;

var deploymentProjectPath = Get("deploymentProjectPath");

Expand Down
Loading