Add Validate-DocsMsToc.ps1 guardrail to fail-closed on a corrupt overview ToC - #16328
Conversation
Adds a fail-closed structural guardrail run immediately after Update-DocsMsToc.ps1 writes the unified Docs.MS ToC. The validator exits non-zero when the emitted toc.yml is structurally broken or suspiciously degraded, plus Pester tests and fixtures.
|
Thank you for your contribution Pavel Shageev (@shageev-msft)! We will review the pull request and get back to you soon. |
|
@microsoft-github-policy-service agree company="Microsoft" |
There was a problem hiding this comment.
Pull request overview
This PR adds a new fail-closed validation step for the unified Docs.MS toc.yml generated by Update-DocsMsToc.ps1, aiming to prevent structurally corrupted or suspiciously degraded ToCs from being published. It introduces a dedicated validator script plus a Pester test suite with fixtures to cover key corruption and collapse scenarios.
Changes:
- Add
Validate-DocsMsToc.ps1to validate emitted ToC YAML (raw-text corruption detection + parsed structural checks + optional drift gate). - Wire the validator into
Update-DocsMsToc.ps1immediately after writingtoc.yml, failing the pipeline on violations. - Add Pester tests and YAML fixtures covering happy path, known corruption shapes, structural collapse, soft-fail behavior, and drift detection.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| eng/common/scripts/Validate-DocsMsToc.ps1 | New guardrail script to validate generated Docs.MS ToC structure and detect known corruption patterns. |
| eng/common/scripts/Update-DocsMsToc.ps1 | Invokes the validator after writing the unified toc.yml and fails publishing on validation errors. |
| eng/common-tests/Validate-DocsMsToc.Tests.ps1 | New Pester suite exercising validator behavior across key failure modes and options. |
| eng/common-tests/Validate-DocsMsToc.Fixtures/good.yml | Valid ToC fixture for baseline passing behavior. |
| eng/common-tests/Validate-DocsMsToc.Fixtures/bad-only-other.yml | Fixture representing structural collapse to only “Other”. |
| eng/common-tests/Validate-DocsMsToc.Fixtures/bad-children-mapping.yml | Fixture reproducing the “children rendered as mappings” corruption shape. |
| eng/common-tests/Validate-DocsMsToc.Fixtures/bad-empty-name.yml | Fixture for empty service-name detection. |
| $badChildPattern = '(?ms)^\s*children:\s*\r?\n(?:\s*-\s*(?:Length|Chars|value)\s*:|\s*-\s*!!?map\b)' | ||
| if ($rawYaml -match $badChildPattern) { | ||
| Add-Violation 'TOC010' ` | ||
| "`children` was serialized as a mapping (probable powershell-yaml regression). First match near offset $($Matches[0].Length)." | ||
| } |
There was a problem hiding this comment.
Fixed in 3fcf3a3 - switched to [regex]::Match(...) and now report the actual .Index.
| $childBlockPattern = '(?ms)^( +)children:\s*\r?\n((?:\1 +- .*\r?\n)+)' | ||
| foreach ($m in [regex]::Matches($rawYaml, $childBlockPattern)) { | ||
| $block = $m.Groups[2].Value | ||
| foreach ($line in $block -split "`r?`n") { | ||
| if ($line -match '^\s*-\s*\{' -or $line -match '^\s*-\s*\S+\s*:\s*\S') { | ||
| Add-Violation 'TOC011' "`children` item is not a scalar string: '$($line.Trim())'" | ||
| break | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Fixed in 3fcf3a3 - relaxed the pattern to \1 *- (same or greater indent) and constrained the wildcard to a single line ([^\r\n]*) so unrelated items: blocks are no longer swept in. TOC011 now actually fires on bad-children-mapping.yml.
| if ($LASTEXITCODE -ne 0) { | ||
| Write-Error "Validate-DocsMsToc reported violations; refusing to publish a corrupt ToC." | ||
| exit $LASTEXITCODE | ||
| } |
There was a problem hiding this comment.
Fixed in 3fcf3a3 - reindented to 2 spaces to match surrounding style.
- TOC010: report actual regex match offset (was reporting match length) - TOC011: fix childBlockPattern so items at same indent as children: are captured; also constrain wildcard to a single line so unrelated items: blocks are not swept in - Update-DocsMsToc.ps1: use 2-space indent to match surrounding style
What
Adds
eng/common/scripts/Validate-DocsMsToc.ps1, a fail-closed structuralguardrail run immediately after
Update-DocsMsToc.ps1writes the unifiedDocs.MS ToC. The validator exits non-zero (and the publish pipeline fails)
when the emitted
toc.ymlis structurally broken or suspiciously degraded.Why
Follow-up to PR #15244 "Update-DocsMsToc.ps1 - Use powershell-yaml 0.4.12,
include fixes for breaking behavior change". That PR mitigated a specific
breaking change in
powershell-yaml'sConvertTo-Yamlthat causedchildrenitems to be serialized as mappings instead of plain scalars. As aresult, on the affected publish, every named service node on the Azure SDK
for .NET overview was dropped on render and only the
Other / Uncategorized Packagessink survived — on both Latest and Preview monikers.Two contributing factors made this a silent, customer-detected failure
rather than a build-time failure:
Update-DocsMsToc.ps1had no structural assertion on its own output, sothe corrupted YAML was committed and published as if it were valid.
Install-ModuleIfNotInstalledaccepts any cached module version>= requested, so a newer breakingpowershell-yamlalready present onthe agent was silently preferred over the requested version. (Tracked
separately — see follow-up note below.)
This PR closes the first gap: produce a loud, actionable failure at the
moment the bad bytes are emitted, before they hit the content repo.
What the validator catches
childrenwas serialized as a mapping (the exact PR #15244 corruption shape) — detected on the raw YAML textchildrenitem is not a scalar stringReferenceMinTopLevelNodesnon-Othertop-level nodes (the original incident's symptom)Otheris the only top-level nodeMaxNodeCountDropPercent% versus the previous good ToC (only if-PreviousTocPathset)The TOC010/TOC011 check is intentionally done by raw-text scanning, not
just by
ConvertFrom-Yaml, so it remains trustworthy even when thepowershell-yamlmodule installed on the agent has a behavioral regressionthat broke
ConvertTo-Yamlin the first place.How it's wired
Update-DocsMsToc.ps1calls the validator immediately afterSet-Content -Path $OutputLocation -Value $outputYaml. Non-zero exit failsthe pipeline. Every per-language repo that consumes
eng/commonpicks thegate up automatically through the standard sync flow, so the protection
applies to every language pipeline, not just .NET.
Rollout
Default behavior is enforce. For teams that want a calibration window
the validator accepts
-SoftFail, which logs violations but exits 0.Suggested rollout if needed:
-SoftFailin callers for one publish cycle.intentional changes.
-SoftFailfrom the call inUpdate-DocsMsToc.ps1to enforce.(The patch in this PR enforces from the start; the soft-fail path is left
available for the per-language repos to opt into during sync if their
custom hooks need it.)
Tests
New Pester suite under
eng/common-tests/:Validate-DocsMsToc.Tests.ps1— six tests covering: happy path,structural collapse (TOC040/TOC041), children-mapping corruption
(TOC010/TOC011), empty service name (TOC030),
-SoftFailreporting mode,and the drift gate (TOC050) both above and below threshold.
Validate-DocsMsToc.Fixtures/— four smalltoc.ymlfixtures, includingone that reproduces the exact PR Update-DocsMsToc.ps1 - Use powershell-yaml 0.4.12, include fixes for breaking behavior change #15244 corruption shape (
childrenitems emitted as
{ Length: …, Chars: … }mappings).Run locally:
Follow-up (separate PR, not in this one)
eng/common/scripts/Helpers/PSModule-Helpers.ps1—moduleIsInstalledcurrently treats a cached module as satisfying the request when its version
is
>= requested, butinstallModuleitself uses-RequiredVersion. Thatasymmetry is what let a newer breaking
powershell-yamlslip in. Afollow-up PR should tighten
moduleIsInstalledto-eqsemantics (or adda
-RequiredVersionswitch) so the helper actually matches its own installbehavior.
Files changed