fix: Settle the severity threshold contract and retry analyzer crashes - #163
fix: Settle the severity threshold contract and retry analyzer crashes#163tablackburn wants to merge 1 commit into
Conversation
…crashes Four related problems in Test-PSBuildScriptAnalysis, all of which needed a decision about the public surface rather than a local fix. ParseError is a fourth PSScriptAnalyzer severity, reported for a file that does not parse at all. It was counted by no threshold, so it escaped even the strictest validated value (Information): the record was printed and the build passed. A file the engine cannot read is at least as severe as an analyzer error, so ParseError is now counted with Error and fails every threshold except None. This is a behavioral change that can turn a passing consumer build red, so it has a migration-guide entry. 'Any' was documented in build.properties.ps1 but missing from the ValidateSet, so setting FailBuildOnSeverityLevel = 'Any' failed parameter binding instead of doing what the documentation promised. It is now accepted and wired to the branch that fails on any record. Additive, so no consumer action is required. The comment documenting all of this contradicted itself -- it opened with a list of valid values that did not include 'Any' and then described 'Any' -- and never said what Information does. Rewritten to describe every value. Finally, PSScriptAnalyzer can crash one of its own rules on an internal race that has nothing to do with the code being analyzed (PSScriptAnalyzer#1538, deferred to 2.0). A re-run succeeds, so the analysis is retried up to three times when a rule reports RULE_ERROR. Errors are captured during the retries rather than allowed to surface, because a consumer with ErrorActionPreference = 'Stop' would otherwise terminate on the first crash and never reach the second attempt; whatever the final attempt still reports is re-emitted, so a persistent failure behaves exactly as it did before. Per #147's own measurements a crashed rule does not cost findings, so this removes a flake without changing what a real failure does to anyone. Closes #144 Closes #147
There was a problem hiding this comment.
Pull request overview
This PR finalizes the FailBuildOnSeverityLevel / SeverityThreshold contract ahead of the 1.0.0 API freeze by (1) making ParseError findings participate in severity gating, (2) accepting the documented-but-rejected Any threshold value, and (3) reducing flakiness by retrying PSScriptAnalyzer rule-crash (RULE_ERROR) failures.
Changes:
- Count
ParseErroralongsideErrorso unparsable files fail all thresholds exceptNone. - Add
Anyto theSeverityThresholdValidateSetand implement the “fail on any diagnostic record” behavior. - Retry script analysis up to 3 times on
RULE_ERRORanalyzer crashes, re-surfacing persistent errors after the final attempt.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
PowerShellBuild/Public/Test-PSBuildScriptAnalysis.ps1 |
Implements Any threshold, counts ParseError with Error, and adds bounded retry for RULE_ERROR analyzer crashes. |
tests/Test-PSBuildScriptAnalysis.tests.ps1 |
Adds/updates Pester coverage for Any, ParseError gating, and retry behavior. |
PowerShellBuild/en-US/Messages.psd1 |
Adds localized warning text for analyzer crash retry. |
PowerShellBuild/build.properties.ps1 |
Updates consumer-facing documentation for severity threshold values and ParseError behavior. |
docs/migration-v0.8-to-v1.0.md |
Documents the behavioral change: unparsable files now fail the script-analysis gate. |
CHANGELOG.md |
Records the breaking/added/fixed user-facing changes for #144 and #147. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| # "None" reports findings but never fails the build. | ||
| # "Information" fails the build on Information, Warning, and Error records. | ||
| # "Warning" fails the build on Warning and Error records. | ||
| # "Error" fails the build only on Error records. |
|
CI is green on all legs, including Windows PowerShell 5.1. Two notes for whoever reviews: Squash-merge, please. The branch commit subject is 76 characters, over the repo's 72-character limit ( On #147, I took the issue's preferred approach rather than its alternative. #147 offered a hard failure ( |
Summary
Settles the
FailBuildOnSeverityLevel/SeverityThresholdcontract before the public APIfreezes at 1.0.0, and removes the analyzer flake folded in from #147.
ParseErrorrecords now fail the gate. They were counted by no threshold at all, so afile that does not parse escaped even the strictest validated value. Behavioral change —
has a migration-guide entry.
Anyis now an acceptedSeverityThreshold. It was documented but missing from theValidateSet, so setting it failed parameter binding. Additive.including
Information, which it never explained.Closes #144. Closes #147.
The four problems
1.
ParseErrorescaped every thresholdPSScriptAnalyzer's severity enum has four members;
Test-PSBuildScriptAnalysiscounted three.A
ParseError— a file that does not parse at all — satisfied no threshold, includingInformation, the strictest validated value. It was printed in the results table and the buildpassed. The most severe possible finding was the only one that could never fail a build.
ParseErroris now counted withError.2.
'Any'was documented but rejectedbuild.properties.ps1promised"Any" will fail the build on any diagnostic record, but theValidateSetwas'None', 'Error', 'Warning', 'Information'. Setting it failed parameterbinding. The catch-all branch that implements the behavior existed but was reachable only when
no threshold was supplied — and the consumer-facing
Analyzetask always supplies one, so noconsumer could ever reach it.
'Any'is now accepted and wired to that branch.3. The comment contradicted itself
It opened with "Valid values are Error, Warning, Information and None", then described
Any,and never said what
Informationdoes. Rewritten to describe all five values plusParseError.4. Analyzer rule crashes (#147)
PSScriptAnalyzer runs script rules in parallel against a process-wide unsynchronised singleton,
so a rule can crash on a race unrelated to the code under analysis
(PSScriptAnalyzer#1538, deferred
to 2.0). A re-run succeeds. The analysis is now retried up to three times on
RULE_ERROR.Errors are captured during the retries rather than allowed to surface, because a consumer with
$ErrorActionPreference = 'Stop'— what this repository itself does — would otherwiseterminate on the first crash and never reach the second attempt. Whatever the final attempt
still reports is re-emitted, so a persistent failure behaves exactly as it did before.
I took #147's preferred approach, not its alternative. The issue measured that a crashed rule
does not cost findings (100 cold runs, never fewer than the known-good count), so a crash is
not by itself a reason to fail — making a hard failure out of it would have cost some consumers
something to help others. This removes the flake without changing what a real failure does.
Test Plan
ParseErrorfails at theError,Warning,Information, andAnythresholds, andstill only reports at
NoneAnyfails onError,Warning, andInformationfindings, and passes with no findingsValidateSetassertion updated toAny,Error,Information,None,Warningerror, and does not retry a non-
RULE_ERRORfailure (1 invocation)Run locally under psake 4.9.1, matching this branch's pin.
One note for reviewers on the retry tests: their mocks set
$ErrorActionPreference = 'SilentlyContinue'inside the mock body. That is deliberate, not cargo cult — the real cmdlethonours the
-ErrorAction SilentlyContinuethe function passes, but a mock body inherits theambient preference, which
build.ps1sets toStopprocess-wide. Without it the mock throws onthe first attempt and the retry can never be exercised.
Breaking Changes
Yes — one.
ParseErrorrecords now fail every threshold exceptNone. A consumer whosebuild has an unparsable file was passing on 0.8.x and will now correctly fail. Documented in
docs/migration-v0.8-to-v1.0.mdwith a detection command:Anyis additive and the retry only removes a flake, so neither requires consumer action.Reviewer notes
SeverityThresholdis part of the public API being frozen.Adding an accepted value is additive and safe now; removing or redefining one after 1.0.0
would not be.
defaultswitch branch (unsupplied threshold) is deliberately left alone — changing it isoutside this issue.
Part of #120.