Show failure details in GitHub Actions step-summary collapsible sections - #10633
Show failure details in GitHub Actions step-summary collapsible sections#10633Azat Mukhametshin (azat-msft) wants to merge 51 commits into
Conversation
Each failed test in the GitHub Actions job summary is now expanded into a collapsible <details> section carrying its failure message, exception type, resolved source location and stack trace, instead of only its name. - Capture failure diagnostics in GitHubActionsSummaryReporter, resolving the source location the same way the annotation reporter does (exception call site, falling back to TestFileLocationProperty). - Propagate the diagnostics through the CI summary fragments so aggregated multi-module dotnet test runs render them too. - Bound the output twice (per value and per section) and state every truncation explicitly, so the summary stays well under GitHub's 1 MiB cap. - HTML-encode test-provided values in <summary> and pick a code fence longer than any backtick run in the body, so a hostile message cannot break out. - Add --report-gh-failure-details on|off to keep the previous compact list. Fixes #10591 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 110eb208-0496-4c66-be51-46dc51b16db5
There was a problem hiding this comment.
Pull request overview
Adds actionable failure diagnostics to GitHub Actions job summaries, including aggregated multi-module runs.
Changes:
- Captures and renders failure details in collapsible, injection-safe sections.
- Adds
--report-gh-failure-details on|offand output-size controls. - Updates tests, documentation, API baselines, and localization resources.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
test/UnitTests/Microsoft.Testing.Extensions.UnitTests/GitHubActionsSummaryReporterTests.cs |
Tests failure-detail rendering and limits. |
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs |
Updates CLI help expectations. |
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/GitHubActionsReportTests.cs |
Adds end-to-end summary tests. |
src/Platform/SharedExtensionHelpers/SummaryReporterHelpers.cs |
Adds failure diagnostics to test records. |
src/Platform/SharedExtensionHelpers/CiRunSummaryAggregation.cs |
Persists diagnostics through aggregation. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/Resources/xlf/GitHubActionsResources.zh-Hant.xlf |
Adds Traditional Chinese localization entries. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/Resources/xlf/GitHubActionsResources.zh-Hans.xlf |
Adds Simplified Chinese localization entries. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/Resources/xlf/GitHubActionsResources.tr.xlf |
Adds Turkish localization entries. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/Resources/xlf/GitHubActionsResources.ru.xlf |
Adds Russian localization entries. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/Resources/xlf/GitHubActionsResources.pt-BR.xlf |
Adds Brazilian Portuguese localization entries. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/Resources/xlf/GitHubActionsResources.pl.xlf |
Adds Polish localization entries. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/Resources/xlf/GitHubActionsResources.ko.xlf |
Adds Korean localization entries. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/Resources/xlf/GitHubActionsResources.ja.xlf |
Adds Japanese localization entries. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/Resources/xlf/GitHubActionsResources.it.xlf |
Adds Italian localization entries. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/Resources/xlf/GitHubActionsResources.fr.xlf |
Adds French localization entries. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/Resources/xlf/GitHubActionsResources.es.xlf |
Adds Spanish localization entries. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/Resources/xlf/GitHubActionsResources.de.xlf |
Adds German localization entries. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/Resources/xlf/GitHubActionsResources.cs.xlf |
Adds Czech localization entries. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/Resources/GitHubActionsResources.resx |
Defines new localized messages. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/PACKAGE.md |
Documents the new option. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/InternalAPI/InternalAPI.Unshipped.txt |
Updates GitHub reporter API baseline. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/GitHubActionsSummaryReporter.cs |
Captures and renders failure diagnostics. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/GitHubActionsSummaryArtifactPostProcessor.cs |
Applies the option during aggregation. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/GitHubActionsFailureDetails.cs |
Implements bounded collapsible rendering. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/GitHubActionsCommandLineProvider.cs |
Registers and validates the option. |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/GitHubActionsCommandLineOptions.cs |
Defines the option name. |
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/InternalAPI/InternalAPI.Unshipped.txt |
Updates shared internal API baseline. |
docs/glossary.md |
Documents detailed failure summaries. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Validation in real GitHub Actions runsValidated end-to-end in azat-msft/gh-report-validation with this build packed into that repo's local feed (extension
The headline number for the size concern: in #4, 31 failures each carrying a ~6 KB message and a 40-frame stack trace produce a 76 KB summary — roughly 7% of GitHub's 1 MiB job-summary limit — with both truncation notes rendered: Those validation PRs also fix a pre-existing bug in that repo's workflow, unrelated to this change: it passed |
Fourth validation run: the failure-count axisAdded azat-msft/gh-report-validation#5, which applies the opposite pressure from the oversized-details run: 5,000 failing tests with tiny diagnostics rather than a few with enormous ones. 5,000 failures produce a 27 KB summary — about 2.6% of GitHub's 1 MiB limit. Varying only the failure count (measured locally):
The size is flat; the 55-byte delta is just the wider count in the text. Notable result: I could not construct a summary that overflows purely from failure count. Both reporters bound their own sections — this one at 20 failures (12,750 B), TUnit's own block at a 50-row table (4,848 B). So failure count cannot push a run past the 1 MiB limit; only per-failure size can, which is exactly what the per-value clips and the per-section budget exist to contain. The two runs bracket the design: #4 shows the size axis is bounded at runtime, #5 shows the count axis is bounded by construction. One design question before this leaves draft
|
…t by rows The details budget was a per-section constant, but GitHub's 1 MiB cap applies to the whole GITHUB_STEP_SUMMARY file, which every test project in a job appends to. Twelve or so projects could therefore each spend a full budget and push the file past the cap, at which point GitHub drops the summary entirely. - Derive the budget from 80% of the 1 MiB cap and share it. The aggregate path divides it across modules; the direct path measures what sibling projects already wrote and claims only the remainder. - Report at the file level when the shared budget forced projects to render without details -- a per-module note is invisible inside a collapsed section. - Clip messages and stack traces by line count (30 each) as well as by length. A 200-frame trace of one-word frames sits under the character cap while being unreadable, so the character cap alone did not bound readability. Adds unit tests for the row limits, the budget arithmetic (including the unreadable-file fallback and the already-over-budget floor), and a 40-module aggregate that asserts the rendered file stays under GitHub's cap.
Validating with 30 test projects writing to one GITHUB_STEP_SUMMARY showed the budget was measuring the wrong thing. It capped the expanded details, but each project also writes several KB of headings, tables and failure lines, and the test framework appends its own ~5 KB block afterwards. Thirty projects landed at 1,018,161 bytes -- 97% of GitHub's 1 MiB cap, where GitHub drops the summary entirely rather than truncating it. - Reserve each project's non-detail overhead before dividing the budget, so the bound applies to the rendered file rather than to the diagnostics alone. - Condense a project's whole section to a single verdict line once the shared file nears the target, since at that point the per-project overhead is itself what would overflow the cap. The line still states the counts and says why it was condensed, so nothing is dropped silently. - Target 40% of the cap rather than 80%. This extension is not the only writer to the file: a test framework appending ~5 KB per project cannot be prevented by this reporter, only left room for. Thirty projects now render at 550,576 bytes (52.5%), down from 1,018,161 (97%).
Update: 30-project run found a budgeting bug, now fixedAdded azat-msft/gh-report-validation#6: 30 test projects appending to one The first run produced a 1,018,161 byte summary — 97% of GitHub's 1 MiB cap. A few more projects and GitHub would have discarded the entire summary, since an oversized summary is dropped rather than truncated. Root causeThe budget capped expanded details, but two other things scaled with project count and were outside it:
Fixes in this PR
Result (measured in CI, 30 projects)
Also in this updateRow limits on failure details. A character cap alone does not bound readability: a 200-frame stack trace of one-word frames sits under the 4,000-character cap while being unreadable. Messages and stack traces are now capped at 30 lines each as well, with the same explicit truncation marker. Validation matrix
Unit tests cover the row limits, the budget arithmetic (including the unreadable-file fallback and the already-over-budget floor), and a 40-module aggregate asserting the rendered file stays under the cap. Full suite: 1,108 passing. |
…lit reporter main split GitHubActionsSummaryReporter into partial classes (#10562), which moved the markdown builders this branch had changed. Re-applies the failure details work onto the new layout: capture and budget helpers stay with the reporter, the collapsible rendering and the shared-budget arithmetic move to the Markdown partial.
An earlier edit dropped the newline between the new failure-details row and the slow-test-notices row, merging them into one seven-cell row that markdownlint rejected (MD056).
|
One or more custom setup steps configured for this repository failed during this Copilot code review run: Setup steps run before each review. If the review above is missing context, or no review was posted at all, the failing step above may be the cause. See the workflow run for failure details, fix your setup steps configuration, and re-request a review. Note You can configure setup steps for Copilot code review separately from Copilot cloud agent with a |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 29 out of 29 changed files in this pull request and generated no new comments.
Suppressed comments (6)
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/GitHubActionsSummaryReporter.cs:333
- A framework can supply an empty or whitespace explanation together with a useful exception message. The null-coalescing expression selects that whitespace value, then
Clipturns it intonull, so the expanded failure omits the promised exception-message fallback. Treat whitespace explanations as absent.
GitHubActionsFailureDetails.Clip(failure.Value.Explanation ?? exception?.Message, GitHubActionsFailureDetails.MaxMessageLength, GitHubActionsFailureDetails.MaxMessageRows),
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/PACKAGE.md:44
- This table row also contains the slow-test option, so the package README renders both options as one malformed row and no longer documents
--report-gh-slow-test-noticescorrectly. Split them into separate rows.
| `--report-gh-failure-details on\|off` | Expand each failed test in the job summary into a collapsible section carrying its failure message, exception type, source location and stack trace | on |
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/GitHubActionsFailureDetails.cs:226
remainingBudgetis based onStream.Length, which is a byte count, but this comparison and subtraction use UTF-16 character counts. Since the summary is written as UTF-8, non-ASCII diagnostics can consume up to several times the reserved space and cross GitHub's byte limit even though the budget accepts them. Account for the UTF-8 byte count of each rendered block.
if (detailsBuilder.Length > remainingBudget)
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/GitHubActionsSummaryReporter.cs:234
- The shared-file size is measured before acquiring the exclusive append handle. Concurrent test-host processes can therefore all observe the same old length, each render up to the full remaining budget, and then serialize multiple oversized sections through
AppendStepSummaryWithRetryAsync; three first writers can exceed 1 MiB. Measure and build while holding the same cross-process lock used for the append.
int detailsBudget = GetRemainingDetailsBudget(_fileSystem, path!, _logger);
string markdown = detailsBudget <= 0 && IsSummaryNearLimit(_fileSystem, path!, _logger)
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/GitHubActionsSummaryArtifactPostProcessor.cs:65
- The aggregate always receives a fresh 40%-of-limit budget without subtracting content already present in
GITHUB_STEP_SUMMARY. If one workflow step runs multipledotnet testcommands (or concurrent aggregate processors use different aggregation IDs), every section can consume that budget and the upserts can collectively exceed 1 MiB. Size the step-summary variant against the existing file under the upsert lock; the standalone artifact can retain the full rendering.
string markdown = GitHubActionsSummaryReporter.BuildAggregateMarkdown(aggregate, _includeFailureDetails);
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/GitHubActionsSummaryReporter.Markdown.cs:166
- This reserve is only an estimate; it does not bound non-detail output. Once the reserve exhausts the details budget, the loop still emits a full section for every module, including uncapped assembly/test names and compact failure/slow-test lines. A sufficiently large aggregate can therefore exceed 1 MiB even with zero expanded details. Enforce the limit against the actual UTF-8 output and condense remaining modules when the budget is reached.
int overheadReserve = moduleCount * GitHubActionsFailureDetails.PerProjectOverheadReserve;
int detailsBudget = Math.Max(0, GitHubActionsFailureDetails.MaxSummaryLength - overheadReserve);
int perModuleBudget = detailsBudget / moduleCount;
|
One or more custom setup steps configured for this repository failed during this Copilot code review run: Setup steps run before each review. If the review above is missing context, or no review was posted at all, the failing step above may be the cause. See the workflow run for failure details, fix your setup steps configuration, and re-request a review. Note You can configure setup steps for Copilot code review separately from Copilot cloud agent with a |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 29 out of 29 changed files in this pull request and generated 2 comments.
Suppressed comments (2)
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/GitHubActionsCommandLineProvider.cs:36
- The new option is missing from the existing command-line provider test matrix in
GitHubActionsCommandLineProviderTests.cs: both sub-option dependency tests enumerate every prior sub-option, and each prior boolean option has invalid-value coverage. AddGitHubActionsFailureDetailscases so the new--report-ghdependency andon|offvalidation remain protected.
GitHubActionsCommandLineOptions.GitHubActionsGroups or GitHubActionsCommandLineOptions.GitHubActionsAnnotations or GitHubActionsCommandLineOptions.GitHubActionsStepSummary or GitHubActionsCommandLineOptions.GitHubActionsSlowTestNotices or GitHubActionsCommandLineOptions.GitHubActionsFailureDetails
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/GitHubActionsFailureDetails.cs:216
remainingBudgetis ultimately derived fromStream.Lengthand GitHub's byte limit, butStringBuilder.Lengthcounts UTF-16 code units. Non-ASCII failure messages are therefore undercharged (often by 2–4× in UTF-8), so the aggregate can satisfy this check yet produce a file over 1 MiB. Track UTF-8 byte counts consistently and assertEncoding.UTF8.GetByteCount(markdown)in the size tests.
if (detailsBuilder.Length > remainingBudget)
IDE0008 is enforced as an error in CI. The type was not apparent from the right-hand side because it comes from a LINQ projection, unlike the other 'var' uses here which are all 'new T(...)'.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 29 out of 29 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/GitHubActionsSummaryReporter.cs:232
- The budget is measured before acquiring the exclusive append handle. Parallel test-host processes can therefore all observe the same file length, each render up to the full remaining budget, and only then serialize their appends; the resulting file can exceed GitHub's limit and be dropped. Measure and render while holding the same interprocess lock used for the append, or re-check and re-render after acquiring it.
int detailsBudget = GetRemainingDetailsBudget(_fileSystem, path!, _logger);
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/GitHubActionsSummaryReporter.Markdown.cs:187
- When the calculated details budget reaches zero, this still appends the full table, failure list, and slow-test list for every module.
PerProjectOverheadReserveis only subtracted from the details allowance; it does not cap actual overhead, so a sufficiently large module count still produces a summary over 1 MiB. Enforce a file-level budget before each module and switch remaining modules to a bounded one-line verdict (with an explicit omission note).
if (AppendModuleMarkdown(builder, module, headingLevel: 3, includeFailureDetails, ref remainingBudget) > 0)
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/GitHubActionsSummaryReporter.cs:368
- An I/O error while measuring an existing summary is not equivalent to an empty file. Returning the full budget here can append hundreds of kilobytes to a file that is already near the cap, causing GitHub to drop the entire summary. Distinguish “file absent” from “measurement failed” and use a conservative/minimal rendering fallback for the latter.
return GitHubActionsFailureDetails.MaxTotalDetailsLength;
|
One or more custom setup steps configured for this repository failed during this Copilot code review run: Setup steps run before each review. If the review above is missing context, or no review was posted at all, the failing step above may be the cause. See the workflow run for failure details, fix your setup steps configuration, and re-request a review. Note You can configure setup steps for Copilot code review separately from Copilot cloud agent with a |
MSTEST0037 is enforced as an error in CI, which builds MSTest.Analyzers from source; the analyzer package restored locally predates the rule, so the local build did not flag it.
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa963c37-6214-46b9-9f7b-b084b293b544
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 37 out of 37 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/StepSummaryWriter.cs:737
- An external summary writer does not acquire this private lock. After
existingis read and the summary handle is closed, it can append while the temp file is being staged; this unconditional replacement then overwrites that append with the stale snapshot. Recheck the summary immediately before the swap and retry from a fresh read when it changed, asAppendWithLeadingNoticeCoreAsyncdoes.
cancellationToken.ThrowIfCancellationRequested();
_fileSystem.ReplaceFile(tempPath, Path);
The aggregated writer built its leading note before taking the writer lock, from its own module count alone. A job that mixes it with the direct per-project writer -- dotnet test aggregating in one step, a standalone test executable writing directly in another -- would then have the note understate how much of the file is fully reported. The note is now built from a factory the writer evaluates under its lock, given the sections already in the file. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa963c37-6214-46b9-9f7b-b084b293b544
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 37 out of 37 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/StepSummaryWriter.cs:315
- The length recheck still leaves a race before
ReplaceFile: a co-writer that does not use this lock can append afterGetSummaryLength()returns but before the swap, and that append is then lost with the old file. The code explicitly supports test frameworks that append without this lock, so hoisting the notice must avoid replacing the shared file unless the append/swap can be coordinated atomically with those writers; otherwise keep the notice in append-only content.
if (GetSummaryLength() is long lengthBeforeSwap && lengthBeforeSwap != lengthAtCapture)
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Caution
agentic threat detected
Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.
Details
Potential security threats were detected in the agent output.
Review the workflow run logs for details.
🤖 Automated content by GitHub Copilot. Generated by the Test Reviewer on PR (on open / sync) workflow. · auto · 124.7 AIC · ⌖ 1.29 AIC · ⊞ 16.9K · ◷
A direct per-project writer sharing the summary file counts project sections to say how many projects the file fully reports. Aggregate modules carried no marker, so a note it wrote later omitted every module of an aggregated run. Full modules are now marked, and the writer counts sections with its own section excised so a re-run does not count its previous modules on top of the ones the caller adds. Also pins four tests to the branch they exercise rather than the verdict alone. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa963c37-6214-46b9-9f7b-b084b293b544
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 37 out of 37 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/GitHubActionsSummaryReporter.Markdown.cs:324
- The shared budget starts only after the aggregate-level coverage table has already been appended.
CiCoverageSummary.Aggregatepreserves every module's coverage thresholds, so a large multi-module run can exceed the 1 MiB cap in this preamble before anySummaryStagecan degrade it; the condensed fallback renders the same preamble and is refused too. Please bring aggregate coverage under the byte budget (or explicitly omit/truncate it) before rendering modules.
var budget = SummaryBudget.ForAggregate(alreadyWrittenBytes + Encoding.UTF8.GetByteCount(builder.ToString()), moduleCount);
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/GitHubActionsSummaryReporter.cs:494
- This selects the condensed form solely from bytes already in the file. If the newly rendered full section itself crosses the cap (for example, through an unbounded coverage table), the writer returns
falseand the caller drops the project entirely; it never retriesBuildMinimalMarkdown. Please retry the minimal verdict on a size refusal so the documented full → compact → condensed degradation also handles an oversized current project.
var budget = SummaryBudget.ForProject(currentLength);
bool condense = budget.Stage is SummaryStage.Condensed or SummaryStage.Unlisted;
string markdown = condense
? BuildMinimalMarkdown(snapshot, assemblyName, _targetFrameworkMoniker.Value, exitCode)
: BuildMarkdown(snapshot, assemblyName, _targetFrameworkMoniker.Value, exitCode, coverage, _sections, _includeFailureDetails, budget);
Checking the message and the stack trace separately would pass with them rendered outside the code block, where an assertion diff's leading spaces and angle brackets are eaten as markdown and stack frames fold onto the line above. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa963c37-6214-46b9-9f7b-b084b293b544
|
Both B-grade findings from the expert test review are now addressed.
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
🤖 Automated content by GitHub Copilot. Generated by the Test Reviewer on PR (on open / sync) workflow. · auto · 196.5 AIC · ⌖ 0.999 AIC · ⊞ 16.9K · ◷
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa963c37-6214-46b9-9f7b-b084b293b544
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 37 out of 37 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/StepSummaryWriter.cs:315
- This final length check still leaves a TOCTOU window:
GetSummaryLength()closes its handle beforeReplaceFile, so a framework or other writer that does not use this lock can append between these calls and have its new bytes silently overwritten by the staged snapshot. This is especially likely while test frameworks append their own summaries concurrently. Keep a destination handle that denies writes (while permitting delete/replace) through the swap, or avoid replacing the shared file.
if (GetSummaryLength() is long lengthBeforeSwap && lengthBeforeSwap != lengthAtCapture)
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
The shared summary file is written by producers this extension does not control, so its size decided how large a buffer this writer allocated -- the int.MaxValue clamp allowed nearly 2 GiB, enough to end the test host with an OutOfMemoryException. Nothing either writing path can produce fits once the existing content alone is over the bound, so both now refuse before reading, with an absolute ceiling for callers that pass no bound of their own. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa963c37-6214-46b9-9f7b-b084b293b544
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 37 out of 37 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/StepSummaryWriter.cs:336
- The length recheck does not make this replacement safe against other summary producers. The summary handle has already been released, and foreign writers do not acquire this extension's lock file, so an append can land after this check and before
ReplaceFile; the replacement then silently deletes that content. Avoid replacing the shared file to hoist the notice (for example, append the notice instead), or use a protocol that can atomically coordinate with every writer—the current check leaves a TOCTOU window.
if (GetSummaryLength() is long lengthBeforeSwap && lengthBeforeSwap != lengthAtCapture)
Discounting this run's own section requires reading the whole shared file, and its size is set by producers this extension does not control. Past the ceiling it now reports the raw length instead of reading: that over-states the occupied space only by this run's previous block, and it makes the caller degrade rather than allocate. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa963c37-6214-46b9-9f7b-b084b293b544
🧵 Parallel-safety audit — PR #10633Nothing audited here touches process-global state, shared filesystem paths, or Re-run with
|
🧪 Expert test review — PR #10633
This advisory comment was generated automatically. Grades are heuristic
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 36 out of 37 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
src/Platform/Microsoft.Testing.Extensions.GitHubActionsReport/GitHubActionsSummaryReporter.Markdown.cs:439
- The condensed aggregate fallback loses module identity: modules with the same assembly/TFM (for example x64 and arm64 runs, or retry attempts) render identical labels even though the full path disambiguates them with architecture/attempt/session. Preserve architecture and include attempt/session when the identity is duplicated so readers can map each verdict to its module.
private static void AppendCondensedModuleLine(StringBuilder builder, CiRunSummaryModule module)
=> builder.Append(BuildCondensedLine(
module.AssemblyName,
module.TargetFramework,
module.TotalTests,
module.PassedTests,
module.FailedTests,
module.SkippedTests,
module.FailedTests > 0 || GitHubActionsExitCode.IndicatesFailure(module.ExitCode)));
src/Platform/SharedExtensionHelpers/SummaryReporterHelpers.cs:4
- This modified C# file is currently UTF-8 without a BOM, but
.editorconfig:66-67requiresutf-8-bomfor all C# files. Re-save it with the BOM so the change follows the repository's encoding convention.
Fixes #10591
What
The GitHub Actions step summary listed only the fully-qualified name of each failed test, so investigating a failure meant leaving the summary page for the Annotations tab (which has no stack trace) or the raw workflow log.
Each failed test is now expanded into a collapsible
<details>section:Namespace.TestClass.TestMethod— 2.40sException:
System.InvalidOperationExceptionLocation:
src/Calc.cs:42The summary line reuses the
test name — durationpresentation and duration formatting of the existing "Slowest tests" section, so the two are visually consistent.--report-gh-failure-details on|off(defaulton) restores the previous compact list. Existing GitHub error/warning annotations are unchanged.Bounding the output
GitHub caps a job summary at 1 MiB and drops it entirely when exceeded — it does not truncate. Every reduction is stated in the rendered output rather than applied silently.
[... truncated]appended[... truncated]appended[... truncated]appended[... truncated]appendedShowing the first 20 of N failed testsThe budget is shared, not per-section: the cap applies to the whole
GITHUB_STEP_SUMMARYfile, which every test project in a job appends to. The aggregate path divides the budget across modules; the direct path measures what sibling projects already wrote and claims only the remainder. Per-project overhead is reserved before dividing, so the bound applies to the rendered file rather than to the diagnostics alone. The final size check is made under the writer lock, in bytes, so two concurrent projects cannot both conclude they fit.Clipping happens at capture time, not render time, so an enormous stack trace never reaches the aggregation fragment written to disk.
Injection safety
<summary>are HTML-encoded — a generic test name likeT.Map<string,int>would otherwise parse as a tag and swallow the rest of the line.Testing
GitHubActionsSummaryReporterTestscovering the rendered section, the off-switch, the no-details fallback, HTML encoding, fence escaping, both row limits, all truncation paths, the budget arithmetic, and a 40-module aggregate asserting the rendered file stays under GitHub's cap.HelpInfoAllExtensionsTests--help/--infoexpectations updated.Docs (
PACKAGE.md,docs/glossary.md) and.xlflocalization files updated.Open question before this leaves draft
The limits above are hardcoded constants, chosen rather than measured — including
MaxFailures = 20and the 40%-of-cap target. The 40% figure exists because this extension is not the only writer to the summary file and cannot control what a test framework appends after it. Worth deciding whether any of these should be configurable options before merge.