Make vacuous CI guards capable of failing - #19177
Conversation
Require the E2E matrix and lockfile registry guards to observe the inputs they validate before checking the input contents. This catches missing E2E matrix rows and lockfiles with no resolved registry entries instead of passing vacuously. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 19177Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 19177" |
There was a problem hiding this comment.
Pull request overview
Strengthens CI guards so missing tests, hang dumps, incomplete E2E matrices, and empty lockfiles fail reliably.
Changes:
- Validates nonzero TRX test counts and correct hang-dump filenames.
- Prevents retrying genuine test hangs.
- Adds lockfile and E2E matrix safeguards.
Show a summary per file
| File | Description |
|---|---|
.github/workflows/run-tests.yml |
Strengthens test-result and hang-dump validation. |
.github/workflows/auto-rerun-transient-ci-failures.js |
Classifies hang detection as test failure. |
tests/Infrastructure.Tests/WorkflowScripts/RunTestsWorkflowTests.cs |
Adds workflow guard tests. |
tests/Infrastructure.Tests/WorkflowScripts/AutoRerunTransientCiFailuresTests.cs |
Tests hang retry classification. |
extension/scripts/validate-lockfile-registry.cjs |
Rejects lockfiles without resolved entries. |
extension/src/test/e2eLaunchProfile.test.ts |
Tests lockfile and E2E matrix completeness. |
Review details
Tip
Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
- Files reviewed: 6/6 changed files
- Comments generated: 2
- Review effort level: Balanced
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Suppressed comments (4)
.github/workflows/run-tests.yml:730
- This still cannot detect hangs from the repository's actual MTP invocation:
eng/Testing.props:41passes--hangdump-type noneto every generatedmtpBaseArgs, anddocs/ci/mtp-args-pipeline.md:56documents that this disables hang-dump file creation. The regression test only creates synthetic_hang.dmpfiles, so it does not show that CI produces anything this glob can match. Either enable dump creation or classify hangs from evidence that exists with the current configuration (for example, the persisted MTP timeout exit code), with a regression test covering that production path.
# MTP's default hang dump file name is "<process>_<pid>_hang.dmp"; use the suffix so
# crash dumps and unrelated .dmp files do not fail otherwise-successful runs.
$hangDumpFiles = Get-ChildItem -Path $testResultsDir -Filter *_hang.dmp -Recurse -ErrorAction SilentlyContinue
.github/workflows/run-tests.yml:685
validFileCountis incremented beforeCounters/@totalis validated, so a TRX with a missing or non-numeric count leaves$totalTestCountat zero and is accepted wheneverallowZeroTestsis true. The opt-out should permit only a successfully parsedtotal="0"; count the file as valid only after its total has been parsed.
if ($countersNode -and $countersNode.total) {
$testCount = [int]$countersNode.total
$totalTestCount += $testCount
.github/workflows/run-tests.yml:47
- The generated test matrix always includes an
uncollected:*backstop (eng/scripts/scan-test-partitions-from-source.ps1:123-124), which runs with--filter-not-trait "Partition=*"(eng/scripts/build-test-matrix.ps1:206-210) and can legitimately match zero tests;eng/Testing.props:41therefore already ignores MTP exit code 8. None of the reusable-workflow callers pass this new input, so those empty generated shards now fail despite the opt-out. Propagate an explicit matrix flag for the uncollected entry and pass it asallowZeroTestsfromtests.yml(and any generated-shard caller that needs it).
This issue also appears on line 728 of the same file.
# Only set this for generated shards that are expected to be empty. Normal CI should
# fail when a named shard produces .trx files that report zero tests.
allowZeroTests:
required: false
type: boolean
default: false
.github/workflows/run-tests.yml:660
- Because the configured
--hangdump-type noneproduces no dump file, a tolerated MTP timeout (exit code 3) with no TRX or recording still falls through to “Tests may not have run” and fails quarantine mode. Include the already-read MTP exit code in this classification (or enable dump creation) so the intended timeout path does not depend on an artifact CI suppresses.
# MTP's default hang dump file name is "<process>_<pid>_hang.dmp"; use the suffix so
# crash dumps and unrelated .dmp files stay out of this timeout-only classification.
$hasHangDumps = Get-ChildItem -Path "${{ github.workspace }}/testresults" -Filter *_hang.dmp -Recurse -ErrorAction SilentlyContinue
if ($hasRecordings -or $hasHangDumps) {
- Files reviewed: 6/6 changed files
- Comments generated: 1
- Review effort level: Balanced
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Suppressed comments (1)
tests/Infrastructure.Tests/WorkflowScripts/RunTestsWorkflowTests.cs:334
Infrastructure.Testsalready providesTemporaryWorkspace(used byBuildTestMatrixTests), but this recreates scratch-directory allocation and cleanup manually. That bypasses the shared workspace cleanup/preservation behavior and the repository temp-directory abstraction. UseTemporaryWorkspace.Create(_output)and dispose it rather than maintaining this helper.
string scratchRoot = Path.Combine(RepoRoot.Path, "artifacts", "tmp", nameof(RunTestsWorkflowTests));
Directory.CreateDirectory(scratchRoot);
string scratchDirectory = Path.Combine(scratchRoot, Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(scratchDirectory);
- Files reviewed: 11/11 changed files
- Comments generated: 0 new
- Review effort level: Balanced
extension/scripts/validate-lockfile-registry.cjs allowed any resolved line that merely *contained* the internal feed string, so a hostile tarball URL could smuggle it into a path, hostname suffix, or URL fragment, or downgrade the scheme to plaintext http, and still pass. Parse each resolved URL with `new URL()` and require exact protocol (https:), exact hostname (pkgs.dev.azure.com), and a pathname prefix match. Any unparseable URL is rejected, not skipped. tests.yml duplicated the same vulnerable substring check inline (in both extension_tests_win and extension_bootstrap_linux) instead of calling the shared script, so it would have kept the bypass alive and let the two implementations drift apart even after the .cjs fix. Both steps now invoke the shared script, matching how extension-e2e-tests.yml already does it. Extend the existing spawnSync-based test harness in e2eLaunchProfile.test.ts with cases for the four bypass shapes (path injection, hostname suffix, http downgrade, fragment injection) and two genuine internal-feed URLs (plain + scoped package, with a sha512 fragment). Add a workflow-parsing test in RunTestsWorkflowTests.cs asserting every "Validate lockfile registries" step in tests.yml invokes the shared script rather than an inline substring check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Suppressed comments (2)
.github/workflows/run-tests.yml:685
allowZeroTestscurrently treats a missing or unparseableCounters/@totalas an explicitly reported zero.validFileCountis incremented before this cast, and the catch only warns, so a TRX withtotal="invalid"(or noCounters) exits successfully when the opt-out is enabled. Track whether a test count was successfully parsed and fail if none was; the opt-out should cover a reportedtotal="0", not malformed result data.
if ($countersNode -and $countersNode.total) {
$testCount = [int]$countersNode.total
$totalTestCount += $testCount
.github/workflows/auto-rerun-transient-ci-failures.js:60
- The new
Verify test results existstep can now fail for a deterministic zero-test run, but it is absent fromtestExecutionFailureStepPatterns. Consequently, an unrelated feed signature anywhere in the job log still enables the broad network override and reruns that zero-test job—the same false-rerun path this change closes for hang-dump detection. Classify this step as a test-execution failure and add the corresponding regression case.
/^Check for hang dump files$/i,
- Files reviewed: 11/11 changed files
- Comments generated: 2
- Review effort level: Balanced
URL.hostname excludes the port, so comparing hostname accepted https://pkgs.dev.azure.com:444/... and let Yarn fetch from an unapproved endpoint on the right host. Compare URL.origin instead, which normalizes the default :443 away so the legitimate explicit-default form still passes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 487b899d-d79d-4349-a84b-a026861bde7f
PR microsoft#19177 parameterized run-tests.yml's zero-test guard, but specialized-test-runner.yml still reads its opt-out from matrix.tests.allowZeroTests. That key is never emitted by the runsheet rows generated in eng/SpecializedTestRunsheetBuilderBase.targets (rows only contain label, project, os, command, properties, mtpBaseArgs), so the expression `matrix.tests.allowZeroTests || false` always evaluates to false. This re-vacuums the guard for the quarantined and outerloop test matrices, which is a regression: those matrices are built exclusively by SpecializedTestRunsheetBuilderBase.targets, and that file unconditionally appends /p:IgnoreZeroTestResult=true to every generated row's test command, since a specialized test class may only exist on some OSes. Pass allowZeroTests: true directly at the specialized-test-runner.yml call site so it mirrors the unconditional IgnoreZeroTestResult=true tolerance already baked into every row this workflow consumes. This does not widen the opt-out anywhere else: tests.yml's separate call sites still read matrix.allowZeroTests from build-test-matrix.ps1's distinct matrix, which explicitly sets it only for the generated shards that need it. Verified by importing the real, unmodified eng/SpecializedTestRunsheetBuilderBase.targets into a throwaway harness project and invoking its RunTests target directly: the generated runsheet row JSON has no allowZeroTests key at all, and its command line unconditionally includes /p:IgnoreZeroTestResult=true. Added two regression tests to RunTestsWorkflowTests.cs: - SpecializedTestRunnerWorkflowAlwaysOptsIntoAllowZeroTests asserts the call site passes allowZeroTests: true and does not read it from the matrix row. Fails against the pre-fix expression. - SpecializedTestRunsheetBuilderUnconditionallyIgnoresZeroTestResult asserts the /p:IgnoreZeroTestResult=true line in the targets file has no Condition, documenting the invariant the fix relies on. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Suppressed comments (2)
.github/workflows/run-tests.yml:685
allowZeroTestsalso accepts a count-less or partially written TRX. A file containing only a valid<TestRun>incrementsvalidFileCount, leavestotalTestCountat zero, and then reaches the opt-out branch as though it were a legitimate empty shard. Only treat a TRX as valid for this guard after itsCounters/@totalvalue has been found and parsed, so the opt-out cannot hide truncated result files.
$countersNode = $trxContent.TestRun.ResultSummary.Counters
if ($countersNode -and $countersNode.total) {
$testCount = [int]$countersNode.total
$totalTestCount += $testCount
tests/Infrastructure.Tests/WorkflowScripts/RunTestsWorkflowTests.cs:161
- This only checks the physical line containing
IgnoreZeroTestResult; a multilineConditionattribute or a condition on the containingPropertyGroupcan make the setting conditional while this test remains green. Since the blanket workflow opt-out depends on every generated row receiving this property, parse the targets XML and verify the command element and its relevant ancestors cannot conditionally omit it.
int commandLineIndex = Array.FindIndex(lines, line => line.Contains("/p:IgnoreZeroTestResult=true", StringComparison.Ordinal));
Assert.True(commandLineIndex >= 0, $"Could not find the /p:IgnoreZeroTestResult=true line in {s_specializedTestRunsheetBuilderTargetsPath}.");
Assert.DoesNotContain("Condition", lines[commandLineIndex], StringComparison.Ordinal);
- Files reviewed: 11/11 changed files
- Comments generated: 0 new
- Review effort level: Balanced
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
Description
Five CI guards in this repo could not fail. Each is individually plausible, which is why they survived review, and each reported success on input it never actually examined.
Verify test results existonly ran when failures were already being ignored. Its condition wasif: ${{ inputs.ignoreTestFailures }}, so on every normal CI run — the runs whose results we act on — the step was skipped entirely. It now runs whenever a test step actually executed.A shard reporting zero tests passed.
.trxfiles were counted but the tests inside them were not, so a run that executed nothing was indistinguishable from a run that executed everything and passed. The step now sumsCounters/@totalacross the shard's.trxfiles and fails when the sum is zero. AnallowZeroTestsinput exists for generated shards that are legitimately empty, and it defaults tofalseso the failure is opt-out rather than opt-in. The success line now prints the test count as well as the file count.The hang dump detectors matched a filename MTP never writes. Both the classification path and the final reporting path globbed
*hangdump*, but MTP writes<process>_<pid>_hang.dmp. The pattern never matched, so the step printed✓ No hang dump files detectedseconds after dumps were written to the directory it was searching. Both now match*_hang.dmp, which is deliberately narrow enough to keep crash dumps — which can appear during cleanup on a fully passing run — out of a timeout-only classification.The transient-rerun classifier could rerun a real hang.
Check for hang dump fileswas not intestExecutionFailureStepPatterns, so an unrelated feed blip elsewhere in the log was enough to retry a job that had genuinely timed out. It is now classified as a test-execution failure.The lockfile registry guard passed on an empty lockfile.
validate-lockfile-registry.cjsfilteredyarn.lockforresolvedentries and then checked that none pointed outside the internal feed. With zero resolved entries the filter is empty, the check is vacuous, and the script exits 0 — including when the file is deleted. It now requires at least one resolved entry before validating them, and the E2E lockfile test asserts the same precondition.The shared shape is asserting over a filtered collection without first asserting the collection is non-empty. That is worth naming, because it will recur.
Related: the E2E workflow guard now compares the full expected matrix rows against the committed
src/test-e2e/*.e2e.test.tsspecs, so deleting a shard row fails immediately instead of silently dropping coverage.No issue.
Testing
Every guard was tested by deleting or emptying the input it is supposed to protect and confirming it now fails. A guard fix that cannot itself fail would be the joke writing itself.
edge-casesrows from.github/workflows/extension-e2e-tests.yml;runs every E2E spec from the workflow matrixfailed on the missing Linux/Windows rows.resolvedlines fromextension/yarn.lock;installs the E2E runner dependencies from the internal npm feedfailed withExpected extension/yarn.lock to contain resolved registry entries.node scripts/validate-lockfile-registry.cjsexited 1 withextension/yarn.lock does not contain any resolved entries.*hangdump*detector,RunTestsWorkflowTests.HangDumpDetectorsMatchMtpHangDumpFilesAndIgnoreOtherDumpsmatched onlynot-a-dump-hangdump.txtand misseddotnet_6079_hang.dmpanddocker_6110_hang.dmp.DoesNotApplyBroadNetworkOverrideWhenHangDumpDetectionFailedretriedCheck for hang dump fileswhen the log also contained a dnceng feed error.TestResultValidationFailsWhenTrxFilesContainNoTestssaw exit0against a.trxreportingtotal: 0.3 passing);node scripts/validate-lockfile-registry.cjsexited 0;corepack yarn lintpassed.docker_6110_hang.dmpanddotnet_6079_hang.dmp, and an empty results directory still reports clean (empty_count=0).RunTestsWorkflowTestspasses 5/5, including the zero-test case failing validation and atotal="3"case passing;AutoRerunTransientCiFailuresTestspassed (100 succeeded);node --check .github/workflows/auto-rerun-transient-ci-failures.jspassed.Checklist