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
15 changes: 7 additions & 8 deletions src/Microsoft.Agents.A365.DevTools.Cli/Models/OryxManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,22 @@ public class OryxManifest
public string Platform { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
public string Command { get; set; } = string.Empty;
public string BuildCommand { get; set; } = string.Empty;
public bool BuildRequired { get; set; } = true;

/// <summary>
/// Write the manifest to a file in TOML format
/// </summary>
public async Task WriteToFileAsync(string filePath)
{
var buildSection = BuildRequired ? $@"[build]
platform = ""{Platform}""
version = ""{Version}""
build-command = ""pip install -r requirements.txt""
var buildSection = BuildRequired ? "[build]\n" +
$@"platform = ""{Platform}""" + "\n" +
$@"version = ""{Version}""" + "\n" +
$@"build-command = ""{BuildCommand}""" +"\n\n" : "";

" : "";
var content = $@"{buildSection}[run]" + "\n" +
$@"command = ""{Command}""";
Comment thread
JesuTerraz marked this conversation as resolved.

var content = $@"{buildSection}[run]
command = ""{Command}""
";
await File.WriteAllTextAsync(filePath, content);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public async Task<bool> DeployAsync(DeploymentConfiguration config, bool verbose
else
{
// 1. Detect platform
_logger.LogInformation("[1/7] Detecting environment...");
var platform = config.Platform ?? _platformDetector.Detect(projectDir);
if (platform == ProjectPlatform.Unknown)
{
Expand All @@ -94,54 +95,55 @@ public async Task<bool> DeployAsync(DeploymentConfiguration config, bool verbose
_logger.LogInformation("Detected platform: {Platform}", platform);

// 2. Get appropriate builder
_logger.LogInformation("[2/7] Getting appropriate builder for {Platform} environment...", platform);
if (!_builders.TryGetValue(platform, out var builder))
{
throw new NotSupportedException($"Platform {platform} is not yet supported for deployment");
}

// 3. Validate environment
_logger.LogInformation("[1/7] Validating {Platform} environment...", platform);
_logger.LogInformation("[3/7] Validating {Platform} environment...", platform);
if (!await builder.ValidateEnvironmentAsync())
{
throw new Exception($"Environment validation failed for {platform}");
}

// 4. Build application (BuildAsync will handle cleaning the publish directory)
_logger.LogInformation("[2/7] Building {Platform} application...", platform);
_logger.LogInformation("[4/7] Building {Platform} application...", platform);
publishPath = await builder.BuildAsync(projectDir, config.PublishOutputPath, verbose);
_logger.LogInformation("Build output: {Path}", publishPath);

// 5. Create Oryx manifest
_logger.LogInformation("[3/7] Creating Oryx manifest...");
_logger.LogInformation("[5/7] Creating Oryx manifest...");
var manifest = await builder.CreateManifestAsync(projectDir, publishPath);
var manifestPath = Path.Combine(publishPath, "oryx-manifest.toml");
await manifest.WriteToFileAsync(manifestPath);
_logger.LogInformation("Manifest command: {Command}", manifest.Command);

// 6. Convert .env to Azure App Settings (for Python projects)
if (platform == ProjectPlatform.Python && builder is PythonBuilder pythonBuilder)
// 6. Convert .env to Azure App Settings (if it exists)
_logger.LogInformation("[6/7] Converting .env to Azure App Settings...");
var envResult = await builder.ConvertEnvToAzureAppSettingsAsync(projectDir, config.ResourceGroup, config.AppName, verbose);
if (!envResult)
{
_logger.LogInformation("[4/7] Converting .env to Azure App Settings...");
var envResult = await pythonBuilder.ConvertEnvToAzureAppSettingsAsync(projectDir, config.ResourceGroup, config.AppName, verbose);
if (!envResult)
{
_logger.LogWarning("Failed to convert environment variables, but continuing with deployment");
}
_logger.LogWarning("Failed to convert environment variables, but continuing with deployment");
}

// Set startup command for Python apps
_logger.LogInformation("[6/7] Setting Python startup command...");
// 7. Set startup command for Python apps
if (platform == ProjectPlatform.Python && builder is PythonBuilder pythonBuilder)
{
_logger.LogInformation("[7/7] Setting Python startup command...");
var startupResult = await pythonBuilder.SetStartupCommandAsync(projectDir, config.ResourceGroup, config.AppName, verbose);
if (!startupResult)
{
_logger.LogWarning("Failed to set startup command, but continuing with deployment");
}

// Add delay to allow Azure configuration to stabilize before deployment
// This prevents "SCM container restart" conflicts
_logger.LogInformation("Waiting for Azure configuration to stabilize...");
await Task.Delay(TimeSpan.FromSeconds(5));
}
Comment thread
JesuTerraz marked this conversation as resolved.

// Add delay to allow Azure configuration to stabilize before deployment
// This prevents "SCM container restart" conflicts
_logger.LogInformation("Waiting for Azure configuration to stabilize...");
await Task.Delay(TimeSpan.FromSeconds(5));
Comment thread
JesuTerraz marked this conversation as resolved.

await builder.CleanAsync(publishPath);
}

Expand Down
30 changes: 10 additions & 20 deletions src/Microsoft.Agents.A365.DevTools.Cli/Services/DotNetBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Text;
using Microsoft.Agents.A365.DevTools.Cli.Models;
using Microsoft.Agents.A365.DevTools.Cli.Services.Helpers;
using Microsoft.Extensions.Logging;

namespace Microsoft.Agents.A365.DevTools.Cli.Services;
Expand All @@ -14,11 +15,13 @@ public class DotNetBuilder : IPlatformBuilder
{
private readonly ILogger<DotNetBuilder> _logger;
private readonly CommandExecutor _executor;
private readonly BuilderHelper _helper;

public DotNetBuilder(ILogger<DotNetBuilder> logger, CommandExecutor executor)
{
_logger = logger;
_executor = executor;
_helper = new BuilderHelper(logger, executor);
}

public async Task<bool> ValidateEnvironmentAsync()
Expand Down Expand Up @@ -81,7 +84,7 @@ public async Task<string> BuildAsync(string projectDir, string outputPath, bool
// Publish
_logger.LogInformation("Publishing .NET application...");
var publishArgs = $"publish \"{projectFile}\" -c Release -o \"{outputPath}\" --self-contained false --verbosity minimal";
var publishResult = await ExecuteWithOutputAsync("dotnet", publishArgs, projectDir, verbose);
var publishResult = await _helper.ExecuteWithOutputAsync("dotnet", publishArgs, projectDir, verbose);

if (!publishResult.Success)
{
Expand Down Expand Up @@ -137,6 +140,12 @@ public async Task<OryxManifest> CreateManifestAsync(string projectDir, string pu
};
}

public async Task<bool> ConvertEnvToAzureAppSettingsAsync(string projectDir, string resourceGroup, string webAppName, bool verbose)
{
// Not needed for dotnet projects.
Comment thread
JesuTerraz marked this conversation as resolved.
return await Task.FromResult(true);
}

private string? ResolveProjectFile(string projectDir)
{
var csprojFiles = Directory.GetFiles(projectDir, "*.csproj", SearchOption.TopDirectoryOnly);
Expand All @@ -159,23 +168,4 @@ public async Task<OryxManifest> CreateManifestAsync(string projectDir, string pu

return Path.GetFileName(allProjectFiles[0]);
}

private async Task<CommandResult> ExecuteWithOutputAsync(string command, string arguments, string workingDirectory, bool verbose)
{
var result = await _executor.ExecuteAsync(command, arguments, workingDirectory);

if (verbose || !result.Success)
{
if (!string.IsNullOrWhiteSpace(result.StandardOutput))
{
_logger.LogInformation("Output:\n{Output}", result.StandardOutput);
}
if (!string.IsNullOrWhiteSpace(result.StandardError))
{
_logger.LogWarning("Warnings/Errors:\n{Error}", result.StandardError);
}
}

return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Extensions.Logging;

namespace Microsoft.Agents.A365.DevTools.Cli.Services.Helpers
{
public class BuilderHelper
{
private readonly ILogger _logger;
private readonly CommandExecutor _executor;

public BuilderHelper(ILogger logger, CommandExecutor executor)
{
_logger = logger;
_executor = executor;
}

/// <summary>
/// Converts .env file to Azure App Settings using a single az webapp config appsettings set command
/// </summary>
public async Task<bool> ConvertEnvToAzureAppSettingsIfExistsAsync(
string projectDir,
string resourceGroup,
string webAppName,
bool verbose)
{
var envFilePath = Path.Combine(projectDir, ".env");
if (!File.Exists(envFilePath))
{
_logger.LogInformation("No .env file found to convert to Azure App Settings");
return true; // Not an error, just no env file
}

_logger.LogInformation("Converting .env file to Azure App Settings...");

var envSettings = new List<string>();
var lines = await File.ReadAllLinesAsync(envFilePath);

foreach (var line in lines)
{
// Skip empty lines and comments
if (string.IsNullOrWhiteSpace(line) || line.Trim().StartsWith("#"))
continue;

// Parse KEY=VALUE format
var equalIndex = line.IndexOf('=');
if (equalIndex > 0 && equalIndex < line.Length - 1)
{
var key = line.Substring(0, equalIndex).Trim();
var value = line.Substring(equalIndex + 1).Trim();

// Remove quotes if present
if ((value.StartsWith("\"") && value.EndsWith("\"")) ||
(value.StartsWith("'") && value.EndsWith("'")))
{
value = value.Substring(1, value.Length - 2);
Comment thread
JesuTerraz marked this conversation as resolved.
}

envSettings.Add($"{key}={value}");
_logger.LogDebug("Found environment variable: {Key}", key);
}
}
Comment thread
JesuTerraz marked this conversation as resolved.
Comment thread
JesuTerraz marked this conversation as resolved.

if (envSettings.Count == 0)
{
_logger.LogInformation("No valid environment variables found in .env file");
return true;
}

// Build single az webapp config appsettings set command with all variables
var settingsArgs = string.Join(" ", envSettings.Select(setting => $"\"{setting}\""));
var azCommand = $"webapp config appsettings set -g {resourceGroup} -n {webAppName} --settings {settingsArgs}";

_logger.LogInformation("Setting {Count} environment variables as Azure App Settings...", envSettings.Count);

var result = await ExecuteWithOutputAsync("az", azCommand, projectDir, verbose);
if (result.Success)
{
_logger.LogInformation("Successfully converted {Count} environment variables to Azure App Settings", envSettings.Count);
return true;
}
else
{
_logger.LogError("Failed to set Azure App Settings: {Error}", result.StandardError);
return false;
}
}
Comment thread
JesuTerraz marked this conversation as resolved.

public async Task<CommandResult> ExecuteWithOutputAsync(string command, string arguments, string workingDirectory, bool verbose)
{
var result = await _executor.ExecuteAsync(command, arguments, workingDirectory);

if (verbose || !result.Success)
{
if (!string.IsNullOrWhiteSpace(result.StandardOutput))
{
_logger.LogInformation("Output:\n{Output}", result.StandardOutput);
}
if (!string.IsNullOrWhiteSpace(result.StandardError))
{
_logger.LogWarning("Warnings/Errors:\n{Error}", result.StandardError);
}
}

return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ public interface IPlatformBuilder
/// Create Oryx manifest for the platform
/// </summary>
Task<OryxManifest> CreateManifestAsync(string projectDir, string publishPath);

Comment thread
JesuTerraz marked this conversation as resolved.
/// <summary>
/// Convert .env file to Azure App Settings for the deployed application
/// </summary>
Task<bool> ConvertEnvToAzureAppSettingsAsync(string projectDir, string resourceGroup, string webAppName, bool verbose);
}
Loading