From 47ad2c9f500787151235bdbdd4f2b6815625acb0 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Fri, 12 Dec 2025 12:12:30 -0500 Subject: [PATCH 1/5] Add tests for package version mismatch detection and improve reporting accuracy --- .../InternalsVisibleTo.Test.CodeMedic.cs | 3 + src/CodeMedic/Output/ConsoleRenderer.cs | 27 +++- .../Plugins/BomAnalysis/BomAnalysisPlugin.cs | 7 +- .../HealthAnalysis/RepositoryScanner.cs | 81 +++++++----- .../RepositoryScannerVersionMismatchTests.cs | 116 ++++++++++++++++++ 5 files changed, 193 insertions(+), 41 deletions(-) create mode 100644 src/CodeMedic/InternalsVisibleTo.Test.CodeMedic.cs create mode 100644 test/Test.CodeMedic/Plugins/HealthAnalysis/RepositoryScannerVersionMismatchTests.cs diff --git a/src/CodeMedic/InternalsVisibleTo.Test.CodeMedic.cs b/src/CodeMedic/InternalsVisibleTo.Test.CodeMedic.cs new file mode 100644 index 0000000..a294df9 --- /dev/null +++ b/src/CodeMedic/InternalsVisibleTo.Test.CodeMedic.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Test.CodeMedic")] diff --git a/src/CodeMedic/Output/ConsoleRenderer.cs b/src/CodeMedic/Output/ConsoleRenderer.cs index e1466d8..558033c 100644 --- a/src/CodeMedic/Output/ConsoleRenderer.cs +++ b/src/CodeMedic/Output/ConsoleRenderer.cs @@ -234,7 +234,8 @@ private void RenderTable(ReportTable reportTable) { var table = new Table { - Border = TableBorder.Rounded + Border = TableBorder.Rounded, + Expand = true }; if (!string.IsNullOrWhiteSpace(reportTable.Title)) @@ -242,10 +243,30 @@ private void RenderTable(ReportTable reportTable) table.Title = new TableTitle($"[bold]{reportTable.Title}[/]"); } - // Add columns + // Add columns (wrap long-text columns; keep short columns compact) + var noWrapHeaders = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Version", + "Latest", + "Type", + "License", + "Source", + "Comm", + "Severity", + "Score", + "Count", + "Status" + }; + foreach (var header in reportTable.Headers) { - table.AddColumn(header); + var column = new TableColumn(header); + if (noWrapHeaders.Contains(header)) + { + column.NoWrap(); + } + + table.AddColumn(column); } // Add rows diff --git a/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs b/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs index 658db83..037ef9e 100644 --- a/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs +++ b/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs @@ -309,8 +309,8 @@ private async Task> AddNuGetPackagesSectionAsync latestVersionDisplay = "Current"; } - // Truncate package names if too long to improve table formatting - var displayName = package.Name.Length > 25 ? package.Name.Substring(0, 22) + "..." : package.Name; + // Keep full package names/licenses for report accuracy. + var displayName = package.Name; // Shorten source type and commercial status for better formatting var sourceType = package.SourceType == "Open Source" ? "Open" : @@ -320,8 +320,7 @@ private async Task> AddNuGetPackagesSectionAsync var commercial = package.Commercial == "Unknown" ? "?" : package.Commercial == "Yes" ? "Y" : "N"; - // Truncate license if too long - var license = package.License?.Length > 12 ? package.License.Substring(0, 9) + "..." : package.License ?? "Unknown"; + var license = package.License ?? "Unknown"; packagesTable.AddRow( displayName, diff --git a/src/CodeMedic/Plugins/HealthAnalysis/RepositoryScanner.cs b/src/CodeMedic/Plugins/HealthAnalysis/RepositoryScanner.cs index 8ab6430..ce86811 100644 --- a/src/CodeMedic/Plugins/HealthAnalysis/RepositoryScanner.cs +++ b/src/CodeMedic/Plugins/HealthAnalysis/RepositoryScanner.cs @@ -171,7 +171,7 @@ public ReportDocument GenerateReport(bool limitPackageLists = true) { var versionDetails = string.Join(", ", mismatch.ProjectVersions .OrderBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase) - .Select(kv => $"{kv.Key}={kv.Value}")); + .Select(kv => $"{kv.Key}={string.Join("|", kv.Value.OrderBy(v => v, StringComparer.OrdinalIgnoreCase))}")); mismatchList.AddItem($"{mismatch.PackageName}: {versionDetails}"); } @@ -651,45 +651,58 @@ private static string InferDefaultCSharpVersion(string? targetFramework) } private List FindPackageVersionMismatches() - { - var packageVersions = new Dictionary>(StringComparer.OrdinalIgnoreCase); + => ComputePackageVersionMismatches(_projects); - foreach (var project in _projects) - { - foreach (var package in project.PackageDependencies) + internal static List ComputePackageVersionMismatches(IEnumerable projects) + { + var all = projects + .SelectMany(project => { - if (string.IsNullOrWhiteSpace(package.Name) || package.Name.Equals("unknown", StringComparison.OrdinalIgnoreCase)) - { - continue; - } + var projectName = string.IsNullOrWhiteSpace(project.ProjectName) ? "unknown" : project.ProjectName; - if (string.IsNullOrWhiteSpace(package.Version) || package.Version.Equals("unknown", StringComparison.OrdinalIgnoreCase)) - { - continue; - } + var direct = project.PackageDependencies + .Select(p => (Name: p.Name, Version: p.Version, Project: projectName)); - if (!packageVersions.TryGetValue(package.Name, out var versionsByProject)) - { - versionsByProject = new Dictionary(StringComparer.OrdinalIgnoreCase); - packageVersions[package.Name] = versionsByProject; - } + var transitive = project.TransitiveDependencies + .Select(t => (Name: t.PackageName, Version: t.Version, Project: projectName)); - versionsByProject[project.ProjectName] = package.Version; - } - } - - var mismatches = new List(); + return direct.Concat(transitive); + }) + .Where(x => + !string.IsNullOrWhiteSpace(x.Name) && + !x.Name.Equals("unknown", StringComparison.OrdinalIgnoreCase) && + !string.IsNullOrWhiteSpace(x.Version) && + !x.Version.Equals("unknown", StringComparison.OrdinalIgnoreCase)); - foreach (var kvp in packageVersions) - { - var distinctVersions = kvp.Value.Values.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); - if (distinctVersions.Count > 1) + return all + .GroupBy(x => x.Name, StringComparer.OrdinalIgnoreCase) + .Select(group => { - mismatches.Add(new PackageVersionMismatch(kvp.Key, kvp.Value)); - } - } - - return mismatches; + var versionsByProject = group + .GroupBy(x => x.Project, StringComparer.OrdinalIgnoreCase) + .ToDictionary( + g => g.Key, + g => (IReadOnlyCollection)g + .Select(x => x.Version) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(), + StringComparer.OrdinalIgnoreCase); + + var distinctVersions = versionsByProject.Values + .SelectMany(v => v) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + return new + { + PackageName = group.Key, + VersionsByProject = versionsByProject, + DistinctVersionCount = distinctVersions.Count + }; + }) + .Where(x => x.DistinctVersionCount > 1) + .Select(x => new PackageVersionMismatch(x.PackageName, x.VersionsByProject)) + .ToList(); } /// @@ -732,7 +745,7 @@ private async Task CollectVulnerabilitiesAsync(CancellationToken cancellationTok } } - private sealed record PackageVersionMismatch(string PackageName, Dictionary ProjectVersions); + internal sealed record PackageVersionMismatch(string PackageName, Dictionary> ProjectVersions); /// /// Counts total lines of code in all C# files included in a project, excluding blank lines and comments. diff --git a/test/Test.CodeMedic/Plugins/HealthAnalysis/RepositoryScannerVersionMismatchTests.cs b/test/Test.CodeMedic/Plugins/HealthAnalysis/RepositoryScannerVersionMismatchTests.cs new file mode 100644 index 0000000..cf4410e --- /dev/null +++ b/test/Test.CodeMedic/Plugins/HealthAnalysis/RepositoryScannerVersionMismatchTests.cs @@ -0,0 +1,116 @@ +using CodeMedic.Models; +using CodeMedic.Plugins.HealthAnalysis; + +namespace Test.CodeMedic.Plugins.HealthAnalysis; + +public class RepositoryScannerVersionMismatchTests +{ + [Fact] + public void ComputePackageVersionMismatches_WhenOnlyTransitiveMismatchesExist_ThenReportsMismatch() + { + var projects = new List + { + new() + { + ProjectPath = "C:/repo/A/A.csproj", + ProjectName = "A", + RelativePath = "A/A.csproj", + TransitiveDependencies = + [ + new TransitiveDependency { PackageName = "Newtonsoft.Json", Version = "13.0.1" } + ] + }, + new() + { + ProjectPath = "C:/repo/B/B.csproj", + ProjectName = "B", + RelativePath = "B/B.csproj", + TransitiveDependencies = + [ + new TransitiveDependency { PackageName = "Newtonsoft.Json", Version = "13.0.3" } + ] + } + }; + + var mismatches = RepositoryScanner.ComputePackageVersionMismatches(projects); + + Assert.Single(mismatches); + Assert.Equal("Newtonsoft.Json", mismatches[0].PackageName, ignoreCase: true); + + var perProject = mismatches[0].ProjectVersions; + Assert.Equal(2, perProject.Count); + Assert.Contains("13.0.1", perProject["A"]); + Assert.Contains("13.0.3", perProject["B"]); + } + + [Fact] + public void ComputePackageVersionMismatches_WhenAllProjectsUseSameTransitiveVersion_ThenNoMismatch() + { + var projects = new List + { + new() + { + ProjectPath = "C:/repo/A/A.csproj", + ProjectName = "A", + RelativePath = "A/A.csproj", + TransitiveDependencies = + [ + new TransitiveDependency { PackageName = "Serilog", Version = "4.0.0" } + ] + }, + new() + { + ProjectPath = "C:/repo/B/B.csproj", + ProjectName = "B", + RelativePath = "B/B.csproj", + TransitiveDependencies = + [ + new TransitiveDependency { PackageName = "Serilog", Version = "4.0.0" } + ] + } + }; + + var mismatches = RepositoryScanner.ComputePackageVersionMismatches(projects); + + Assert.Empty(mismatches); + } + + [Fact] + public void ComputePackageVersionMismatches_WhenAProjectHasMultipleResolvedVersions_ThenShowsAllVersionsForThatProject() + { + var projects = new List + { + new() + { + ProjectPath = "C:/repo/A/A.csproj", + ProjectName = "A", + RelativePath = "A/A.csproj", + TransitiveDependencies = + [ + new TransitiveDependency { PackageName = "Example.Package", Version = "1.0.0" }, + new TransitiveDependency { PackageName = "Example.Package", Version = "2.0.0" } + ] + }, + new() + { + ProjectPath = "C:/repo/B/B.csproj", + ProjectName = "B", + RelativePath = "B/B.csproj", + TransitiveDependencies = + [ + new TransitiveDependency { PackageName = "Example.Package", Version = "1.0.0" } + ] + } + }; + + var mismatches = RepositoryScanner.ComputePackageVersionMismatches(projects); + + Assert.Single(mismatches); + Assert.Equal("Example.Package", mismatches[0].PackageName, ignoreCase: true); + + Assert.Equal(2, mismatches[0].ProjectVersions["A"].Count); + Assert.Contains("1.0.0", mismatches[0].ProjectVersions["A"]); + Assert.Contains("2.0.0", mismatches[0].ProjectVersions["A"]); + Assert.Single(mismatches[0].ProjectVersions["B"]); + } +} From 874ae812232e002bc97d190b795fe8cd758e42e1 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Fri, 12 Dec 2025 12:22:57 -0500 Subject: [PATCH 2/5] Enhance Docker workflows and add NativeAOT release process; improve plugin loading for NativeAOT compatibility --- .github/workflows/docker-publish.yml | 25 +++- .github/workflows/release-binaries.yml | 123 ++++++++++++++++++ src/CodeMedic/CodeMedic.csproj | 16 +++ .../Plugins/BomAnalysis/BomAnalysisPlugin.cs | 39 ++++-- src/CodeMedic/Utilities/PluginLoader.cs | 72 ++++++---- 5 files changed, 235 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/release-binaries.yml diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 6452261..1d78421 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -13,7 +13,7 @@ jobs: build-and-push: runs-on: ubuntu-latest permissions: - contents: read + contents: write packages: write steps: @@ -45,6 +45,7 @@ jobs: type=raw,value=latest,enable={{is_default_branch}} - name: Build and push Docker image + id: build uses: docker/build-push-action@v5 with: context: . @@ -59,3 +60,25 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max platforms: linux/amd64,linux/arm64 + + - name: Create release image reference + if: github.event_name == 'release' + shell: bash + run: | + set -euo pipefail + { + echo "CodeMedic container image" + echo "Release: ${{ github.event.release.tag_name }}" + echo "Repository: ${{ github.repository }}" + echo "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" + echo "Digest: ${{ steps.build.outputs.digest }}" + echo "" + echo "Tags:" + echo "${{ steps.meta.outputs.tags }}" | sed 's/^/ - /' + } > codemedic-container-image.txt + + - name: Attach image reference to release + if: github.event_name == 'release' + uses: softprops/action-gh-release@v2 + with: + files: codemedic-container-image.txt diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml new file mode 100644 index 0000000..cab20bc --- /dev/null +++ b/.github/workflows/release-binaries.yml @@ -0,0 +1,123 @@ +name: Release Binaries (NativeAOT) + +on: + release: + types: [published] + workflow_dispatch: + +env: + DOTNET_NOLOGO: true + +jobs: + publish-aot: + name: Publish NativeAOT (${{ matrix.rid }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + rid: win-x64 + ext: .exe + - os: ubuntu-latest + rid: linux-x64 + ext: "" + - os: ubuntu-latest + rid: linux-arm64 + ext: "" + needsCross: true + - os: macos-13 + rid: osx-x64 + ext: "" + - os: macos-latest + rid: osx-arm64 + ext: "" + + permissions: + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + - name: Install NativeAOT prerequisites (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y clang zlib1g-dev + + - name: Install cross toolchain (linux-arm64) + if: runner.os == 'Linux' && matrix.needsCross + shell: bash + run: | + set -euo pipefail + sudo apt-get install -y gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu + + - name: Restore + run: dotnet restore src/CodeMedic.sln + + - name: Publish (NativeAOT) + run: >- + dotnet publish src/CodeMedic/CodeMedic.csproj + -c Release + -r ${{ matrix.rid }} + /p:PublishAot=true + + - name: Prepare artifact + shell: bash + run: | + set -euo pipefail + VERSION="${{ github.event.release.tag_name }}" + if [ -z "$VERSION" ]; then VERSION="manual"; fi + + OUTDIR="src/CodeMedic/bin/Release/net10.0/${{ matrix.rid }}/publish" + BIN="$OUTDIR/CodeMedic${{ matrix.ext }}" + + if [ ! -f "$BIN" ]; then + echo "Expected binary not found at $BIN" + echo "Publish directory contents:" + ls -la "$OUTDIR" || true + exit 1 + fi + + mkdir -p artifacts + DEST="artifacts/CodeMedic-${VERSION}-${{ matrix.rid }}${{ matrix.ext }}" + cp "$BIN" "$DEST" + + - name: Generate checksums (SHA-256) + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + + $files = Get-ChildItem -LiteralPath 'artifacts' -File | Where-Object { $_.Name -notmatch '\.sha256$' } + if (-not $files -or $files.Count -eq 0) { + throw 'No artifacts found to checksum.' + } + + foreach ($file in $files) { + $hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $file.FullName).Hash.ToLowerInvariant() + $line = "$hash $($file.Name)" + $checksumPath = "$($file.FullName).sha256" + $line | Out-File -FilePath $checksumPath -Encoding ascii + } + + - name: Upload workflow artifact + uses: actions/upload-artifact@v4 + with: + name: CodeMedic-${{ matrix.rid }} + path: artifacts/* + + - name: Attach binaries to GitHub Release + if: github.event_name == 'release' + uses: softprops/action-gh-release@v2 + with: + files: artifacts/* diff --git a/src/CodeMedic/CodeMedic.csproj b/src/CodeMedic/CodeMedic.csproj index f4741c8..809b90f 100644 --- a/src/CodeMedic/CodeMedic.csproj +++ b/src/CodeMedic/CodeMedic.csproj @@ -9,6 +9,22 @@ true Linux ..\.. + + + win-x64;linux-x64;linux-arm64;osx-x64;osx-arm64 + + + + + true + true + true + true + + + none + false + false diff --git a/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs b/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs index 037ef9e..b181431 100644 --- a/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs +++ b/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs @@ -526,19 +526,36 @@ private async Task FetchLatestVersionForPackageAsync(PackageInfo package) var apiUrl = $"https://api.nuget.org/v3-flatcontainer/{package.Name.ToLowerInvariant()}/index.json"; var response = await httpClient.GetStringAsync(apiUrl); - var versionData = JsonSerializer.Deserialize(response, new JsonSerializerOptions + + using var doc = JsonDocument.Parse(response); + if (doc.RootElement.ValueKind != JsonValueKind.Object) { - PropertyNameCaseInsensitive = true - }); - - if (versionData?.Versions?.Length > 0) + return; + } + + if (!doc.RootElement.TryGetProperty("versions", out var versionsElement) || + versionsElement.ValueKind != JsonValueKind.Array) + { + return; + } + + var versions = new List(); + foreach (var element in versionsElement.EnumerateArray()) + { + if (element.ValueKind == JsonValueKind.String) + { + var value = element.GetString(); + if (!string.IsNullOrWhiteSpace(value)) + { + versions.Add(value); + } + } + } + + if (versions.Count > 0) { - // Get the latest stable version (not pre-release) - var latestVersion = versionData.Versions - .Where(v => !IsPreReleaseVersion(v)) - .LastOrDefault() ?? versionData.Versions.Last(); - - package.LatestVersion = latestVersion; + var latestStable = versions.Where(v => !IsPreReleaseVersion(v)).LastOrDefault(); + package.LatestVersion = latestStable ?? versions.Last(); } } catch (HttpRequestException ex) diff --git a/src/CodeMedic/Utilities/PluginLoader.cs b/src/CodeMedic/Utilities/PluginLoader.cs index 32a6b37..a4326df 100644 --- a/src/CodeMedic/Utilities/PluginLoader.cs +++ b/src/CodeMedic/Utilities/PluginLoader.cs @@ -1,5 +1,9 @@ +using System.Diagnostics.CodeAnalysis; using System.Reflection; using CodeMedic.Abstractions.Plugins; +using CodeMedic.Plugins.BomAnalysis; +using CodeMedic.Plugins.HealthAnalysis; +using CodeMedic.Plugins.VulnerabilityAnalysis; namespace CodeMedic.Utilities; @@ -33,8 +37,19 @@ public class PluginLoader /// Cancellation token for async operations. public async Task LoadInternalPluginsAsync(CancellationToken cancellationToken = default) { - var assembly = Assembly.GetExecutingAssembly(); - await LoadPluginsFromAssemblyAsync(assembly, cancellationToken); + // NativeAOT + trimming are not compatible with reflection-based plugin discovery. + // Internal plugins are known at compile-time, so register them explicitly. + var plugins = new IPlugin[] + { + new BomAnalysisPlugin(), + new HealthAnalysisPlugin(), + new VulnerabilityAnalysisPlugin() + }; + + foreach (var plugin in plugins) + { + await LoadPluginInstanceAsync(plugin, cancellationToken); + } } /// @@ -42,6 +57,8 @@ public async Task LoadInternalPluginsAsync(CancellationToken cancellationToken = /// /// The assembly to scan for plugins. /// Cancellation token. + [UnconditionalSuppressMessage("Trimming", "IL2026:Assembly.GetTypes", Justification = "Optional reflection-based plugin discovery is not used for NativeAOT publishing.")] + [UnconditionalSuppressMessage("Trimming", "IL2072:Activator.CreateInstance", Justification = "Optional reflection-based plugin discovery is not used for NativeAOT publishing.")] private async Task LoadPluginsFromAssemblyAsync(Assembly assembly, CancellationToken cancellationToken) { var pluginTypes = assembly.GetTypes() @@ -51,41 +68,40 @@ private async Task LoadPluginsFromAssemblyAsync(Assembly assembly, CancellationT { try { - // Create instance of the plugin var plugin = Activator.CreateInstance(pluginType) as IPlugin; - if (plugin == null) + if (plugin != null) { - continue; + await LoadPluginInstanceAsync(plugin, cancellationToken); } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Failed to load plugin {pluginType.Name}: {ex.Message}"); + } + } + } - // Initialize the plugin - await plugin.InitializeAsync(cancellationToken); + private async Task LoadPluginInstanceAsync(IPlugin plugin, CancellationToken cancellationToken) + { + await plugin.InitializeAsync(cancellationToken); - // Register the plugin based on its type - if (plugin is IAnalysisEnginePlugin analysisEngine) - { - _analysisEngines.Add(analysisEngine); - - // Register commands if the plugin provides them - var commands = analysisEngine.RegisterCommands(); - if (commands != null) - { - foreach (var command in commands) - { - _commandRegistrations[command.Name] = command; - } - } - } + if (plugin is IAnalysisEnginePlugin analysisEngine) + { + _analysisEngines.Add(analysisEngine); - if (plugin is IReporterPlugin reporter) + var commands = analysisEngine.RegisterCommands(); + if (commands != null) + { + foreach (var command in commands) { - _reporters.Add(reporter); + _commandRegistrations[command.Name] = command; } } - catch (Exception ex) - { - Console.Error.WriteLine($"Failed to load plugin {pluginType.Name}: {ex.Message}"); - } + } + + if (plugin is IReporterPlugin reporter) + { + _reporters.Add(reporter); } } From 858d81d8ee2064b36e8f9ec8cb6664d015a52cce Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Mon, 15 Dec 2025 11:39:25 -0500 Subject: [PATCH 3/5] Add support for Windows ARM64 in NativeAOT publishing workflow --- .github/workflows/release-binaries.yml | 3 +++ src/CodeMedic/CodeMedic.csproj | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index cab20bc..f9d16e5 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -19,6 +19,9 @@ jobs: - os: windows-latest rid: win-x64 ext: .exe + - os: windows-latest + rid: win-arm64 + ext: .exe - os: ubuntu-latest rid: linux-x64 ext: "" diff --git a/src/CodeMedic/CodeMedic.csproj b/src/CodeMedic/CodeMedic.csproj index 809b90f..a562f1e 100644 --- a/src/CodeMedic/CodeMedic.csproj +++ b/src/CodeMedic/CodeMedic.csproj @@ -11,7 +11,7 @@ ..\.. - win-x64;linux-x64;linux-arm64;osx-x64;osx-arm64 + win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64 From 75e02fa71f9e48d077d130a785ed2401554b75ac Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Tue, 16 Dec 2025 09:34:49 -0500 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=90=92=20Chaos=20Monkey:=20Add=20goof?= =?UTF-8?q?y=20placeholder=20test=20for=20ergonrod's=20donation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added CommandArgument_WhenChaosMonkeyStrikesWithBananas_ThenShouldStillWorkPerfectly test method - Test validates CommandArgument functionality with banana-themed parameters - Includes proper assertions and equality checks for comprehensive coverage - All tests pass and build remains stable - Donation ID: ergonrod - Chaos instruction: Insert goofy placeholder test --- .../Plugins/CommandArgumentTests.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/Test.CodeMedic/Plugins/CommandArgumentTests.cs b/test/Test.CodeMedic/Plugins/CommandArgumentTests.cs index f6a6753..06d60e6 100644 --- a/test/Test.CodeMedic/Plugins/CommandArgumentTests.cs +++ b/test/Test.CodeMedic/Plugins/CommandArgumentTests.cs @@ -139,4 +139,42 @@ public void CommandArgument_GivenVariousParameters_WhenCreated_ThenMatchesExpect Assert.Equal(isRequired, argument.IsRequired); Assert.Equal(valueName, argument.ValueName); } + + [Fact] + public void CommandArgument_WhenChaosMonkeyStrikesWithBananas_ThenShouldStillWorkPerfectly() + { + // 🐒 Chaos Monkey was here! This goofy test brought to you by ergonrod's donation! + // Given: A completely ridiculous but technically valid command argument + var bananaArgument = new CommandArgument( + Description: "The number of bananas required for optimal monkey performance", + ShortName: "b", + LongName: "bananas", + IsRequired: false, + HasValue: true, + DefaultValue: "42", // Because it's the answer to everything + ValueName: "banana-count"); + + // When: We test this completely silly but functional argument + // Then: It should work exactly like any other argument (because code doesn't care about our jokes!) + Assert.Equal("The number of bananas required for optimal monkey performance", bananaArgument.Description); + Assert.Equal("b", bananaArgument.ShortName); + Assert.Equal("bananas", bananaArgument.LongName); + Assert.False(bananaArgument.IsRequired); // Bananas are optional but recommended + Assert.True(bananaArgument.HasValue); + Assert.Equal("42", bananaArgument.DefaultValue); + Assert.Equal("banana-count", bananaArgument.ValueName); + + // 🐒 Bonus assertion: Verify that even silly arguments maintain their equality contracts + var anotherBananaArgument = new CommandArgument( + Description: "The number of bananas required for optimal monkey performance", + ShortName: "b", + LongName: "bananas", + IsRequired: false, + HasValue: true, + DefaultValue: "42", + ValueName: "banana-count"); + + Assert.Equal(bananaArgument, anotherBananaArgument); + // Because two identical banana arguments should always be equal, obviously! 🍌 + } } \ No newline at end of file From c5adf175c457ff5945c3eb7f19f418f091ab0009 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Tue, 16 Dec 2025 09:43:45 -0500 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=90=92=20Chaos=20Monkey:=20Rename=20t?= =?UTF-8?q?ests=20to=20ridiculous=20names=20(On=20behalf=20of=20donor:=20M?= =?UTF-8?q?cNets)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Renamed SignalR detector test to include 'LikeABossWhoKnowsHowToSignalRightAndLeft' - Renamed NuGet inspector test to include 'LikeAPackageWhispererWithSupernaturalVersionDetectionSkills' - Added entertaining chaos monkey comments with 🐒 emoji - All tests still pass and code compiles successfully Supporting St. Jude Children's Research Hospital ❤️ --- test/Test.CodeMedic/Plugins/BomAnalysis/NuGetInspectorTests.cs | 3 ++- .../Plugins/Detectors/WebFrameworkDetectorTests.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/Test.CodeMedic/Plugins/BomAnalysis/NuGetInspectorTests.cs b/test/Test.CodeMedic/Plugins/BomAnalysis/NuGetInspectorTests.cs index 2d69670..e7c807c 100644 --- a/test/Test.CodeMedic/Plugins/BomAnalysis/NuGetInspectorTests.cs +++ b/test/Test.CodeMedic/Plugins/BomAnalysis/NuGetInspectorTests.cs @@ -173,7 +173,8 @@ public void ReadPackageReferences_GivenProjectWithDirectVersions_WhenReading_The } [Fact] - public void ReadPackageReferences_GivenPackageWithVersionElement_WhenReading_ThenReadsVersionFromElement() + // 🐒 Chaos Monkey: This test now has a spectacularly ridiculous name! Thanks McNets! + public void ReadPackageReferences_GivenPackageWithVersionElement_WhenReading_ThenReadsVersionFromElement_LikeAPackageWhispererWithSupernaturalVersionDetectionSkills() { // Given var rootPath = TestRootPath; diff --git a/test/Test.CodeMedic/Plugins/Detectors/WebFrameworkDetectorTests.cs b/test/Test.CodeMedic/Plugins/Detectors/WebFrameworkDetectorTests.cs index a6e50c6..18eb869 100644 --- a/test/Test.CodeMedic/Plugins/Detectors/WebFrameworkDetectorTests.cs +++ b/test/Test.CodeMedic/Plugins/Detectors/WebFrameworkDetectorTests.cs @@ -50,7 +50,8 @@ public void DetectFeatures_WithAspNetCoreMvc_DetectsMvc() } [Fact] - public void DetectFeatures_WithSignalR_DetectsSignalR() + // 🐒 Chaos Monkey: Renamed this test to be ridiculously punny! Donation by: McNets + public void DetectFeatures_WithSignalR_DetectsSignalR_LikeABossWhoKnowsHowToSignalRightAndLeft() { // Arrange var detector = new WebFrameworkDetector();