From 4b9a046aad65d23f6eb8e7db8cef1e4388a3b0af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:53:07 +0000 Subject: [PATCH 1/3] Initial plan From 5cfdb7e6c9e9791672877e824f5d220d7d025171 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 15:13:42 +0000 Subject: [PATCH 2/3] Fix DockerComposeEnvironment image name duplication in EnvFile Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- src/Aspire.Hosting.Docker/EnvFile.cs | 27 ++- .../EnvFileTests.cs | 157 ++++++++++++++++++ 2 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 tests/Aspire.Hosting.Docker.Tests/EnvFileTests.cs diff --git a/src/Aspire.Hosting.Docker/EnvFile.cs b/src/Aspire.Hosting.Docker/EnvFile.cs index a95d668bf8e..b25e2e6d0ff 100644 --- a/src/Aspire.Hosting.Docker/EnvFile.cs +++ b/src/Aspire.Hosting.Docker/EnvFile.cs @@ -35,10 +35,33 @@ public static EnvFile Load(string path) public void Add(string key, string? value, string? comment, bool onlyIfMissing = true) { - if (onlyIfMissing && _keys.Contains(key)) + if (_keys.Contains(key)) { - return; + if (onlyIfMissing) + { + return; + } + + // Update the existing key's value + for (int i = 0; i < _lines.Count; i++) + { + var trimmed = _lines[i].TrimStart(); + if (!trimmed.StartsWith('#') && trimmed.Contains('=')) + { + var eqIndex = trimmed.IndexOf('='); + if (eqIndex > 0) + { + var lineKey = trimmed[..eqIndex].Trim(); + if (lineKey == key) + { + _lines[i] = value is not null ? $"{key}={value}" : $"{key}="; + return; + } + } + } + } } + if (!string.IsNullOrWhiteSpace(comment)) { _lines.Add($"# {comment}"); diff --git a/tests/Aspire.Hosting.Docker.Tests/EnvFileTests.cs b/tests/Aspire.Hosting.Docker.Tests/EnvFileTests.cs new file mode 100644 index 00000000000..58d23741b61 --- /dev/null +++ b/tests/Aspire.Hosting.Docker.Tests/EnvFileTests.cs @@ -0,0 +1,157 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.Docker.Tests; + +public class EnvFileTests +{ + [Fact] + public void Add_WithOnlyIfMissingTrue_DoesNotAddDuplicate() + { + using var tempDir = new TempDirectory(); + var envFilePath = Path.Combine(tempDir.Path, ".env"); + + // Create initial .env file + File.WriteAllLines(envFilePath, [ + "# Comment for KEY1", + "KEY1=value1", + "" + ]); + + // Load and try to add the same key with onlyIfMissing=true + var envFile = EnvFile.Load(envFilePath); + envFile.Add("KEY1", "value2", "New comment", onlyIfMissing: true); + envFile.Save(envFilePath); + + var lines = File.ReadAllLines(envFilePath); + var keyLines = lines.Where(l => l.StartsWith("KEY1=")).ToArray(); + + // Should still have only one KEY1 line with original value + Assert.Single(keyLines); + Assert.Equal("KEY1=value1", keyLines[0]); + } + + [Fact] + public void Add_WithOnlyIfMissingFalse_UpdatesExistingKey() + { + using var tempDir = new TempDirectory(); + var envFilePath = Path.Combine(tempDir.Path, ".env"); + + // Create initial .env file + File.WriteAllLines(envFilePath, [ + "# Comment for KEY1", + "KEY1=value1", + "" + ]); + + // Load and try to add the same key with onlyIfMissing=false + var envFile = EnvFile.Load(envFilePath); + envFile.Add("KEY1", "value2", "New comment", onlyIfMissing: false); + envFile.Save(envFilePath); + + var lines = File.ReadAllLines(envFilePath); + var keyLines = lines.Where(l => l.StartsWith("KEY1=")).ToArray(); + + // Should still have only one KEY1 line, but with updated value + Assert.Single(keyLines); + Assert.Equal("KEY1=value2", keyLines[0]); + } + + [Fact] + public void Add_WithOnlyIfMissingFalse_UpdatesImageNameWithoutDuplication() + { + using var tempDir = new TempDirectory(); + var envFilePath = Path.Combine(tempDir.Path, ".env"); + + // Create initial .env file simulating a project resource + File.WriteAllLines(envFilePath, [ + "# Default container port for project1", + "PROJECT1_PORT=8080", + "", + "# Container image name for project1", + "PROJECT1_IMAGE=project1:latest", + "" + ]); + + // Load the file + var envFile = EnvFile.Load(envFilePath); + + // Add PORT with onlyIfMissing=true (should be skipped since it exists) + envFile.Add("PROJECT1_PORT", "8080", "Default container port for project1", onlyIfMissing: true); + + // Add IMAGE with onlyIfMissing=false (should update the existing value) + envFile.Add("PROJECT1_IMAGE", "project1:1.0.0", "Container image name for project1", onlyIfMissing: false); + + envFile.Save(envFilePath); + + var lines = File.ReadAllLines(envFilePath); + var imageLines = lines.Where(l => l.StartsWith("PROJECT1_IMAGE=")).ToArray(); + + // Should have exactly one IMAGE line with the new value + Assert.Single(imageLines); + Assert.Equal("PROJECT1_IMAGE=project1:1.0.0", imageLines[0]); + + // PORT should also still be present once + var portLines = lines.Where(l => l.StartsWith("PROJECT1_PORT=")).ToArray(); + Assert.Single(portLines); + Assert.Equal("PROJECT1_PORT=8080", portLines[0]); + } + + [Fact] + public void Add_NewKey_AddsToFile() + { + using var tempDir = new TempDirectory(); + var envFilePath = Path.Combine(tempDir.Path, ".env"); + + // Create initial .env file + File.WriteAllLines(envFilePath, [ + "# Comment for KEY1", + "KEY1=value1", + "" + ]); + + // Load and add a new key + var envFile = EnvFile.Load(envFilePath); + envFile.Add("KEY2", "value2", "Comment for KEY2", onlyIfMissing: true); + envFile.Save(envFilePath); + + var lines = File.ReadAllLines(envFilePath); + + // Should have both keys + Assert.Contains("KEY1=value1", lines); + Assert.Contains("KEY2=value2", lines); + } + + [Fact] + public void Load_EmptyFile_ReturnsEmptyEnvFile() + { + using var tempDir = new TempDirectory(); + var envFilePath = Path.Combine(tempDir.Path, ".env"); + + // Create empty file + File.WriteAllText(envFilePath, string.Empty); + + var envFile = EnvFile.Load(envFilePath); + envFile.Add("KEY1", "value1", "Comment"); + envFile.Save(envFilePath); + + var lines = File.ReadAllLines(envFilePath); + Assert.Contains("KEY1=value1", lines); + } + + [Fact] + public void Load_NonExistentFile_ReturnsEmptyEnvFile() + { + using var tempDir = new TempDirectory(); + var envFilePath = Path.Combine(tempDir.Path, ".env"); + + // Don't create the file + var envFile = EnvFile.Load(envFilePath); + envFile.Add("KEY1", "value1", "Comment"); + envFile.Save(envFilePath); + + Assert.True(File.Exists(envFilePath)); + var lines = File.ReadAllLines(envFilePath); + Assert.Contains("KEY1=value1", lines); + } +} From f2a1504ca7a6a9dfa314a88544405a3f2bed4555 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 05:22:46 +0000 Subject: [PATCH 3/3] Refactor EnvFile to extract duplicate parsing logic into helper method Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- src/Aspire.Hosting.Docker/EnvFile.cs | 41 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/Aspire.Hosting.Docker/EnvFile.cs b/src/Aspire.Hosting.Docker/EnvFile.cs index b25e2e6d0ff..a5283127223 100644 --- a/src/Aspire.Hosting.Docker/EnvFile.cs +++ b/src/Aspire.Hosting.Docker/EnvFile.cs @@ -19,15 +19,9 @@ public static EnvFile Load(string path) foreach (var line in File.ReadAllLines(path)) { envFile._lines.Add(line); - var trimmed = line.TrimStart(); - if (!trimmed.StartsWith('#') && trimmed.Contains('=')) + if (TryParseKey(line, out var key)) { - var eqIndex = trimmed.IndexOf('='); - if (eqIndex > 0) - { - var key = trimmed[..eqIndex].Trim(); - envFile._keys.Add(key); - } + envFile._keys.Add(key); } } return envFile; @@ -45,19 +39,10 @@ public void Add(string key, string? value, string? comment, bool onlyIfMissing = // Update the existing key's value for (int i = 0; i < _lines.Count; i++) { - var trimmed = _lines[i].TrimStart(); - if (!trimmed.StartsWith('#') && trimmed.Contains('=')) + if (TryParseKey(_lines[i], out var lineKey) && lineKey == key) { - var eqIndex = trimmed.IndexOf('='); - if (eqIndex > 0) - { - var lineKey = trimmed[..eqIndex].Trim(); - if (lineKey == key) - { - _lines[i] = value is not null ? $"{key}={value}" : $"{key}="; - return; - } - } + _lines[i] = value is not null ? $"{key}={value}" : $"{key}="; + return; } } } @@ -71,6 +56,22 @@ public void Add(string key, string? value, string? comment, bool onlyIfMissing = _keys.Add(key); } + private static bool TryParseKey(string line, out string key) + { + key = string.Empty; + var trimmed = line.TrimStart(); + if (!trimmed.StartsWith('#') && trimmed.Contains('=')) + { + var eqIndex = trimmed.IndexOf('='); + if (eqIndex > 0) + { + key = trimmed[..eqIndex].Trim(); + return true; + } + } + return false; + } + public void Save(string path) { File.WriteAllLines(path, _lines);