From fa7b26d81d5234c7f66cb118997a087e9ab407b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:10:58 +0000 Subject: [PATCH 01/10] Initial plan From 9052215eb210b2fa973e82d1bc7a464cea72c5ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:24:13 +0000 Subject: [PATCH 02/10] Add automatic Dockerfile generation support for Golang integration Co-authored-by: tommasodotNET <12819039+tommasodotNET@users.noreply.github.com> --- .../GolangAppHostingExtension.cs | 47 ++++++++++++++++++- .../README.md | 12 +++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs index 37f9f179b..6e2ca47f4 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs @@ -68,10 +68,55 @@ public static IResourceBuilder AddGolangApp(this ID return builder.AddResource(resource) .WithGolangDefaults() - .WithArgs([.. allArgs]); + .WithArgs([.. allArgs]) + .PublishAsGolangDockerfile(workingDirectory, executable, buildTags); } private static IResourceBuilder WithGolangDefaults( this IResourceBuilder builder) => builder.WithOtlpExporter(); + + /// + /// Configures the Golang application to be published as a Dockerfile with automatic multi-stage build generation. + /// + /// The resource builder. + /// The working directory containing the Golang application. + /// The path to the Golang package directory or source file to be executed. + /// The optional build tags to be used when building the Golang application. + /// A reference to the . +#pragma warning disable ASPIREDOCKERFILEBUILDER001 + private static IResourceBuilder PublishAsGolangDockerfile( + this IResourceBuilder builder, + string workingDirectory, + string executable, + string[]? buildTags) + { + return builder.PublishAsDockerFile(publish => + { + publish.WithDockerfileBuilder(workingDirectory, context => + { + var buildArgs = new List { "build", "-o", "/app/server" }; + + if (buildTags is { Length: > 0 }) + { + buildArgs.Add("-tags"); + buildArgs.Add(string.Join(",", buildTags)); + } + + buildArgs.Add(executable); + + var buildStage = context.Builder + .From("golang:1.23", "builder") + .WorkDir("/build") + .Copy(".", "./") + .Run(string.Join(" ", ["go", .. buildArgs])); + + context.Builder + .From("alpine:latest") + .CopyFrom(buildStage.StageName!, "/app/server", "/app/server") + .Entrypoint(["/app/server"]); + }); + }); + } +#pragma warning restore ASPIREDOCKERFILEBUILDER001 } diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/README.md b/src/CommunityToolkit.Aspire.Hosting.Golang/README.md index b95f40d6b..e3e614b1c 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/README.md @@ -29,6 +29,18 @@ To have the Golang application listen on the correct port, you can use the follo r.Run(":"+os.Getenv("PORT")) ``` +## Publishing + +When publishing your Aspire application, the Golang resource automatically generates a multi-stage Dockerfile for containerization. This means you don't need to manually create a Dockerfile for your Golang application. + +The generated Dockerfile: +- Uses `golang:1.23` as the build stage to compile your application +- Uses `alpine:latest` as the runtime stage for a smaller final image +- Respects your build tags if specified +- Builds the executable specified in your `AddGolangApp` call + +This automatic Dockerfile generation happens when you publish your Aspire application and requires no additional configuration. + ## Additional Information https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-golang From 590c9e04217667eced5430221906482344df113e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:31:30 +0000 Subject: [PATCH 03/10] Fix code formatting for line endings Co-authored-by: tommasodotNET <12819039+tommasodotNET@users.noreply.github.com> --- .../GolangAppHostingExtension.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs index 6e2ca47f4..d7258c705 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs @@ -96,13 +96,13 @@ private static IResourceBuilder PublishAsGolangDock publish.WithDockerfileBuilder(workingDirectory, context => { var buildArgs = new List { "build", "-o", "/app/server" }; - + if (buildTags is { Length: > 0 }) { buildArgs.Add("-tags"); buildArgs.Add(string.Join(",", buildTags)); } - + buildArgs.Add(executable); var buildStage = context.Builder @@ -119,4 +119,4 @@ private static IResourceBuilder PublishAsGolangDock }); } #pragma warning restore ASPIREDOCKERFILEBUILDER001 -} +} \ No newline at end of file From 1e62e495fea38d8212ffd725e07a081a3076e311 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:33:23 +0000 Subject: [PATCH 04/10] Use explicit Alpine version tag instead of latest Co-authored-by: tommasodotNET <12819039+tommasodotNET@users.noreply.github.com> --- .../GolangAppHostingExtension.cs | 2 +- src/CommunityToolkit.Aspire.Hosting.Golang/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs index d7258c705..9332c9cdb 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs @@ -112,7 +112,7 @@ private static IResourceBuilder PublishAsGolangDock .Run(string.Join(" ", ["go", .. buildArgs])); context.Builder - .From("alpine:latest") + .From("alpine:3.21") .CopyFrom(buildStage.StageName!, "/app/server", "/app/server") .Entrypoint(["/app/server"]); }); diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/README.md b/src/CommunityToolkit.Aspire.Hosting.Golang/README.md index e3e614b1c..840daf92b 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/README.md @@ -35,7 +35,7 @@ When publishing your Aspire application, the Golang resource automatically gener The generated Dockerfile: - Uses `golang:1.23` as the build stage to compile your application -- Uses `alpine:latest` as the runtime stage for a smaller final image +- Uses `alpine:3.21` as the runtime stage for a smaller final image - Respects your build tags if specified - Builds the executable specified in your `AddGolangApp` call From 46c54ea7a5af61919368fa16bad65bfd878f6570 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 00:57:12 +0000 Subject: [PATCH 05/10] Add DockerfileBaseImageAnnotation support and automatic Go version detection Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../GolangAppHostingExtension.cs | 103 +++++++++++++++++- .../README.md | 27 ++++- 2 files changed, 126 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs index 9332c9cdb..f9e4dc194 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs @@ -1,5 +1,11 @@ using Aspire.Hosting.ApplicationModel; using CommunityToolkit.Aspire.Utils; +using System.Diagnostics; +using System.Globalization; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; namespace Aspire.Hosting; @@ -91,6 +97,8 @@ private static IResourceBuilder PublishAsGolangDock string executable, string[]? buildTags) { + const string DefaultAlpineVersion = "3.21"; + return builder.PublishAsDockerFile(publish => { publish.WithDockerfileBuilder(workingDirectory, context => @@ -105,18 +113,109 @@ private static IResourceBuilder PublishAsGolangDock buildArgs.Add(executable); + // Get custom base image from annotation, if present + context.Resource.TryGetLastAnnotation(out var baseImageAnnotation); + var goVersion = baseImageAnnotation?.BuildImage ?? GetDefaultGoBaseImage(workingDirectory, context.Services); + var buildStage = context.Builder - .From("golang:1.23", "builder") + .From(goVersion, "builder") .WorkDir("/build") .Copy(".", "./") .Run(string.Join(" ", ["go", .. buildArgs])); + var runtimeImage = baseImageAnnotation?.RuntimeImage ?? $"alpine:{DefaultAlpineVersion}"; + context.Builder - .From("alpine:3.21") + .From(runtimeImage) .CopyFrom(buildStage.StageName!, "/app/server", "/app/server") .Entrypoint(["/app/server"]); }); }); } #pragma warning restore ASPIREDOCKERFILEBUILDER001 + + private static string GetDefaultGoBaseImage(string workingDirectory, IServiceProvider serviceProvider) + { + const string DefaultGoVersion = "1.23"; + var logger = serviceProvider.GetService>() ?? NullLogger.Instance; + var goVersion = DetectGoVersion(workingDirectory, logger) ?? DefaultGoVersion; + return $"golang:{goVersion}"; + } + + /// + /// Detects the Go version to use for a project by checking go.mod and the installed Go toolchain. + /// + /// The working directory of the Go project. + /// The logger for diagnostic messages. + /// The detected Go version as a string, or null if no version is detected. + private static string? DetectGoVersion(string workingDirectory, ILogger logger) + { + // Check go.mod file + var goModPath = Path.Combine(workingDirectory, "go.mod"); + if (File.Exists(goModPath)) + { + try + { + var goModContent = File.ReadAllText(goModPath); + // Look for "go X.Y" or "go X.Y.Z" line in go.mod + var match = Regex.Match(goModContent, @"^\s*go\s+(\d+\.\d+(?:\.\d+)?)", RegexOptions.Multiline); + if (match.Success) + { + var version = match.Groups[1].Value; + // Extract major.minor (e.g., "1.22" from "1.22.3") + var versionParts = version.Split('.'); + if (versionParts.Length >= 2) + { + var majorMinor = $"{versionParts[0]}.{versionParts[1]}"; + logger.LogDebug("Detected Go version {Version} from go.mod file", majorMinor); + return majorMinor; + } + } + } + catch (Exception ex) + { + logger.LogDebug(ex, "Failed to parse go.mod file"); + } + } + + // Try to detect from installed Go toolchain + try + { + var startInfo = new ProcessStartInfo + { + FileName = "go", + Arguments = "version", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process != null) + { + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + if (process.ExitCode == 0) + { + // Output format: "go version goX.Y.Z ..." + var match = Regex.Match(output, @"go version go(\d+\.\d+)"); + if (match.Success) + { + var version = match.Groups[1].Value; + logger.LogDebug("Detected Go version {Version} from installed toolchain", version); + return version; + } + } + } + } + catch (Exception ex) + { + logger.LogDebug(ex, "Failed to detect Go version from installed toolchain"); + } + + logger.LogDebug("No Go version detected, will use default version"); + return null; + } } \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/README.md b/src/CommunityToolkit.Aspire.Hosting.Golang/README.md index 840daf92b..a4e21ac46 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/README.md @@ -33,8 +33,31 @@ r.Run(":"+os.Getenv("PORT")) When publishing your Aspire application, the Golang resource automatically generates a multi-stage Dockerfile for containerization. This means you don't need to manually create a Dockerfile for your Golang application. -The generated Dockerfile: -- Uses `golang:1.23` as the build stage to compile your application +### Automatic Version Detection + +The integration automatically detects the Go version to use by: +1. Checking the `go.mod` file for the Go version directive +2. Falling back to the installed Go toolchain version +3. Using Go 1.23 as the default if no version is detected + +### Customizing Base Images + +You can customize the base images used in the Dockerfile by using the `DockerfileBaseImageAnnotation`: + +```csharp +var golang = builder.AddGolangApp("golang", "../gin-api") + .WithHttpEndpoint(env: "PORT") + .WithAnnotation(new DockerfileBaseImageAnnotation + { + BuildImage = "golang:1.22-alpine", // Custom build stage image + RuntimeImage = "alpine:3.20" // Custom runtime stage image + }); +``` + +### Generated Dockerfile + +The automatically generated Dockerfile: +- Uses the detected or default Go version (e.g., `golang:1.22`) as the build stage - Uses `alpine:3.21` as the runtime stage for a smaller final image - Respects your build tags if specified - Builds the executable specified in your `AddGolangApp` call From 4fb890c7908a387f26b81ed4d73b360e40fad967 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 01:08:09 +0000 Subject: [PATCH 06/10] Update README to use WithDockerfileBaseImage extension method Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- src/CommunityToolkit.Aspire.Hosting.Golang/README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/README.md b/src/CommunityToolkit.Aspire.Hosting.Golang/README.md index a4e21ac46..3d7517bbc 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/README.md @@ -42,16 +42,14 @@ The integration automatically detects the Go version to use by: ### Customizing Base Images -You can customize the base images used in the Dockerfile by using the `DockerfileBaseImageAnnotation`: +You can customize the base images used in the Dockerfile: ```csharp var golang = builder.AddGolangApp("golang", "../gin-api") .WithHttpEndpoint(env: "PORT") - .WithAnnotation(new DockerfileBaseImageAnnotation - { - BuildImage = "golang:1.22-alpine", // Custom build stage image - RuntimeImage = "alpine:3.20" // Custom runtime stage image - }); + .WithDockerfileBaseImage( + buildImage: "golang:1.22-alpine", + runtimeImage: "alpine:3.20"); ``` ### Generated Dockerfile From 65106bf2accda0e351e94a5352af7c841cfab56e Mon Sep 17 00:00:00 2001 From: Tommaso Stocchi Date: Sun, 16 Nov 2025 20:11:16 +0100 Subject: [PATCH 07/10] Update src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../GolangAppHostingExtension.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs index f9e4dc194..422df9b26 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs @@ -1,7 +1,6 @@ using Aspire.Hosting.ApplicationModel; using CommunityToolkit.Aspire.Utils; using System.Diagnostics; -using System.Globalization; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; From 2ce083538ac13f0685fbb52a0683432f71318dee Mon Sep 17 00:00:00 2001 From: tommasodotnet Date: Sun, 16 Nov 2025 20:42:45 +0100 Subject: [PATCH 08/10] fixes exec /app/server: no such file or directory --- .../GolangAppHostingExtension.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs index 422df9b26..b6269058e 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs @@ -102,7 +102,7 @@ private static IResourceBuilder PublishAsGolangDock { publish.WithDockerfileBuilder(workingDirectory, context => { - var buildArgs = new List { "build", "-o", "/app/server" }; + var buildArgs = new List { "build", "-o", "server" }; if (buildTags is { Length: > 0 }) { @@ -120,13 +120,13 @@ private static IResourceBuilder PublishAsGolangDock .From(goVersion, "builder") .WorkDir("/build") .Copy(".", "./") - .Run(string.Join(" ", ["go", .. buildArgs])); + .Run(string.Join(" ", ["CGO_ENABLED=0", "go", .. buildArgs])); var runtimeImage = baseImageAnnotation?.RuntimeImage ?? $"alpine:{DefaultAlpineVersion}"; context.Builder .From(runtimeImage) - .CopyFrom(buildStage.StageName!, "/app/server", "/app/server") + .CopyFrom(buildStage.StageName!, "/build/server", "/app/server") .Entrypoint(["/app/server"]); }); }); From 6897c21e4bd4b7769bc287ee51f9a44ff204e349 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 23:22:57 +0000 Subject: [PATCH 09/10] Add unit tests for Go version detection and fix exception handling Co-authored-by: tommasodotNET <12819039+tommasodotNET@users.noreply.github.com> --- ...munityToolkit.Aspire.Hosting.Golang.csproj | 4 + .../GolangAppHostingExtension.cs | 27 +++- .../GoVersionDetectionTests.cs | 147 ++++++++++++++++++ 3 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Golang.Tests/GoVersionDetectionTests.cs diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/CommunityToolkit.Aspire.Hosting.Golang.csproj b/src/CommunityToolkit.Aspire.Hosting.Golang/CommunityToolkit.Aspire.Hosting.Golang.csproj index 5ce3671d1..8c02b1c83 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/CommunityToolkit.Aspire.Hosting.Golang.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/CommunityToolkit.Aspire.Hosting.Golang.csproj @@ -8,6 +8,10 @@ + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs index b6269058e..1d87e9625 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs @@ -147,7 +147,7 @@ private static string GetDefaultGoBaseImage(string workingDirectory, IServicePro /// The working directory of the Go project. /// The logger for diagnostic messages. /// The detected Go version as a string, or null if no version is detected. - private static string? DetectGoVersion(string workingDirectory, ILogger logger) + internal static string? DetectGoVersion(string workingDirectory, ILogger logger) { // Check go.mod file var goModPath = Path.Combine(workingDirectory, "go.mod"); @@ -171,9 +171,17 @@ private static string GetDefaultGoBaseImage(string workingDirectory, IServicePro } } } - catch (Exception ex) + catch (IOException ex) { - logger.LogDebug(ex, "Failed to parse go.mod file"); + logger.LogDebug(ex, "Failed to parse go.mod file due to IO error"); + } + catch (UnauthorizedAccessException ex) + { + logger.LogDebug(ex, "Failed to parse go.mod file due to unauthorized access"); + } + catch (RegexMatchTimeoutException ex) + { + logger.LogDebug(ex, "Failed to parse go.mod file due to regex timeout"); } } @@ -193,8 +201,11 @@ private static string GetDefaultGoBaseImage(string workingDirectory, IServicePro using var process = Process.Start(startInfo); if (process != null) { - var output = process.StandardOutput.ReadToEnd(); + // Read both output and error asynchronously to avoid deadlock + var outputTask = process.StandardOutput.ReadToEndAsync(); + var errorTask = process.StandardError.ReadToEndAsync(); process.WaitForExit(); + var output = outputTask.GetAwaiter().GetResult(); if (process.ExitCode == 0) { @@ -209,9 +220,13 @@ private static string GetDefaultGoBaseImage(string workingDirectory, IServicePro } } } - catch (Exception ex) + catch (IOException ex) + { + logger.LogDebug(ex, "Failed to detect Go version from installed toolchain due to IO error"); + } + catch (System.ComponentModel.Win32Exception ex) { - logger.LogDebug(ex, "Failed to detect Go version from installed toolchain"); + logger.LogDebug(ex, "Failed to detect Go version from installed toolchain - go command not found or not executable"); } logger.LogDebug("No Go version detected, will use default version"); diff --git a/tests/CommunityToolkit.Aspire.Hosting.Golang.Tests/GoVersionDetectionTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Golang.Tests/GoVersionDetectionTests.cs new file mode 100644 index 000000000..f664193ea --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Golang.Tests/GoVersionDetectionTests.cs @@ -0,0 +1,147 @@ +using Aspire.Hosting; +using Microsoft.Extensions.Logging.Abstractions; + +namespace CommunityToolkit.Aspire.Hosting.Golang.Tests; + +public class GoVersionDetectionTests +{ + [Fact] + public void DetectGoVersionFromGoMod() + { + // Arrange + var workingDirectory = Path.GetFullPath(Path.Combine("..", "..", "..", "..", "..", "examples", "golang", "gin-api")); + var logger = NullLogger.Instance; + + // Act + var version = GolangAppHostingExtension.DetectGoVersion(workingDirectory, logger); + + // Assert + Assert.NotNull(version); + Assert.Equal("1.22", version); + } + + [Fact] + public void DetectGoVersionFromGoMod_NonExistentDirectory() + { + // Arrange + var workingDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var logger = NullLogger.Instance; + + // Act + var version = GolangAppHostingExtension.DetectGoVersion(workingDirectory, logger); + + // Assert - should fall back to checking installed toolchain or return null + // We don't assert a specific value because it depends on the system's Go installation + Assert.True(version == null || !string.IsNullOrEmpty(version)); + } + + [Fact] + public void DetectGoVersionFromGoMod_WithPatchVersion() + { + // Arrange - Create a temporary directory with a go.mod file + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + try + { + var goModPath = Path.Combine(tempDir, "go.mod"); + File.WriteAllText(goModPath, @"module testmodule + +go 1.21.5 + +require ( + github.com/example/package v1.0.0 +) +"); + + var logger = NullLogger.Instance; + + // Act + var version = GolangAppHostingExtension.DetectGoVersion(tempDir, logger); + + // Assert + Assert.NotNull(version); + Assert.Equal("1.21", version); + } + finally + { + // Cleanup + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, true); + } + } + } + + [Fact] + public void DetectGoVersionFromGoMod_WithMajorMinorOnly() + { + // Arrange - Create a temporary directory with a go.mod file + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + try + { + var goModPath = Path.Combine(tempDir, "go.mod"); + File.WriteAllText(goModPath, @"module testmodule + +go 1.20 + +require ( + github.com/example/package v1.0.0 +) +"); + + var logger = NullLogger.Instance; + + // Act + var version = GolangAppHostingExtension.DetectGoVersion(tempDir, logger); + + // Assert + Assert.NotNull(version); + Assert.Equal("1.20", version); + } + finally + { + // Cleanup + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, true); + } + } + } + + [Fact] + public void DetectGoVersionFromGoMod_InvalidFormat() + { + // Arrange - Create a temporary directory with an invalid go.mod file + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + try + { + var goModPath = Path.Combine(tempDir, "go.mod"); + File.WriteAllText(goModPath, @"module testmodule + +go invalid + +require ( + github.com/example/package v1.0.0 +) +"); + + var logger = NullLogger.Instance; + + // Act + var version = GolangAppHostingExtension.DetectGoVersion(tempDir, logger); + + // Assert - should fall back to checking installed toolchain or return null + Assert.True(version == null || !string.IsNullOrEmpty(version)); + } + finally + { + // Cleanup + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, true); + } + } + } +} From 8c72923a716174d084d2ba0ca1c24abfb3e7b695 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 09:38:31 +0000 Subject: [PATCH 10/10] Add CA certificates and working directory to runtime stage Co-authored-by: tommasodotNET <12819039+tommasodotNET@users.noreply.github.com> --- .../GolangAppHostingExtension.cs | 2 ++ src/CommunityToolkit.Aspire.Hosting.Golang/README.md | 1 + 2 files changed, 3 insertions(+) diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs index 1d87e9625..7e1adaa84 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs @@ -126,6 +126,8 @@ private static IResourceBuilder PublishAsGolangDock context.Builder .From(runtimeImage) + .Run("apk --no-cache add ca-certificates") + .WorkDir("/app") .CopyFrom(buildStage.StageName!, "/build/server", "/app/server") .Entrypoint(["/app/server"]); }); diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/README.md b/src/CommunityToolkit.Aspire.Hosting.Golang/README.md index 3d7517bbc..e7de82f24 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/README.md @@ -57,6 +57,7 @@ var golang = builder.AddGolangApp("golang", "../gin-api") The automatically generated Dockerfile: - Uses the detected or default Go version (e.g., `golang:1.22`) as the build stage - Uses `alpine:3.21` as the runtime stage for a smaller final image +- Installs CA certificates in the runtime image for HTTPS support - Respects your build tags if specified - Builds the executable specified in your `AddGolangApp` call