diff --git a/build/RetryLedger.cs b/build/RetryLedger.cs index 342c1764e..31671f960 100644 --- a/build/RetryLedger.cs +++ b/build/RetryLedger.cs @@ -115,10 +115,41 @@ static FlakyFailure firstFailure(TestReport report) ErrorType = string.IsNullOrWhiteSpace(outcome?.ErrorType) ? null : outcome.ErrorType, // One line and bounded: this is a lead to open the log with, not a copy of it. A // multi-line reason would also break the ::warning workflow command downstream. - Reason = condense(outcome?.ErrorMessage) + Reason = condense(outcome?.ErrorMessage), + Stack = topFrames(outcome?.StackTrace) }; } + /// + /// The top frames of the failing attempt's stack, for the ledger JSON only. + /// + /// The message says what went wrong; the stack is what says where, and for a whole + /// class of flake that is the only thing that distinguishes the candidates. The MQTT + /// Broken pipe flake is the worked example: the message alone is equally consistent with a + /// teardown ordering bug, a port collision between worker processes, and a keep-alive drop under + /// CI load. Three local experiments eliminated the first two and the third needs a call site, so + /// the investigation stalls on precisely this field. + /// + /// Deliberately kept out of the step summary, the annotation and the roll-up — all three are + /// glanceable surfaces and a stack would drown them. The JSON ledger is already a downloadable + /// artifact, which is the right place for something you go looking for on purpose. + /// + static string[] topFrames(string stackTrace) + { + if (string.IsNullOrWhiteSpace(stackTrace)) return []; + + // Enough to cross a teardown/dispatch boundary and name the caller, not so much that the + // ledger becomes a copy of the log. + const int maxFrames = 12; + + return stackTrace + .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()) + .Where(x => x.Length > 0) + .Take(maxFrames) + .ToArray(); + } + /// /// Collapses a failure reason to a single bounded line, or null when there is nothing to say. /// @@ -290,5 +321,11 @@ class FlakyFailure /// The failing attempt's own error message, flattened to one bounded line. public string Reason { get; init; } + + /// + /// Top frames of the failing attempt's stack. Ledger JSON only — see + /// for why it is not rendered on the summary surfaces. + /// + public string[] Stack { get; init; } = []; } }