From 9d6d48f4511cd20a0ce67faf11225f5be6301032 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Thu, 24 Sep 2026 12:02:26 +0200 Subject: [PATCH] Capture a binlog when nuget restore in gitproperties fixture fails --- .github/workflows/Steeltoe.All.yml | 3 +- .../workflows/component-shared-workflow.yml | 3 +- .github/workflows/sonarcube.yml | 14 +++++++ .../FakeSteeltoeManagementEndpointPackager.cs | 4 +- .../GitPropertiesSourcePackager.cs | 4 +- .../GitProperties.Build.Test/ProcessRunner.cs | 42 +++++++++++++++++++ 6 files changed, 64 insertions(+), 6 deletions(-) diff --git a/.github/workflows/Steeltoe.All.yml b/.github/workflows/Steeltoe.All.yml index 66cb418fb6..a95a5ea783 100644 --- a/.github/workflows/Steeltoe.All.yml +++ b/.github/workflows/Steeltoe.All.yml @@ -130,7 +130,7 @@ jobs: -RepoRoot ${{ github.workspace }} -ProjectRelativePath src/Management/src/GitProperties.Build - - name: Upload crash/hang dumps (on failure) + - name: Upload crash dumps and build diagnostics (on failure) if: ${{ !cancelled() && (steps.test.outcome == 'failure' || steps.test-memory-dumps.outcome == 'failure' || steps.test-gitproperties.outcome == 'failure') }} uses: actions/upload-artifact@v7 with: @@ -138,6 +138,7 @@ jobs: path: | ${{ github.workspace }}/TestOutput/**/*.dmp ${{ github.workspace }}/TestOutput/**/Sequence_*.xml + ${{ github.workspace }}/TestOutput/**/*.binlog if-no-files-found: ignore - name: Report test results diff --git a/.github/workflows/component-shared-workflow.yml b/.github/workflows/component-shared-workflow.yml index 29a36d49b2..e8812315c2 100644 --- a/.github/workflows/component-shared-workflow.yml +++ b/.github/workflows/component-shared-workflow.yml @@ -104,7 +104,7 @@ jobs: if: ${{ inputs.component == 'Management' }} run: dotnet test src/Management/test/Endpoint.Test --filter "Category=MemoryDumps" ${{ env.COMMON_TEST_ARGS }} - - name: Upload crash/hang dumps (on failure) + - name: Upload crash dumps and build diagnostics (on failure) if: ${{ !cancelled() && (steps.test.outcome == 'failure' || steps.test-memory-dumps.outcome == 'failure') }} uses: actions/upload-artifact@v7 with: @@ -112,6 +112,7 @@ jobs: path: | ${{ github.workspace }}/TestOutput/**/*.dmp ${{ github.workspace }}/TestOutput/**/Sequence_*.xml + ${{ github.workspace }}/TestOutput/**/*.binlog if-no-files-found: ignore - name: Report test results diff --git a/.github/workflows/sonarcube.yml b/.github/workflows/sonarcube.yml index 62d219dcca..a6bb1e0b70 100644 --- a/.github/workflows/sonarcube.yml +++ b/.github/workflows/sonarcube.yml @@ -101,12 +101,15 @@ jobs: run: dotnet build ${{ env.SOLUTION_FILE }} --no-restore --configuration Release --verbosity minimal /p:NuGetAuditLevel=low /p:WarningsNotAsErrors='${{ env.NUGET_VULNERABLE_PACKAGE_WARNINGS }}' - name: Test + id: test run: dotnet test ${{ env.SOLUTION_FILE }} --filter "Category!=MemoryDumps&Category!=GitProperties" ${{ env.COMMON_TEST_ARGS }} ${{ env.COVERAGE_COLLECT_ARGS }} - name: Test (memory dumps) + id: test-memory-dumps run: dotnet test src/Management/test/Endpoint.Test --filter "Category=MemoryDumps" ${{ env.COMMON_TEST_ARGS }} ${{ env.COVERAGE_COLLECT_ARGS }} - name: Test (git.properties) + id: test-gitproperties run: >- dotnet dotnet-coverage collect -f cobertura -o ${{ github.workspace }}/TestOutput/GitProperties.Build.Test.cobertura.xml -- dotnet test src/Management/test/GitProperties.Build.Test ${{ env.COMMON_TEST_ARGS }} @@ -119,6 +122,17 @@ jobs: -RepoRoot ${{ github.workspace }} -ProjectRelativePath src/Management/src/GitProperties.Build + - name: Upload crash dumps and build diagnostics (on failure) + if: ${{ !cancelled() && (steps.test.outcome == 'failure' || steps.test-memory-dumps.outcome == 'failure' || steps.test-gitproperties.outcome == 'failure') }} + uses: actions/upload-artifact@v7 + with: + name: FailedTestOutput-sonarcube + path: | + ${{ github.workspace }}/TestOutput/**/*.dmp + ${{ github.workspace }}/TestOutput/**/Sequence_*.xml + ${{ github.workspace }}/TestOutput/**/*.binlog + if-no-files-found: ignore + - name: End Sonar .NET scanner if: ${{ !cancelled() && steps.sonar_begin.outcome == 'success' }} env: diff --git a/src/Management/test/GitProperties.Build.Test/FakeSteeltoeManagementEndpointPackager.cs b/src/Management/test/GitProperties.Build.Test/FakeSteeltoeManagementEndpointPackager.cs index 9da79fcd44..f1ab80a69b 100644 --- a/src/Management/test/GitProperties.Build.Test/FakeSteeltoeManagementEndpointPackager.cs +++ b/src/Management/test/GitProperties.Build.Test/FakeSteeltoeManagementEndpointPackager.cs @@ -30,8 +30,8 @@ public static async Task PackAsync(DirectoryInfo sessionDirect string projectFilePath = Path.Combine(sourceDirectory, $"{PackageId}.csproj"); await File.WriteAllTextAsync(projectFilePath, projectContent, TestContext.Current.CancellationToken); - await ProcessRunner.RunDotNetAsync(sourceDirectory, 0, null, "build", "-c", "Release", $"-p:Version={PackageVersion}", - $"-p:PackageOutputPath={nuGetFeedDirectory}"); + await ProcessRunner.RunDotNetBuildWithFailureDiagnosticsAsync(sourceDirectory, "fake-endpoint-build", "build", "-c", "Release", + $"-p:Version={PackageVersion}", $"-p:PackageOutputPath={nuGetFeedDirectory}"); return new PackageReference(PackageId, PackageVersion, null); } diff --git a/src/Management/test/GitProperties.Build.Test/GitPropertiesSourcePackager.cs b/src/Management/test/GitProperties.Build.Test/GitPropertiesSourcePackager.cs index 89c48f6653..d70d7254a1 100644 --- a/src/Management/test/GitProperties.Build.Test/GitPropertiesSourcePackager.cs +++ b/src/Management/test/GitProperties.Build.Test/GitPropertiesSourcePackager.cs @@ -20,8 +20,8 @@ public static async Task PackAsync(string nuGetFeedDirectory) string sourceDirectory = Path.Combine(repositoryRoot, "src", "Management", "src", "GitProperties.Build"); string packageVersion = $"{PackageVersionPrefix}.{$"{Guid.NewGuid():N}"[..8]}"; - await ProcessRunner.RunDotNetAsync(sourceDirectory, 0, null, "build", "-c", "Release", $"-p:Version={packageVersion}", - $"-p:PackageOutputPath={nuGetFeedDirectory}"); + await ProcessRunner.RunDotNetBuildWithFailureDiagnosticsAsync(sourceDirectory, "git-properties-build", "build", "-c", "Release", + $"-p:Version={packageVersion}", $"-p:PackageOutputPath={nuGetFeedDirectory}"); return new PackageReference(PackageId, packageVersion, null); } diff --git a/src/Management/test/GitProperties.Build.Test/ProcessRunner.cs b/src/Management/test/GitProperties.Build.Test/ProcessRunner.cs index f5b224b85a..17dd2abd9d 100644 --- a/src/Management/test/GitProperties.Build.Test/ProcessRunner.cs +++ b/src/Management/test/GitProperties.Build.Test/ProcessRunner.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Text; namespace Steeltoe.Management.GitProperties.Build.Test; @@ -24,6 +25,7 @@ internal static class ProcessRunner private static readonly TimeSpan ProcessExitTimeout = TimeSpan.FromMinutes(2); private static readonly Task RealGitExecutableTask = ResolveGitExecutableAsync(); + private static readonly Task DiagnosticsDirectoryTask = ResolveDiagnosticsDirectoryAsync(); private static async Task ResolveGitExecutableAsync() { @@ -50,6 +52,46 @@ public static async Task RunGitAsync(string workingDirectory, Cancellati return output.Trim(); } + /// + /// Runs a "dotnet build" that is known to occasionally fail restore without logging a reason (MSBuild task returns false but does not log an error, for + /// example when a shared NuGet resource is briefly contended). Captures a binlog and keeps it only when the build fails, so a next occurrence can be + /// diagnosed from the CI-uploaded artifact instead of just the generic "did not log an error" message. + /// + public static async Task RunDotNetBuildWithFailureDiagnosticsAsync(string workingDirectory, string diagnosticsFileNamePrefix, + params string[] arguments) + { + string diagnosticsDirectory = await DiagnosticsDirectoryTask; + string binlogPath = Path.Combine(diagnosticsDirectory, $"{diagnosticsFileNamePrefix}-{$"{Guid.NewGuid():N}"[..8]}.binlog"); + + string[] argumentsWithBinlog = + [ + .. arguments, + $"-bl:{binlogPath}" + ]; + + string output = await RunDotNetAsync(workingDirectory, 0, null, argumentsWithBinlog); + + try + { + File.Delete(binlogPath); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + // Best-effort cleanup only: a transiently locked file (e.g. an antivirus scan) must not fail the test run. + } + + return output; + } + + private static async Task ResolveDiagnosticsDirectoryAsync([CallerFilePath] string sourceFilePath = "") + { + string sourceDirectory = Path.GetDirectoryName(sourceFilePath) ?? throw new InvalidOperationException("Could not determine the test source directory."); + string repositoryRoot = await RunGitAsync(sourceDirectory, CancellationToken.None, "rev-parse", "--show-toplevel"); + string diagnosticsDirectory = Path.Combine(repositoryRoot.Replace('/', Path.DirectorySeparatorChar), "TestOutput"); + Directory.CreateDirectory(diagnosticsDirectory); + return diagnosticsDirectory; + } + public static Task RunDotNetAsync(string workingDirectory, int exitCodeExpected, Dictionary? environmentVariables, params string[] arguments) {